Skip to content
2 changes: 1 addition & 1 deletion dependencyManagement/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ val jmhVersion = "1.37"
val mockitoVersion = "4.11.0"
val slf4jVersion = "2.0.17"
val opencensusVersion = "0.31.1"
val prometheusServerVersion = "1.5.1"
val prometheusServerVersion = "1.6.1"
val armeriaVersion = "1.38.0"
val junitVersion = "5.14.4"
val okhttpVersion = "5.3.2"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
import io.opentelemetry.sdk.metrics.export.MetricReader;
import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import java.io.IOException;
Expand Down Expand Up @@ -73,6 +74,7 @@ public static PrometheusHttpServerBuilder builder() {
@Nullable HttpHandler defaultHandler,
DefaultAggregationSelector defaultAggregationSelector,
@Nullable Authenticator authenticator,
TranslationStrategy translationStrategy,
PrometheusMetricReader prometheusMetricReader) {
this.host = host;
this.port = port;
Expand All @@ -95,9 +97,17 @@ public static PrometheusHttpServerBuilder builder() {
new LinkedBlockingQueue<>(),
new DaemonThreadFactory("prometheus-http-server"));
}
HTTPServer.Builder httpServerBuilder = HTTPServer.builder();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Based on the "Interaction with Translation Strategy" spec, I was expecting to see some content negotiation, basically reading accept headers somewhere.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's park this until open-telemetry/opentelemetry-specification#5062 is resolved.

if (translationStrategy != TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES) {
// Intentionally enable OM2 without content negotiation so OpenMetrics responses keep the
// legacy OM1 content type while using OM2 name-preservation semantics.
PrometheusProperties prometheusProperties =
PrometheusProperties.builder().enableOpenMetrics2(om2 -> {}).build();
httpServerBuilder = HTTPServer.builder(prometheusProperties);
}
try {
this.httpServer =
HTTPServer.builder()
httpServerBuilder
.hostname(host)
.port(port)
.executorService(executor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ public PrometheusHttpServerBuilder setTargetInfoMetricEnabled(boolean targetInfo
return this;
}

/**
* Sets the translation strategy for metric and label name conversion.
*
* @param translationStrategy the strategy to use
* @return this builder
* @see TranslationStrategy
*/
public PrometheusHttpServerBuilder setTranslationStrategy(
TranslationStrategy translationStrategy) {
requireNonNull(translationStrategy, "translationStrategy");
metricReaderBuilder.setTranslationStrategy(translationStrategy);
return this;
}

/**
* Set if the resource attributes should be added as labels on each exported metric.
*
Expand Down Expand Up @@ -201,6 +215,7 @@ public PrometheusHttpServer build() {
defaultHandler,
defaultAggregationSelector,
authenticator,
metricReaderBuilder.getTranslationStrategy(),
metricReaderBuilder.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public PrometheusMetricReader(
this(
allowedResourceAttributesFilter,
/* otelScopeLabelsEnabled= */ true,
/* targetInfoMetricEnabled= */ true);
/* targetInfoMetricEnabled= */ true,
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
}

/**
Expand All @@ -65,18 +66,23 @@ public PrometheusMetricReader(@Nullable Predicate<String> allowedResourceAttribu
this(
allowedResourceAttributesFilter,
/* otelScopeLabelsEnabled= */ true,
/* targetInfoMetricEnabled= */ true);
/* targetInfoMetricEnabled= */ true,
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
}

// Package-private constructor used by builder
@SuppressWarnings("InconsistentOverloads")
PrometheusMetricReader(
@Nullable Predicate<String> allowedResourceAttributesFilter,
boolean otelScopeLabelsEnabled,
boolean targetInfoMetricEnabled) {
boolean targetInfoMetricEnabled,
TranslationStrategy translationStrategy) {
this.converter =
new Otel2PrometheusConverter(
otelScopeLabelsEnabled, targetInfoMetricEnabled, allowedResourceAttributesFilter);
otelScopeLabelsEnabled,
targetInfoMetricEnabled,
translationStrategy,
allowedResourceAttributesFilter);
}

