Skip to content

Commit 145e722

Browse files
committed
add a flag to avoid a breaking change
1 parent dfd1b2b commit 145e722

File tree

8 files changed

+140
-24
lines changed

8 files changed

+140
-24
lines changed

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.opentelemetry.exporter.prometheus;
77

8+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
89
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
910
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
1011
import static java.util.Objects.requireNonNull;
@@ -91,6 +92,8 @@ final class Otel2PrometheusConverter {
9192
*/
9293
private final Map<Attributes, List<AttributeKey<?>>> resourceAttributesToAllowedKeysCache;
9394

95+
private final boolean utf8SupportEnabled;
96+
9497
/**
9598
* Constructor with feature flag parameter.
9699
*
@@ -100,13 +103,16 @@ final class Otel2PrometheusConverter {
100103
* matching this predicate will be added as labels on each exported metric
101104
*/
102105
Otel2PrometheusConverter(
103-
boolean otelScopeEnabled, @Nullable Predicate<String> allowedResourceAttributesFilter) {
106+
boolean otelScopeEnabled,
107+
@Nullable Predicate<String> allowedResourceAttributesFilter,
108+
boolean utf8SupportEnabled) {
104109
this.otelScopeEnabled = otelScopeEnabled;
105110
this.allowedResourceAttributesFilter = allowedResourceAttributesFilter;
106111
this.resourceAttributesToAllowedKeysCache =
107112
allowedResourceAttributesFilter != null
108113
? new ConcurrentHashMap<>()
109114
: Collections.emptyMap();
115+
this.utf8SupportEnabled = utf8SupportEnabled;
110116
}
111117

112118
MetricSnapshots convert(@Nullable Collection<MetricData> metricDataCollection) {
@@ -457,8 +463,8 @@ private InfoSnapshot makeScopeInfo(Set<InstrumentationScopeInfo> scopes) {
457463
* Convert OpenTelemetry attributes to Prometheus labels.
458464
*
459465
* @param resource optional resource (attributes) to be converted.
460-
* @param scope will be converted to {@code otel_scope_*} labels if {@code otelScopeEnabled} is
461-
* {@code true}.
466+
* @param scope that will be converted to {@code otel_scope_*} labels if {@code otelScopeEnabled}
467+
* is {@code true}.
462468
* @param attributes the attributes to be converted.
463469
* @param additionalAttributes optional list of key/value pairs, may be empty.
464470
*/
@@ -548,8 +554,13 @@ private List<AttributeKey<?>> filterAllowedResourceAttributeKeys(@Nullable Resou
548554
return allowedAttributeKeys;
549555
}
550556

551-
private static MetricMetadata convertMetadata(MetricData metricData) {
552-
String name = sanitizeMetricName(metricData.getName());
557+
private MetricMetadata convertMetadata(MetricData metricData) {
558+
String name = metricData.getName();
559+
if (!utf8SupportEnabled) {
560+
name = prometheusName(name);
561+
}
562+
name = sanitizeMetricName(name);
563+
553564
String help = metricData.getDescription();
554565
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());
555566
if (unit != null && !name.endsWith(unit.toString())) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public final class PrometheusHttpServer implements MetricReader {
4343
private final String host;
4444
private final int port;
4545
private final boolean otelScopeEnabled;
46+
private final boolean utf8SupportEnabled;
4647
@Nullable private final Predicate<String> allowedResourceAttributesFilter;
4748
private final MemoryMode memoryMode;
4849
private final DefaultAggregationSelector defaultAggregationSelector;
@@ -73,6 +74,7 @@ public static PrometheusHttpServerBuilder builder() {
7374
@Nullable ExecutorService executor,
7475
PrometheusRegistry prometheusRegistry,
7576
boolean otelScopeEnabled,
77+
boolean utf8SupportEnabled,
7678
@Nullable Predicate<String> allowedResourceAttributesFilter,
7779
MemoryMode memoryMode,
7880
@Nullable HttpHandler defaultHandler,
@@ -81,12 +83,14 @@ public static PrometheusHttpServerBuilder builder() {
8183
this.host = host;
8284
this.port = port;
8385
this.otelScopeEnabled = otelScopeEnabled;
86+
this.utf8SupportEnabled = utf8SupportEnabled;
8487
this.allowedResourceAttributesFilter = allowedResourceAttributesFilter;
8588
this.memoryMode = memoryMode;
8689
this.defaultAggregationSelector = defaultAggregationSelector;
8790
this.builder = builder;
8891
this.prometheusMetricReader =
89-
new PrometheusMetricReader(otelScopeEnabled, allowedResourceAttributesFilter);
92+
new PrometheusMetricReader(
93+
otelScopeEnabled, allowedResourceAttributesFilter, utf8SupportEnabled);
9094
this.prometheusRegistry = prometheusRegistry;
9195
prometheusRegistry.register(prometheusMetricReader);
9296
// When memory mode is REUSABLE_DATA, concurrent reads lead to data corruption. To prevent this,
@@ -172,6 +176,7 @@ public String toString() {
172176
joiner.add("host=" + host);
173177
joiner.add("port=" + port);
174178
joiner.add("otelScopeEnabled=" + otelScopeEnabled);
179+
joiner.add("utf8SupportEnabled=" + utf8SupportEnabled);
175180
joiner.add("allowedResourceAttributesFilter=" + allowedResourceAttributesFilter);
176181
joiner.add("memoryMode=" + memoryMode);
177182
joiner.add(

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class PrometheusHttpServerBuilder {
3131
private int port = DEFAULT_PORT;
3232
private PrometheusRegistry prometheusRegistry = new PrometheusRegistry();
3333
private boolean otelScopeEnabled = true;
34+
private boolean utf8SupportEnabled = false;
3435
@Nullable private Predicate<String> allowedResourceAttributesFilter;
3536
@Nullable private ExecutorService executor;
3637
private MemoryMode memoryMode = DEFAULT_MEMORY_MODE;
@@ -46,6 +47,7 @@ public final class PrometheusHttpServerBuilder {
4647
this.port = builder.port;
4748
this.prometheusRegistry = builder.prometheusRegistry;
4849
this.otelScopeEnabled = builder.otelScopeEnabled;
50+
this.utf8SupportEnabled = builder.utf8SupportEnabled;
4951
this.allowedResourceAttributesFilter = builder.allowedResourceAttributesFilter;
5052
this.executor = builder.executor;
5153
this.memoryMode = builder.memoryMode;
@@ -90,6 +92,30 @@ public PrometheusHttpServerBuilder setOtelScopeEnabled(boolean otelScopeEnabled)
9092
return this;
9193
}
9294

95+
/**
96+
* Set if UTF-8 support is enabled.
97+
*
98+
* <p>If set to {@code true}, the exporter will pass metric names and labels unchanged to the
99+
* prometheus client library, which supports UTF-8.
100+
*
101+
* <p>UTF-8 will only be seen in the exported metrics if the prometheus server <a
102+
* href="https://prometheus.github.io/client_java/exporters/unicode/">signals support for
103+
* UTF-8</a>
104+
*
105+
* <p>Therefore, it's safe to always set this setting to {@code true} if you're not affected by
106+
* following change in behavior:
107+
*
108+
* <p>If set to {@code true}, multiple non-legacy characters (e.g. <code>%%</code>) in a row will
109+
* not be replaced with a single underscore as recommended in the <a href=
110+
* "https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1">Prometheus
111+
* conversion specification</a>.
112+
*/
113+
@SuppressWarnings("UnusedReturnValue")
114+
public PrometheusHttpServerBuilder setUtf8SupportEnabled(boolean utf8SupportEnabled) {
115+
this.utf8SupportEnabled = utf8SupportEnabled;
116+
return this;
117+
}
118+
93119
/**
94120
* Set if the resource attributes should be added as labels on each exported metric.
95121
*
@@ -177,6 +203,7 @@ public PrometheusHttpServer build() {
177203
executor,
178204
prometheusRegistry,
179205
otelScopeEnabled,
206+
utf8SupportEnabled,
180207
allowedResourceAttributesFilter,
181208
memoryMode,
182209
defaultHandler,

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ public class PrometheusMetricReader implements MetricReader, MultiCollector {
2929
private final Otel2PrometheusConverter converter;
3030

3131
// TODO: refactor to public static create or builder pattern to align with project style
32-
/** See {@link Otel2PrometheusConverter#Otel2PrometheusConverter(boolean, Predicate)}. */
32+
/** See {@link Otel2PrometheusConverter#Otel2PrometheusConverter(boolean, Predicate, boolean)}. */
3333
public PrometheusMetricReader(
34-
boolean otelScopeEnabled, @Nullable Predicate<String> allowedResourceAttributesFilter) {
34+
boolean otelScopeEnabled,
35+
@Nullable Predicate<String> allowedResourceAttributesFilter,
36+
boolean utf8SupportEnabled) {
3537
this.converter =
36-
new Otel2PrometheusConverter(otelScopeEnabled, allowedResourceAttributesFilter);
38+
new Otel2PrometheusConverter(
39+
otelScopeEnabled, allowedResourceAttributesFilter, utf8SupportEnabled);
3740
}
3841

3942
@Override

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusComponentProvider.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ public String getName() {
3333

3434
@Override
3535
public MetricReader create(DeclarativeConfigProperties config) {
36-
PrometheusHttpServerBuilder prometheusBuilder = PrometheusHttpServer.builder();
36+
PrometheusHttpServerBuilder prometheusBuilder =
37+
PrometheusHttpServer.builder()
38+
.setUtf8SupportEnabled(true); // we can accept a breaking change in declarative config
3739

3840
Integer port = config.getInt("port");
3941
if (port != null) {

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProvider.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public MetricReader createMetricReader(ConfigProperties config) {
3333
prometheusBuilder.setHost(host);
3434
}
3535

36+
prometheusBuilder.setUtf8SupportEnabled(
37+
config.getBoolean("otel.exporter.prometheus.utf8", false));
38+
3639
ExporterBuilderUtil.configureExporterMemoryMode(config, prometheusBuilder::setMemoryMode);
3740

3841
String defaultHistogramAggregation =

exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.opentelemetry.sdk.metrics.internal.data.ImmutableSummaryData;
3838
import io.opentelemetry.sdk.metrics.internal.data.ImmutableSummaryPointData;
3939
import io.opentelemetry.sdk.resources.Resource;
40+
import io.prometheus.metrics.config.EscapingScheme;
4041
import io.prometheus.metrics.expositionformats.ExpositionFormats;
4142
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
4243
import io.prometheus.metrics.model.snapshots.Labels;
@@ -68,24 +69,77 @@ class Otel2PrometheusConverterTest {
6869
"(.|\\n)*# HELP (?<help>.*)\n# TYPE (?<type>.*)\n(?<metricName>.*)\\{"
6970
+ "otel_scope_foo=\"bar\",otel_scope_name=\"scope\","
7071
+ "otel_scope_schema_url=\"schemaUrl\",otel_scope_version=\"version\"}(.|\\n)*");
72+
private static final Pattern ESCAPE_PATTERN =
73+
Pattern.compile(
74+
"(.|\\n)*# HELP (?<help>.*)\n# TYPE (?<type>.*)\n\\{\"(?<metricName>.*)\","
75+
+ "otel_scope_foo=\"bar\",otel_scope_name=\"scope\","
76+
+ "otel_scope_schema_url=\"schemaUrl\",otel_scope_version=\"version\"}(.|\\n)*");
7177
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
7278

7379
private final Otel2PrometheusConverter converter =
74-
new Otel2PrometheusConverter(true, /* allowedResourceAttributesFilter= */ null);
80+
new Otel2PrometheusConverter(
81+
true, /* allowedResourceAttributesFilter= */ null, /* utf8SupportEnabled */ true);
7582

7683
@ParameterizedTest
7784
@MethodSource("metricMetadataArgs")
7885
void metricMetadata(
7986
MetricData metricData, String expectedType, String expectedHelp, String expectedMetricName)
8087
throws IOException {
88+
assertMetricData(
89+
metricData,
90+
expectedType,
91+
expectedHelp,
92+
expectedMetricName,
93+
new Otel2PrometheusConverter(
94+
true, /* allowedResourceAttributesFilter= */ null, /* utf8SupportEnabled */ false),
95+
EscapingScheme.UNDERSCORE_ESCAPING,
96+
PATTERN);
97+
}
98+
99+
@Test
100+
void metricMetadataUtf8() throws IOException {
101+
// all UTF-8 chars are accepted as is
102+
// repeated "_" are collapsed, but 2 λ are not collapsed to a single "_"
103+
// In a real application, the escaping scheme is passed using the "escaping" header when
104+
// scraping the metrics
105+
106+
MetricData metricData = createSampleMetricData("λλbe__happy", "1", MetricDataType.LONG_GAUGE);
107+
assertMetricData(
108+
metricData,
109+
"__be_happy_ratio gauge",
110+
"__be_happy_ratio description",
111+
"__be_happy_ratio",
112+
converter,
113+
EscapingScheme.UNDERSCORE_ESCAPING,
114+
PATTERN);
115+
116+
assertMetricData(
117+
metricData,
118+
"\"λλbe_happy_ratio\" gauge",
119+
"\"λλbe_happy_ratio\" description",
120+
"λλbe_happy_ratio",
121+
converter,
122+
EscapingScheme.ALLOW_UTF8,
123+
ESCAPE_PATTERN);
124+
}
125+
126+
private static void assertMetricData(
127+
MetricData metricData,
128+
String expectedType,
129+
String expectedHelp,
130+
String expectedMetricName,
131+
Otel2PrometheusConverter converter,
132+
EscapingScheme escapingScheme,
133+
Pattern pattern)
134+
throws IOException {
81135
ByteArrayOutputStream out = new ByteArrayOutputStream();
82136
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
83-
ExpositionFormats.init().getPrometheusTextFormatWriter().write(out, snapshots);
137+
ExpositionFormats.init().getPrometheusTextFormatWriter().write(out, snapshots, escapingScheme);
84138
String expositionFormat = new String(out.toByteArray(), StandardCharsets.UTF_8);
85139

86140
assertThat(expositionFormat)
87141
.matchesSatisfying(
88-
PATTERN,
142+
pattern,
89143
matcher -> {
90144
assertThat(matcher.group("help")).isEqualTo(expectedHelp);
91145
assertThat(matcher.group("type")).isEqualTo(expectedType);
@@ -138,12 +192,13 @@ private static Stream<Arguments> metricMetadataArgs() {
138192
"metric_name_2 summary",
139193
"metric_name_2 description",
140194
"metric_name_2_count"),
141-
// unsupported characters are translated to "_", repeated "_" are dropped
195+
// unsupported characters are translated to "_", repeated "_" are collapsed if
196+
// the original name had consecutive "_"
142197
Arguments.of(
143-
createSampleMetricData("s%%ple", "%/min", MetricDataType.SUMMARY),
144-
"s_ple_percent_per_minute summary",
145-
"s_ple_percent_per_minute description",
146-
"s_ple_percent_per_minute_count"),
198+
createSampleMetricData("s%%p__le", "%/min", MetricDataType.SUMMARY),
199+
"s_p_le_percent_per_minute summary",
200+
"s_p_le_percent_per_minute description",
201+
"s_p_le_percent_per_minute_count"),
147202
// metric unit is not appended if the name already contains the unit
148203
Arguments.of(
149204
createSampleMetricData("metric_name_total", "total", MetricDataType.LONG_SUM),
@@ -201,7 +256,8 @@ void resourceAttributesAddition(
201256
throws IOException {
202257

203258
Otel2PrometheusConverter converter =
204-
new Otel2PrometheusConverter(true, allowedResourceAttributesFilter);
259+
new Otel2PrometheusConverter(
260+
true, allowedResourceAttributesFilter, /* utf8SupportEnabled */ true);
205261

206262
ByteArrayOutputStream out = new ByteArrayOutputStream();
207263
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
@@ -501,7 +557,10 @@ void validateCacheIsBounded() {
501557
};
502558

503559
Otel2PrometheusConverter otel2PrometheusConverter =
504-
new Otel2PrometheusConverter(true, /* allowedResourceAttributesFilter= */ countPredicate);
560+
new Otel2PrometheusConverter(
561+
true,
562+
/* allowedResourceAttributesFilter= */ countPredicate,
563+
/* utf8SupportEnabled */ true);
505564

506565
// Create 20 different metric data objects with 2 different resource attributes;
507566
Resource resource1 = Resource.builder().put("cluster", "cluster1").build();

exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReaderTest.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ class PrometheusMetricReaderTest {
6161
void setUp() {
6262
this.testClock.setTime(Instant.ofEpochMilli((System.currentTimeMillis() / 100) * 100));
6363
this.createdTimestamp = convertTimestamp(testClock.now());
64-
this.reader = new PrometheusMetricReader(true, /* allowedResourceAttributesFilter= */ null);
64+
this.reader =
65+
new PrometheusMetricReader(
66+
true, /* allowedResourceAttributesFilter= */ null, /* utf8SupportEnabled */ true);
6567
this.meter =
6668
SdkMeterProvider.builder()
6769
.setClock(testClock)
@@ -776,7 +778,8 @@ void exponentialHistogramBucketConversion() {
776778
int otelScale = random.nextInt(24) - 4;
777779
int prometheusScale = Math.min(otelScale, 8);
778780
PrometheusMetricReader reader =
779-
new PrometheusMetricReader(true, /* allowedResourceAttributesFilter= */ null);
781+
new PrometheusMetricReader(
782+
true, /* allowedResourceAttributesFilter= */ null, /* utf8SupportEnabled */ true);
780783
Meter meter =
781784
SdkMeterProvider.builder()
782785
.registerMetricReader(reader)
@@ -1029,7 +1032,8 @@ void otelScopeComplete() throws IOException {
10291032
@Test
10301033
void otelScopeDisabled() throws IOException {
10311034
PrometheusMetricReader reader =
1032-
new PrometheusMetricReader(false, /* allowedResourceAttributesFilter= */ null);
1035+
new PrometheusMetricReader(
1036+
false, /* allowedResourceAttributesFilter= */ null, /* utf8SupportEnabled */ true);
10331037
Meter meter =
10341038
SdkMeterProvider.builder()
10351039
.setClock(testClock)
@@ -1060,7 +1064,9 @@ void otelScopeDisabled() throws IOException {
10601064
void addResourceAttributesWorks() throws IOException {
10611065
PrometheusMetricReader reader =
10621066
new PrometheusMetricReader(
1063-
true, /* allowedResourceAttributesFilter= */ Predicates.is("cluster"));
1067+
true,
1068+
/* allowedResourceAttributesFilter= */ Predicates.is("cluster"),
1069+
/* utf8SupportEnabled */ true);
10641070
Meter meter =
10651071
SdkMeterProvider.builder()
10661072
.setClock(testClock)

0 commit comments

Comments
 (0)