Skip to content

Commit 1ab1fc2

Browse files
committed
Add Prometheus translation strategy support
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent df8063b commit 1ab1fc2

13 files changed

Lines changed: 355 additions & 26 deletions

File tree

dependencyManagement/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ val jmhVersion = "1.37"
1515
val mockitoVersion = "4.11.0"
1616
val slf4jVersion = "2.0.17"
1717
val opencensusVersion = "0.31.1"
18-
val prometheusServerVersion = "1.5.1"
18+
val prometheusServerVersion = "1.6.1"
1919
val armeriaVersion = "1.38.0"
2020
val junitVersion = "5.14.4"
2121
val okhttpVersion = "5.3.2"

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
99
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
10-
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
1110
import static java.util.Objects.requireNonNull;
1211

1312
import io.opentelemetry.api.common.AttributeKey;
@@ -84,6 +83,7 @@ final class Otel2PrometheusConverter {
8483

8584
private final boolean otelScopeLabelsEnabled;
8685
private final boolean targetInfoMetricEnabled;
86+
private final TranslationStrategy translationStrategy;
8787
@Nullable private final Predicate<String> allowedResourceAttributesFilter;
8888

8989
/**
@@ -104,9 +104,11 @@ final class Otel2PrometheusConverter {
104104
Otel2PrometheusConverter(
105105
boolean otelScopeLabelsEnabled,
106106
boolean targetInfoMetricEnabled,
107+
TranslationStrategy translationStrategy,
107108
@Nullable Predicate<String> allowedResourceAttributesFilter) {
108109
this.otelScopeLabelsEnabled = otelScopeLabelsEnabled;
109110
this.targetInfoMetricEnabled = targetInfoMetricEnabled;
111+
this.translationStrategy = translationStrategy;
110112
this.allowedResourceAttributesFilter = allowedResourceAttributesFilter;
111113
this.resourceAttributesToAllowedKeysCache =
112114
allowedResourceAttributesFilter != null
@@ -122,6 +124,10 @@ boolean isTargetInfoMetricEnabled() {
122124
return targetInfoMetricEnabled;
123125
}
124126

127+
TranslationStrategy getTranslationStrategy() {
128+
return translationStrategy;
129+
}
130+
125131
@Nullable
126132
Predicate<String> getAllowedResourceAttributesFilter() {
127133
return allowedResourceAttributesFilter;
@@ -155,7 +161,8 @@ private MetricSnapshot convert(MetricData metricData) {
155161
// Note that AggregationTemporality.DELTA should never happen
156162
// because PrometheusMetricReader#getAggregationTemporality returns CUMULATIVE.
157163

158-
MetricMetadata metadata = convertMetadata(metricData);
164+
boolean isCounter = isMonotonicSum(metricData);
165+
MetricMetadata metadata = convertMetadata(metricData, isCounter);
159166
InstrumentationScopeInfo scope = metricData.getInstrumentationScopeInfo();
160167
switch (metricData.getType()) {
161168
case LONG_GAUGE:
@@ -210,6 +217,17 @@ private MetricSnapshot convert(MetricData metricData) {
210217
return null;
211218
}
212219

220+
private static boolean isMonotonicSum(MetricData metricData) {
221+
switch (metricData.getType()) {
222+
case LONG_SUM:
223+
return metricData.getLongSumData().isMonotonic();
224+
case DOUBLE_SUM:
225+
return metricData.getDoubleSumData().isMonotonic();
226+
default:
227+
return false;
228+
}
229+
}
230+
213231
private GaugeSnapshot convertLongGauge(
214232
MetricMetadata metadata,
215233
InstrumentationScopeInfo scope,
@@ -550,29 +568,91 @@ private List<AttributeKey<?>> filterAllowedResourceAttributeKeys(@Nullable Resou
550568
* non-standard characters (dots, dashes, etc.) to underscores, and {@code sanitizeLabelName}
551569
* strips invalid leading prefixes.
552570
*/
553-
private static String convertLabelName(String key) {
554-
return sanitizeLabelName(prometheusName(key));
571+
private String convertLabelName(String key) {
572+
if (translationStrategy.shouldEscape()) {
573+
return sanitizeLabelName(prometheusName(key));
574+
}
575+
return key;
576+
}
577+
578+
private MetricMetadata convertMetadata(MetricData metricData, boolean isCounter) {
579+
switch (translationStrategy) {
580+
case UNDERSCORE_ESCAPING_WITH_SUFFIXES:
581+
return convertMetadataEscapedWithSuffixes(metricData);
582+
case UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES:
583+
return convertMetadataEscapedWithoutSuffixes(metricData);
584+
case NO_UTF8_ESCAPING_WITH_SUFFIXES:
585+
return convertMetadataUtf8WithSuffixes(metricData, isCounter);
586+
case NO_TRANSLATION:
587+
return convertMetadataNoTranslation(metricData);
588+
}
589+
throw new IllegalStateException("Unknown strategy: " + translationStrategy);
590+
}
591+
592+
private static MetricMetadata convertMetadataEscapedWithSuffixes(MetricData metricData) {
593+
String name = prometheusName(metricData.getName());
594+
String help = metricData.getDescription();
595+
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());
596+
name = stripRepeatedUnderscores(stripReservedMetricSuffixes(name));
597+
if (unit != null && !name.endsWith(unit.toString())) {
598+
name = name + "_" + unit;
599+
}
600+
return new MetricMetadata(stripRepeatedUnderscores(name), help, unit);
555601
}
556602

557-
private static MetricMetadata convertMetadata(MetricData metricData) {
558-
String name = sanitizeMetricName(prometheusName(metricData.getName()));
603+
private static MetricMetadata convertMetadataEscapedWithoutSuffixes(MetricData metricData) {
604+
String rawName = stripRepeatedUnderscores(prometheusName(metricData.getName()));
605+
String name = stripReservedMetricSuffixes(rawName);
606+
return new MetricMetadata(name, rawName, rawName, metricData.getDescription(), null);
607+
}
608+
609+
private static MetricMetadata convertMetadataUtf8WithSuffixes(
610+
MetricData metricData, boolean isCounter) {
611+
String name = metricData.getName();
559612
String help = metricData.getDescription();
560613
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());
561614
if (unit != null && !name.endsWith(unit.toString())) {
562615
name = name + "_" + unit;
563616
}
564-
// Repeated __ are discouraged according to spec, although this is allowed in prometheus, see
565-
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
617+
String expositionBaseName = name;
618+
if (isCounter && !expositionBaseName.endsWith("_total")) {
619+
expositionBaseName = expositionBaseName + "_total";
620+
}
621+
return new MetricMetadata(stripReservedMetricSuffixes(name), expositionBaseName, help, unit);
622+
}
623+
624+
private static MetricMetadata convertMetadataNoTranslation(MetricData metricData) {
625+
String rawName = metricData.getName();
626+
String name = stripReservedMetricSuffixes(rawName);
627+
return new MetricMetadata(name, rawName, rawName, metricData.getDescription(), null);
628+
}
629+
630+
private static String stripReservedMetricSuffixes(String name) {
631+
boolean modified = true;
632+
while (modified) {
633+
modified = false;
634+
for (String suffix : PrometheusUnitsHelper.RESERVED_SUFFIXES) {
635+
if (name.equals(suffix)) {
636+
return name.substring(1);
637+
}
638+
if (name.endsWith(suffix)) {
639+
name = name.substring(0, name.length() - suffix.length());
640+
modified = true;
641+
}
642+
}
643+
}
644+
return name;
645+
}
646+
647+
private static String stripRepeatedUnderscores(String name) {
566648
while (name.contains("__")) {
567649
name = name.replace("__", "_");
568650
}
569-
570-
return new MetricMetadata(name, help, unit);
651+
return name;
571652
}
572653

573-
private static void putOrMerge(
574-
Map<String, MetricSnapshot> snapshotsByName, MetricSnapshot snapshot) {
575-
String name = snapshot.getMetadata().getPrometheusName();
654+
private void putOrMerge(Map<String, MetricSnapshot> snapshotsByName, MetricSnapshot snapshot) {
655+
String name = getMergeKey(snapshot.getMetadata());
576656
if (snapshotsByName.containsKey(name)) {
577657
MetricSnapshot merged = merge(snapshotsByName.get(name), snapshot);
578658
if (merged != null) {
@@ -583,6 +663,13 @@ private static void putOrMerge(
583663
}
584664
}
585665

666+
private String getMergeKey(MetricMetadata metadata) {
667+
if (translationStrategy.shouldEscape()) {
668+
return metadata.getPrometheusName();
669+
}
670+
return metadata.getName();
671+
}
672+
586673
/**
587674
* OpenTelemetry may use the same metric name multiple times but in different instrumentation
588675
* scopes. In that case, we try to merge the metrics. They will have different {@code

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
2222
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
2323
import io.opentelemetry.sdk.metrics.export.MetricReader;
24+
import io.prometheus.metrics.config.PrometheusProperties;
2425
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
2526
import io.prometheus.metrics.model.registry.PrometheusRegistry;
2627
import java.io.IOException;
@@ -73,6 +74,7 @@ public static PrometheusHttpServerBuilder builder() {
7374
@Nullable HttpHandler defaultHandler,
7475
DefaultAggregationSelector defaultAggregationSelector,
7576
@Nullable Authenticator authenticator,
77+
TranslationStrategy translationStrategy,
7678
PrometheusMetricReader prometheusMetricReader) {
7779
this.host = host;
7880
this.port = port;
@@ -95,9 +97,17 @@ public static PrometheusHttpServerBuilder builder() {
9597
new LinkedBlockingQueue<>(),
9698
new DaemonThreadFactory("prometheus-http-server"));
9799
}
100+
HTTPServer.Builder httpServerBuilder = HTTPServer.builder();
101+
if (translationStrategy != TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES) {
102+
// Intentionally enable OM2 without content negotiation so OpenMetrics responses keep the
103+
// legacy OM1 content type while using OM2 name-preservation semantics.
104+
PrometheusProperties prometheusProperties =
105+
PrometheusProperties.builder().enableOpenMetrics2(om2 -> {}).build();
106+
httpServerBuilder = HTTPServer.builder(prometheusProperties);
107+
}
98108
try {
99109
this.httpServer =
100-
HTTPServer.builder()
110+
httpServerBuilder
101111
.hostname(host)
102112
.port(port)
103113
.executorService(executor)

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ public PrometheusHttpServerBuilder setTargetInfoMetricEnabled(boolean targetInfo
102102
return this;
103103
}
104104

105+
/**
106+
* Sets the translation strategy for metric and label name conversion.
107+
*
108+
* @param translationStrategy the strategy to use
109+
* @return this builder
110+
* @see TranslationStrategy
111+
*/
112+
public PrometheusHttpServerBuilder setTranslationStrategy(
113+
TranslationStrategy translationStrategy) {
114+
requireNonNull(translationStrategy, "translationStrategy");
115+
metricReaderBuilder.setTranslationStrategy(translationStrategy);
116+
return this;
117+
}
118+
105119
/**
106120
* Set if the resource attributes should be added as labels on each exported metric.
107121
*
@@ -201,6 +215,7 @@ public PrometheusHttpServer build() {
201215
defaultHandler,
202216
defaultAggregationSelector,
203217
authenticator,
218+
metricReaderBuilder.getTranslationStrategy(),
204219
metricReaderBuilder.build());
205220
}
206221
}

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReader.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public PrometheusMetricReader(
5252
this(
5353
allowedResourceAttributesFilter,
5454
/* otelScopeLabelsEnabled= */ true,
55-
/* targetInfoMetricEnabled= */ true);
55+
/* targetInfoMetricEnabled= */ true,
56+
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
5657
}
5758

5859
/**
@@ -65,18 +66,23 @@ public PrometheusMetricReader(@Nullable Predicate<String> allowedResourceAttribu
6566
this(
6667
allowedResourceAttributesFilter,
6768
/* otelScopeLabelsEnabled= */ true,
68-
/* targetInfoMetricEnabled= */ true);
69+
/* targetInfoMetricEnabled= */ true,
70+
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
6971
}
7072