@Override
Expand Down Expand Up @@ -109,6 +115,7 @@ public String toString() {
StringJoiner joiner = new StringJoiner(",", "PrometheusMetricReader{", "}");
joiner.add("otelScopeLabelsEnabled=" + converter.isOtelScopeLabelsEnabled());
joiner.add("targetInfoMetricEnabled=" + converter.isTargetInfoMetricEnabled());
joiner.add("translationStrategy=" + converter.getTranslationStrategy());
joiner.add("allowedResourceAttributesFilter=" + converter.getAllowedResourceAttributesFilter());
return joiner.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package io.opentelemetry.exporter.prometheus;

import static java.util.Objects.requireNonNull;

import java.util.function.Predicate;
import javax.annotation.Nullable;

Expand All @@ -13,13 +15,16 @@ public final class PrometheusMetricReaderBuilder {

private boolean otelScopeLabelsEnabled = true;
private boolean targetInfoMetricEnabled = true;
private TranslationStrategy translationStrategy =
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES;
@Nullable private Predicate<String> allowedResourceAttributesFilter;

PrometheusMetricReaderBuilder() {}

PrometheusMetricReaderBuilder(PrometheusMetricReaderBuilder metricReaderBuilder) {
this.otelScopeLabelsEnabled = metricReaderBuilder.otelScopeLabelsEnabled;
this.targetInfoMetricEnabled = metricReaderBuilder.targetInfoMetricEnabled;
this.translationStrategy = metricReaderBuilder.translationStrategy;
this.allowedResourceAttributesFilter = metricReaderBuilder.allowedResourceAttributesFilter;
}

Expand Down Expand Up @@ -47,6 +52,20 @@ public PrometheusMetricReaderBuilder setTargetInfoMetricEnabled(boolean targetIn
return this;
}

/**
* Sets the translation strategy for metric and label name conversion.
*
* @param translationStrategy the strategy to use
* @return this builder
* @see TranslationStrategy
*/
public PrometheusMetricReaderBuilder setTranslationStrategy(
TranslationStrategy translationStrategy) {
requireNonNull(translationStrategy, "translationStrategy");
this.translationStrategy = translationStrategy;
return this;
}

/**
* Sets a filter to control which resource attributes are added as labels on each exported metric.
* If {@code null}, no resource attributes will be added as labels. Default is {@code null}.
Expand All @@ -60,9 +79,16 @@ public PrometheusMetricReaderBuilder setAllowedResourceAttributesFilter(
return this;
}

TranslationStrategy getTranslationStrategy() {
return translationStrategy;
}

/** Builds a new {@link PrometheusMetricReader}. */
public PrometheusMetricReader build() {
return new PrometheusMetricReader(
allowedResourceAttributesFilter, otelScopeLabelsEnabled, targetInfoMetricEnabled);
allowedResourceAttributesFilter,
otelScopeLabelsEnabled,
targetInfoMetricEnabled,
translationStrategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

package io.opentelemetry.exporter.prometheus;

import io.prometheus.metrics.model.snapshots.PrometheusNaming;
import io.prometheus.metrics.model.snapshots.Unit;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -14,6 +13,8 @@
/** Convert OpenTelemetry unit names to Prometheus units. */
class PrometheusUnitsHelper {

static final String[] RESERVED_SUFFIXES = {"_total", "_created", "_bucket", "_info"};

private static final Map<String, String> pluralNames = new ConcurrentHashMap<>();
private static final Map<String, String> singularNames = new ConcurrentHashMap<>();
private static final Map<String, Unit> predefinedUnits = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -97,11 +98,74 @@ static Unit convertUnit(String otelUnit) {
@Nullable
private static Unit unitOrNull(String name) {
try {
return new Unit(PrometheusNaming.sanitizeUnitName(name));
String sanitized = sanitizeUnitName(name);
sanitized = stripReservedUnitSuffixes(sanitized);
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
if (sanitized.isEmpty()) {
return null;
}
return new Unit(sanitized);
} catch (IllegalArgumentException e) {
// This happens if the name cannot be converted to a valid Prometheus unit name,
// for example if name is "total".
return null;
}
}

// These helpers are adapted from Prometheus naming sanitization. We keep a local copy because
// the exporter still needs unit normalization behavior that fits the Prometheus Java model API.
private static String sanitizeUnitName(String unitName) {
Comment thread
zeitlinger marked this conversation as resolved.
if (unitName.isEmpty()) {
throw new IllegalArgumentException("Cannot convert an empty string to a valid unit name.");
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
}
String sanitizedName = replaceIllegalCharsInUnitName(unitName);
while (sanitizedName.startsWith("_") || sanitizedName.startsWith(".")) {
sanitizedName = sanitizedName.substring(1);
}
while (sanitizedName.endsWith(".") || sanitizedName.endsWith("_")) {
sanitizedName = sanitizedName.substring(0, sanitizedName.length() - 1);
}
if (sanitizedName.isEmpty()) {
throw new IllegalArgumentException(
"Cannot convert '" + unitName + "' into a valid unit name.");
}
return sanitizedName;
}

private static String replaceIllegalCharsInUnitName(String name) {
int length = name.length();
char[] sanitized = new char[length];
for (int i = 0; i < length; i++) {
char ch = name.charAt(i);
if (ch == ':'
|| ch == '.'
|| (ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9')) {
sanitized[i] = ch;
} else {
sanitized[i] = '_';
}
}
return new String(sanitized);
}

private static String stripReservedUnitSuffixes(String name) {
boolean modified = true;
while (modified) {
modified = false;
for (String suffix : RESERVED_SUFFIXES) {
String suffixWithoutUnderscore = suffix.substring(1);
if (name.equals(suffixWithoutUnderscore)) {
return "";
Comment thread
zeitlinger marked this conversation as resolved.
}
if (name.endsWith(suffix)) {
name = name.substring(0, name.length() - suffix.length());
modified = true;
}
}
while (name.endsWith("_") || name.endsWith(".")) {
name = name.substring(0, name.length() - 1);
modified = true;
}
}
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.prometheus;

/**
* Controls how OpenTelemetry metric and label names are translated to Prometheus format.
*
* @see <a
* href="https://opentelemetry.io/docs/specs/otel/metrics/sdk_exporters/prometheus/">Prometheus
* exporter configuration</a>
*/
public enum TranslationStrategy {
/**
* Default. Non-standard characters are converted to underscores, and type / unit suffixes are
* attached.
*/
UNDERSCORE_ESCAPING_WITH_SUFFIXES,

/**
* Non-standard characters are converted to underscores, but type / unit suffixes are not attached
* by the exporter.
*/
UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES,

/** UTF-8 metric and label names are preserved, while type / unit suffixes are still attached. */
NO_UTF8_ESCAPING_WITH_SUFFIXES,

/** Metric and label names are passed through without translation. */
NO_TRANSLATION;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
import io.opentelemetry.exporter.prometheus.PrometheusHttpServerBuilder;
import io.opentelemetry.exporter.prometheus.TranslationStrategy;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
import io.opentelemetry.sdk.common.internal.IncludeExcludePredicate;
import io.opentelemetry.sdk.metrics.export.MetricReader;
Expand Down Expand Up @@ -54,6 +55,10 @@ public MetricReader create(DeclarativeConfigProperties config) {
if (withoutScopeInfo != null) {
prometheusBuilder.setOtelScopeLabelsEnabled(!withoutScopeInfo);
}
String translationStrategy = config.getString("translation_strategy");
if (translationStrategy != null) {
prometheusBuilder.setTranslationStrategy(parseTranslationStrategy(translationStrategy));
}

DeclarativeConfigProperties withResourceConstantLabels =
config.getStructured("with_resource_constant_labels");
Expand All @@ -72,4 +77,19 @@ public MetricReader create(DeclarativeConfigProperties config) {

return prometheusBuilder.build();
}

private static TranslationStrategy parseTranslationStrategy(String value) {
switch (value) {
case "underscore_escaping_with_suffixes":
return TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES;
case "underscore_escaping_without_suffixes/development":
return TranslationStrategy.UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES;
case "no_utf8_escaping_with_suffixes/development":
return TranslationStrategy.NO_UTF8_ESCAPING_WITH_SUFFIXES;
case "no_translation/development":
return TranslationStrategy.NO_TRANSLATION;
default:
throw new DeclarativeConfigException("Unsupported translation_strategy: " + value);
}
}
}
Loading
Loading