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