7173
// Package-private constructor used by builder
7274
@SuppressWarnings("InconsistentOverloads")
7375
PrometheusMetricReader(
7476
@Nullable Predicate<String> allowedResourceAttributesFilter,
7577
boolean otelScopeLabelsEnabled,
76-
boolean targetInfoMetricEnabled) {
78+
boolean targetInfoMetricEnabled,
79+
TranslationStrategy translationStrategy) {
7780
this.converter =
7881
new Otel2PrometheusConverter(
79-
otelScopeLabelsEnabled, targetInfoMetricEnabled, allowedResourceAttributesFilter);
82+
otelScopeLabelsEnabled,
83+
targetInfoMetricEnabled,
84+
translationStrategy,
85+
allowedResourceAttributesFilter);
8086
}
8187

8288
@Override
@@ -109,6 +115,7 @@ public String toString() {
109115
StringJoiner joiner = new StringJoiner(",", "PrometheusMetricReader{", "}");
110116
joiner.add("otelScopeLabelsEnabled=" + converter.isOtelScopeLabelsEnabled());
111117
joiner.add("targetInfoMetricEnabled=" + converter.isTargetInfoMetricEnabled());
118+
joiner.add("translationStrategy=" + converter.getTranslationStrategy());
112119
joiner.add("allowedResourceAttributesFilter=" + converter.getAllowedResourceAttributesFilter());
113120
return joiner.toString();
114121
}

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReaderBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package io.opentelemetry.exporter.prometheus;
77

