001package io.prometheus.metrics.config;
002
003import io.prometheus.metrics.annotations.StableApi;
004import java.util.HashMap;
005import java.util.Map;
006import javax.annotation.Nullable;
007
008/**
009 * Properties for configuring the OpenTelemetry exporter.
010 *
011 * <p>These properties can be configured via {@code prometheus.properties}, system properties, or
012 * programmatically.
013 *
014 * <p>All properties are prefixed with {@code io.prometheus.exporter.opentelemetry}.
015 *
016 * <p>Available properties:
017 *
018 * <ul>
019 *   <li>{@code protocol} - OTLP protocol: {@code "grpc"} or {@code "http/protobuf"}
020 *   <li>{@code endpoint} - OTLP endpoint URL
021 *   <li>{@code headers} - HTTP headers for outgoing requests
022 *   <li>{@code intervalSeconds} - Export interval in seconds
023 *   <li>{@code timeoutSeconds} - Request timeout in seconds
024 *   <li>{@code serviceName} - Service name resource attribute
025 *   <li>{@code serviceNamespace} - Service namespace resource attribute
026 *   <li>{@code serviceInstanceId} - Service instance ID resource attribute
027 *   <li>{@code serviceVersion} - Service version resource attribute
028 *   <li>{@code resourceAttributes} - Additional resource attributes
029 * </ul>
030 *
031 * @see <a
032 *     href="https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/">OpenTelemetry
033 *     SDK Environment Variables</a>
034 */
035@StableApi
036public class ExporterOpenTelemetryProperties {
037
038  // See
039  // https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md
040  private static final String PROTOCOL = "protocol"; // otel.exporter.otlp.protocol
041  private static final String ENDPOINT = "endpoint"; // otel.exporter.otlp.endpoint
042  private static final String HEADERS = "headers"; // otel.exporter.otlp.headers
043  private static final String INTERVAL_SECONDS = "interval_seconds"; // otel.metric.export.interval
044  private static final String TIMEOUT_SECONDS = "timeout_seconds"; // otel.exporter.otlp.timeout
045  private static final String SERVICE_NAME = "service_name"; // otel.service.name
046  private static final String SERVICE_NAMESPACE = "service_namespace";
047  private static final String SERVICE_INSTANCE_ID = "service_instance_id";
048  private static final String SERVICE_VERSION = "service_version";
049  private static final String RESOURCE_ATTRIBUTES =
050      "resource_attributes"; // otel.resource.attributes
051  private static final String PRESERVE_NAMES = "preserve_names";
052  private static final String PREFIX = "io.prometheus.exporter.opentelemetry";
053
054  @Nullable private final String endpoint;
055  @Nullable private final String protocol;
056  private final Map<String, String> headers;
057  @Nullable private final String interval;
058  @Nullable private final String timeout;
059  @Nullable private final String serviceName;
060  @Nullable private final String serviceNamespace;
061  @Nullable private final String serviceInstanceId;
062  @Nullable private final String serviceVersion;
063  private final Map<String, String> resourceAttributes;
064  @Nullable private final Boolean preserveNames;
065
066  private ExporterOpenTelemetryProperties(
067      @Nullable String protocol,
068      @Nullable String endpoint,
069      Map<String, String> headers,
070      @Nullable String interval,
071      @Nullable String timeout,
072      @Nullable String serviceName,
073      @Nullable String serviceNamespace,
074      @Nullable String serviceInstanceId,
075      @Nullable String serviceVersion,
076      Map<String, String> resourceAttributes,
077      @Nullable Boolean preserveNames) {
078    this.protocol = protocol;
079    this.endpoint = endpoint;
080    this.headers = headers;
081    this.interval = interval;
082    this.timeout = timeout;
083    this.serviceName = serviceName;
084    this.serviceNamespace = serviceNamespace;
085    this.serviceInstanceId = serviceInstanceId;
086    this.serviceVersion = serviceVersion;
087    this.resourceAttributes = resourceAttributes;
088    this.preserveNames = preserveNames;
089  }
090
091  @Nullable
092  public String getProtocol() {
093    return protocol;
094  }
095
096  @Nullable
097  public String getEndpoint() {
098    return endpoint;
099  }
100
101  public Map<String, String> getHeaders() {
102    return headers;
103  }
104
105  @Nullable
106  public String getInterval() {
107    return interval;
108  }
109
110  @Nullable
111  public String getTimeout() {
112    return timeout;
113  }
114
115  @Nullable
116  public String getServiceName() {
117    return serviceName;
118  }
119
120  @Nullable
121  public String getServiceNamespace() {
122    return serviceNamespace;
123  }
124
125  @Nullable
126  public String getServiceInstanceId() {
127    return serviceInstanceId;
128  }
129
130  @Nullable
131  public String getServiceVersion() {
132    return serviceVersion;
133  }
134
135  public Map<String, String> getResourceAttributes() {
136    return resourceAttributes;
137  }
138
139  /**
140   * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
141   * When {@code false} (default), standard OTel name normalization is applied (stripping unit
142   * suffix).
143   */
144  @Nullable
145  public Boolean getPreserveNames() {
146    return preserveNames;
147  }
148
149  /**
150   * Note that this will remove entries from {@code propertySource}. This is because we want to know
151   * if there are unused properties remaining after all properties have been loaded.
152   */
153  static ExporterOpenTelemetryProperties load(PropertySource propertySource)
154      throws PrometheusPropertiesException {
155    String protocol = Util.loadString(PREFIX, PROTOCOL, propertySource);
156    String endpoint = Util.loadString(PREFIX, ENDPOINT, propertySource);
157    Map<String, String> headers = Util.loadMap(PREFIX, HEADERS, propertySource);
158    String interval = Util.loadStringAddSuffix(PREFIX, INTERVAL_SECONDS, propertySource, "s");
159    String timeout = Util.loadStringAddSuffix(PREFIX, TIMEOUT_SECONDS, propertySource, "s");
160    String serviceName = Util.loadString(PREFIX, SERVICE_NAME, propertySource);
161    String serviceNamespace = Util.loadString(PREFIX, SERVICE_NAMESPACE, propertySource);
162    String serviceInstanceId = Util.loadString(PREFIX, SERVICE_INSTANCE_ID, propertySource);
163    String serviceVersion = Util.loadString(PREFIX, SERVICE_VERSION, propertySource);
164    Map<String, String> resourceAttributes =
165        Util.loadMap(PREFIX, RESOURCE_ATTRIBUTES, propertySource);
166    Boolean preserveNames = Util.loadBoolean(PREFIX, PRESERVE_NAMES, propertySource);
167    return new ExporterOpenTelemetryProperties(
168        protocol,
169        endpoint,
170        headers,
171        interval,
172        timeout,
173        serviceName,
174        serviceNamespace,
175        serviceInstanceId,
176        serviceVersion,
177        resourceAttributes,
178        preserveNames);
179  }
180
181  public static Builder builder() {
182    return new Builder();
183  }
184
185  public static class Builder {
186
187    @Nullable private String protocol;
188    @Nullable private String endpoint;
189    private final Map<String, String> headers = new HashMap<>();
190    @Nullable private String interval;
191    @Nullable private String timeout;
192    @Nullable private String serviceName;
193    @Nullable private String serviceNamespace;
194    @Nullable private String serviceInstanceId;
195    @Nullable private String serviceVersion;
196    private final Map<String, String> resourceAttributes = new HashMap<>();
197    @Nullable private Boolean preserveNames;
198
199    private Builder() {}
200
201    /**
202     * The OTLP protocol to use.
203     *
204     * <p>Supported values: {@code "grpc"} or {@code "http/protobuf"}.
205     *
206     * <p>See OpenTelemetry's <a
207     * href="https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_protocol">OTEL_EXPORTER_OTLP_PROTOCOL</a>.
208     */
209    public Builder protocol(String protocol) {
210      if (!protocol.equals("grpc") && !protocol.equals("http/protobuf")) {
211        throw new IllegalArgumentException(
212            protocol + ": Unsupported protocol. Expecting grpc or http/protobuf");
213      }
214      this.protocol = protocol;
215      return this;
216    }
217
218    /**
219     * The OTLP endpoint to send metric data to.
220     *
221     * <p>The default depends on the protocol:
222     *
223     * <ul>
224     *   <li>{@code "grpc"}: {@code "http://localhost:4317"}
225     *   <li>{@code "http/protobuf"}: {@code "http://localhost:4318/v1/metrics"}
226     * </ul>
227     *
228     * <p>See OpenTelemetry's <a
229     * href="https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_metrics_endpoint">OTEL_EXPORTER_OTLP_METRICS_ENDPOINT</a>.
230     */
231    public Builder endpoint(String endpoint) {
232      this.endpoint = endpoint;
233      return this;
234    }
235
236    /**
237     * Add an HTTP header to be applied to outgoing requests. Call multiple times to add multiple
238     * headers.
239     *
240     * <p>See OpenTelemetry's <a
241     * href="https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_headers">OTEL_EXPORTER_OTLP_HEADERS</a>.
242     */
243    public Builder header(String name, String value) {
244      this.headers.put(name, value);
245      return this;
246    }
247
248    /**
249     * The interval between the start of two export attempts. Default is 60 seconds.
250     *
251     * <p>Like OpenTelemetry's <a
252     * href="https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#periodic-metric-reader">OTEL_METRIC_EXPORT_INTERVAL</a>
253     * (which defaults to 60000 milliseconds), but specified in seconds rather than milliseconds.
254     */
255    public Builder intervalSeconds(int intervalSeconds) {
256      if (intervalSeconds <= 0) {
257        throw new IllegalArgumentException(intervalSeconds + ": Expecting intervalSeconds > 0");
258      }
259      this.interval = intervalSeconds + "s";
260      return this;
261    }
262
263    /**
264     * The timeout for outgoing requests. Default is 10.
265     *
266     * <p>Like OpenTelemetry's <a
267     * href="https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_metrics_timeout">OTEL_EXPORTER_OTLP_METRICS_TIMEOUT</a>,
268     * but in seconds rather than milliseconds.
269     */
270    public Builder timeoutSeconds(int timeoutSeconds) {
271      if (timeoutSeconds <= 0) {
272        throw new IllegalArgumentException(timeoutSeconds + ": Expecting timeoutSeconds > 0");
273      }
274      this.timeout = timeoutSeconds + "s";
275      return this;
276    }
277
278    /**
279     * The {@code service.name} resource attribute.
280     *
281     * <p>If not explicitly specified, {@code client_java} will try to initialize it with a
282     * reasonable default, like the JAR file name.
283     *
284     * <p>See {@code service.name} in OpenTelemetry's <a
285     * href="https://opentelemetry.io/docs/specs/otel/resource/semantic_conventions/#service">Resource
286     * Semantic Conventions</a>.
287     */
288    public Builder serviceName(String serviceName) {
289      this.serviceName = serviceName;
290      return this;
291    }
292
293    /**
294     * The {@code service.namespace} resource attribute.
295     *
296     * <p>See {@code service.namespace} in OpenTelemetry's <a
297     * href="https://opentelemetry.io/docs/specs/otel/resource/semantic_conventions/#service-experimental">Resource
298     * Semantic Conventions</a>.
299     */
300    public Builder serviceNamespace(String serviceNamespace) {
301      this.serviceNamespace = serviceNamespace;
302      return this;
303    }
304
305    /**
306     * The {@code service.instance.id} resource attribute.
307     *
308     * <p>See {@code service.instance.id} in OpenTelemetry's <a
309     * href="https://opentelemetry.io/docs/specs/otel/resource/semantic_conventions/#service-experimental">Resource
310     * Semantic Conventions</a>.
311     */
312    public Builder serviceInstanceId(String serviceInstanceId) {
313      this.serviceInstanceId = serviceInstanceId;
314      return this;
315    }
316
317    /**
318     * The {@code service.version} resource attribute.
319     *
320     * <p>See {@code service.version} in OpenTelemetry's <a
321     * href="https://opentelemetry.io/docs/specs/otel/resource/semantic_conventions/#service-experimental">Resource
322     * Semantic Conventions</a>.
323     */
324    public Builder serviceVersion(String serviceVersion) {
325      this.serviceVersion = serviceVersion;
326      return this;
327    }
328
329    /**
330     * Add a resource attribute. Call multiple times to add multiple resource attributes.
331     *
332     * <p>See OpenTelemetry's <a
333     * href="https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration">OTEL_RESOURCE_ATTRIBUTES</a>.
334     */
335    public Builder resourceAttribute(String name, String value) {
336      this.resourceAttributes.put(name, value);
337      return this;
338    }
339
340    /**
341     * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
342     * When {@code false} (default), standard OTel name normalization is applied.
343     */
344    public Builder preserveNames(boolean preserveNames) {
345      this.preserveNames = preserveNames;
346      return this;
347    }
348
349    public ExporterOpenTelemetryProperties build() {
350      return new ExporterOpenTelemetryProperties(
351          protocol,
352          endpoint,
353          headers,
354          interval,
355          timeout,
356          serviceName,
357          serviceNamespace,
358          serviceInstanceId,
359          serviceVersion,
360          resourceAttributes,
361          preserveNames);
362    }
363  }
364}