8+
import static java.util.Objects.requireNonNull;
9+
810
import java.util.function.Predicate;
911
import javax.annotation.Nullable;
1012

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

1416
private boolean otelScopeLabelsEnabled = true;
1517
private boolean targetInfoMetricEnabled = true;
18+
private TranslationStrategy translationStrategy =
19+
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES;
1620
@Nullable private Predicate<String> allowedResourceAttributesFilter;
1721

1822
PrometheusMetricReaderBuilder() {}
1923

2024
PrometheusMetricReaderBuilder(PrometheusMetricReaderBuilder metricReaderBuilder) {
2125
this.otelScopeLabelsEnabled = metricReaderBuilder.otelScopeLabelsEnabled;
2226
this.targetInfoMetricEnabled = metricReaderBuilder.targetInfoMetricEnabled;
27+
this.translationStrategy = metricReaderBuilder.translationStrategy;
2328
this.allowedResourceAttributesFilter = metricReaderBuilder.allowedResourceAttributesFilter;
2429
}
2530

@@ -47,6 +52,20 @@ public PrometheusMetricReaderBuilder setTargetInfoMetricEnabled(boolean targetIn
4752
return this;
4853
}
4954

55+
/**
56+
* Sets the translation strategy for metric and label name conversion.
57+
*
58+
* @param translationStrategy the strategy to use
59+
* @return this builder
60+
* @see TranslationStrategy
61+
*/
62+
public PrometheusMetricReaderBuilder setTranslationStrategy(
63+
TranslationStrategy translationStrategy) {
64+
requireNonNull(translationStrategy, "translationStrategy");
65+
this.translationStrategy = translationStrategy;
66+
return this;
67+
}
68+
5069
/**
5170
* Sets a filter to control which resource attributes are added as labels on each exported metric.
5271
* If {@code null}, no resource attributes will be added as labels. Default is {@code null}.
@@ -60,9 +79,16 @@ public PrometheusMetricReaderBuilder setAllowedResourceAttributesFilter(
6079
return this;
6180
}
6281

82+
TranslationStrategy getTranslationStrategy() {
83+
return translationStrategy;
84+
}
85+
6386
/** Builds a new {@link PrometheusMetricReader}. */
6487
public PrometheusMetricReader build() {
6588
return new PrometheusMetricReader(
66-
allowedResourceAttributesFilter, otelScopeLabelsEnabled, targetInfoMetricEnabled);
89+
allowedResourceAttributesFilter,
90+
otelScopeLabelsEnabled,
91+
targetInfoMetricEnabled,
92+
translationStrategy);
6793
}
6894
}

0 commit comments

Comments
 (0)