Skip to content

Commit b2ae70f

Browse files
authored
fix: restore legacy suffix compatibility (#2100)
Fixes #2095 ## Summary Restores OM1/protobuf compatibility for dotted gauge names after `feat: move suffix handling to scrape time (#1955)`. The bug was that non-OpenMetrics exposition changed visible output for gauge names that merely ended in suffix-like dotted strings such as `.created` and `.total`. Examples: - `Gauge("test3.created")` regressed from `test3` to `test3_created` - `Gauge("test6.total")` regressed from `test6` to `test6_total` This PR restores the legacy OM1/protobuf behavior while keeping OpenMetrics on literal-name handling. This is the extracted prom-side fix from #2093. The Micrometer workflow and related downstream testing were split into a stacked follow-up PR so this can merge independently. ## What changed - Fix OM1 text exposition for dotted gauge names ending in `.created` and `.total` - Fix protobuf exposition for the same compatibility cases - Add regression tests that cover the restored OM1/protobuf behavior and the preserved OpenMetrics behavior - Clean up protobuf family-name resolution so legacy gauge handling lives in one path instead of pre-rewriting metadata objects ## Follow-up stacked PR - Micrometer workflow/task split: zeitlinger#1 ## Related - Replaces: #2093 - Issue: #2095 ## Testing - `mise run build` - `mise run lint` - `./mvnw test -pl prometheus-metrics-exposition-textformats,prometheus-metrics-exposition-formats -Dtest=ExpositionFormatsTest,ProtobufExpositionFormatsTest,DuplicateNamesProtobufTest -Dcoverage.skip=true -Dcheckstyle.skip=true`
1 parent 63f82ad commit b2ae70f

5 files changed

Lines changed: 211 additions & 19 deletions

File tree

prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ public String toDebugString(MetricSnapshots metricSnapshots, EscapingScheme esca
5050
for (MetricSnapshot s : merged) {
5151
MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, escapingScheme);
5252
if (!snapshot.getDataPoints().isEmpty()) {
53-
stringBuilder.append(TextFormat.printer().printToString(convert(snapshot, escapingScheme)));
53+
stringBuilder.append(
54+
TextFormat.printer()
55+
.printToString(
56+
convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme)));
5457
}
5558
}
5659
return stringBuilder.toString();
@@ -64,12 +67,17 @@ public void write(
6467
for (MetricSnapshot s : merged) {
6568
MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, escapingScheme);
6669
if (!snapshot.getDataPoints().isEmpty()) {
67-
convert(snapshot, escapingScheme).writeDelimitedTo(out);
70+
convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme).writeDelimitedTo(out);
6871
}
6972
}
7073
}
7174

7275
public Metrics.MetricFamily convert(MetricSnapshot snapshot, EscapingScheme scheme) {
76+
return convert(snapshot, snapshot.getMetadata().getOriginalName(), scheme);
77+
}
78+
79+
private Metrics.MetricFamily convert(
80+
MetricSnapshot snapshot, String rawOriginalName, EscapingScheme scheme) {
7381
Metrics.MetricFamily.Builder builder = Metrics.MetricFamily.newBuilder();
7482
if (snapshot instanceof CounterSnapshot) {
7583
for (CounterDataPointSnapshot data : ((CounterSnapshot) snapshot).getDataPoints()) {
@@ -82,7 +90,13 @@ public Metrics.MetricFamily convert(MetricSnapshot snapshot, EscapingScheme sche
8290
builder.addMetric(convert(data, scheme));
8391
}
8492
setMetadataUnlessEmpty(
85-
builder, snapshot.getMetadata(), null, Metrics.MetricType.GAUGE, scheme);
93+
builder,
94+
snapshot.getMetadata(),
95+
rawOriginalName,
96+
null,
97+
Metrics.MetricType.GAUGE,
98+
scheme,
99+
true);
86100
} else if (snapshot instanceof HistogramSnapshot) {
87101
HistogramSnapshot histogram = (HistogramSnapshot) snapshot;
88102
for (HistogramSnapshot.HistogramDataPointSnapshot data : histogram.getDataPoints()) {
@@ -290,25 +304,49 @@ private void setMetadataUnlessEmpty(
290304
@Nullable String nameSuffix,
291305
Metrics.MetricType type,
292306
EscapingScheme scheme) {
307+
setMetadataUnlessEmpty(
308+
builder, metadata, metadata.getOriginalName(), nameSuffix, type, scheme, false);
309+
}
310+
311+
private void setMetadataUnlessEmpty(
312+
Metrics.MetricFamily.Builder builder,
313+
MetricMetadata metadata,
314+
String rawOriginalName,
315+
@Nullable String nameSuffix,
316+
Metrics.MetricType type,
317+
EscapingScheme scheme,
318+
boolean normalizeLegacyGaugeName) {
293319
if (builder.getMetricCount() == 0) {
294320
return;
295321
}
296-
if (nameSuffix == null) {
297-
builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme));
298-
} else {
299-
String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme);
300-
if (expositionBaseName.endsWith(nameSuffix)) {
301-
builder.setName(expositionBaseName);
302-
} else {
303-
builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix);
304-
}
305-
}
322+
builder.setName(
323+
resolveMetricFamilyName(
324+
metadata, rawOriginalName, nameSuffix, scheme, normalizeLegacyGaugeName));
306325
if (metadata.getHelp() != null) {
307326
builder.setHelp(metadata.getHelp());
308327
}
309328
builder.setType(type);
310329
}
311330

331+
private String resolveMetricFamilyName(
332+
MetricMetadata metadata,
333+
String rawOriginalName,
334+
@Nullable String nameSuffix,
335+
EscapingScheme scheme,
336+
boolean normalizeLegacyGaugeName) {
337+
if (normalizeLegacyGaugeName) {
338+
return SnapshotEscaper.getLegacyGaugeName(metadata, rawOriginalName, scheme);
339+
}
340+
if (nameSuffix == null) {
341+
return SnapshotEscaper.getMetadataName(metadata, scheme);
342+
}
343+
String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme);
344+
if (expositionBaseName.endsWith(nameSuffix)) {
345+
return expositionBaseName;
346+
}
347+
return SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix;
348+
}
349+
312350
private long getNativeCount(HistogramSnapshot.HistogramDataPointSnapshot data) {
313351
if (data.hasCount()) {
314352
return data.getCount();

prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,53 @@ void testDifferentMetrics_producesSeparateMetricFamilies() throws IOException {
239239
assertThat(gaugeFamily.getMetric(0).getGauge().getValue()).isEqualTo(50.0);
240240
}
241241

242+
@Test
243+
void testLegacyGaugeNameWithAlternateEscapingSchemes_usesBaseName() throws IOException {
244+
MetricSnapshots snapshots =
245+
MetricSnapshots.of(
246+
GaugeSnapshot.builder()
247+
.name("legacy.name.total")
248+
.dataPoint(GaugeSnapshot.GaugeDataPointSnapshot.builder().value(7).build())
249+
.build());
250+
251+
assertThat(getSingleMetricFamilyName(snapshots, EscapingScheme.DOTS_ESCAPING))
252+
.isEqualTo("legacy_dot_name");
253+
assertThat(getSingleMetricFamilyName(snapshots, EscapingScheme.VALUE_ENCODING_ESCAPING))
254+
.isEqualTo("U__legacy_2e_name");
255+
}
256+
257+
@Test
258+
void testLegacyGaugeNameWithDotTotal_usesBaseName() throws IOException {
259+
MetricSnapshots snapshots =
260+
MetricSnapshots.of(
261+
GaugeSnapshot.builder()
262+
.name("legacy.total")
263+
.dataPoint(GaugeSnapshot.GaugeDataPointSnapshot.builder().value(7).build())
264+
.build());
265+
ByteArrayOutputStream out = new ByteArrayOutputStream();
266+
PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl();
267+
writer.write(out, snapshots, EscapingScheme.UNDERSCORE_ESCAPING);
268+
269+
List<Metrics.MetricFamily> metricFamilies = parseProtobufOutput(out);
270+
271+
assertThat(metricFamilies).hasSize(1);
272+
Metrics.MetricFamily family = metricFamilies.get(0);
273+
assertThat(family.getName()).isEqualTo("legacy");
274+
assertThat(family.getType()).isEqualTo(Metrics.MetricType.GAUGE);
275+
assertThat(family.getMetricCount()).isEqualTo(1);
276+
assertThat(family.getMetric(0).getGauge().getValue()).isEqualTo(7.0);
277+
}
278+
279+
private static String getSingleMetricFamilyName(
280+
MetricSnapshots snapshots, EscapingScheme escapingScheme) throws IOException {
281+
ByteArrayOutputStream out = new ByteArrayOutputStream();
282+
PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl();
283+
writer.write(out, snapshots, escapingScheme);
284+
List<Metrics.MetricFamily> metricFamilies = parseProtobufOutput(out);
285+
assertThat(metricFamilies).hasSize(1);
286+
return metricFamilies.get(0).getName();
287+
}
288+
242289
private static MetricSnapshots getMetricSnapshots() {
243290
PrometheusRegistry registry = new PrometheusRegistry();
244291

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp;
99
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot;
1010
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
11+
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getLegacyGaugeName;
1112
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName;
1213
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
1314

@@ -123,7 +124,7 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch
123124
if (snapshot instanceof CounterSnapshot) {
124125
writeCounter(writer, (CounterSnapshot) snapshot, scheme);
125126
} else if (snapshot instanceof GaugeSnapshot) {
126-
writeGauge(writer, (GaugeSnapshot) snapshot, scheme);
127+
writeGauge(writer, (GaugeSnapshot) snapshot, s.getMetadata().getOriginalName(), scheme);
127128
} else if (snapshot instanceof HistogramSnapshot) {
128129
writeHistogram(writer, (HistogramSnapshot) snapshot, scheme);
129130
} else if (snapshot instanceof SummarySnapshot) {
@@ -189,13 +190,14 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem
189190
}
190191
}
191192

192-
private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
193+
private void writeGauge(
194+
Writer writer, GaugeSnapshot snapshot, String rawOriginalName, EscapingScheme scheme)
193195
throws IOException {
194196
MetricMetadata metadata = snapshot.getMetadata();
195-
writeMetadata(writer, null, "gauge", metadata, scheme);
196-
String name = getMetadataName(metadata, scheme);
197+
String gaugeName = getLegacyGaugeName(metadata, rawOriginalName, scheme);
198+
writeMetadataWithFullName(writer, gaugeName, "gauge", metadata);
197199
for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
198-
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
200+
writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme);
199201
writeDouble(writer, data.getValue());
200202
writeScrapeTimestampAndNewline(writer, data);
201203
}

prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,86 @@ void testGaugeWithDots() throws IOException {
666666
assertPrometheusProtobuf(prometheusProtobuf, gauge);
667667
}
668668

669+
@Test
670+
void testGaugeReservedSuffixCompatibilityWithAlternateEscapingSchemes() throws IOException {
671+
GaugeSnapshot totalGauge =
672+
GaugeSnapshot.builder()
673+
.name("legacy.name.total")
674+
.dataPoint(GaugeDataPointSnapshot.builder().value(6).build())
675+
.build();
676+
assertPrometheusText(
677+
"""
678+
# TYPE legacy_dot_name gauge
679+
legacy_dot_name 6.0
680+
""",
681+
totalGauge,
682+
EscapingScheme.DOTS_ESCAPING);
683+
assertPrometheusText(
684+
"""
685+
# TYPE U__legacy_2e_name gauge
686+
U__legacy_2e_name 6.0
687+
""",
688+
totalGauge,
689+
EscapingScheme.VALUE_ENCODING_ESCAPING);
690+
}
691+
692+
@Test
693+
void testGaugeReservedSuffixCompatibilityOutsideOpenMetrics() throws IOException {
694+
GaugeSnapshot createdGauge =
695+
GaugeSnapshot.builder()
696+
.name("test3.created")
697+
.dataPoint(GaugeDataPointSnapshot.builder().value(3).build())
698+
.build();
699+
assertOpenMetricsText(
700+
"""
701+
# TYPE U__test3_2e_created gauge
702+
U__test3_2e_created 3.0
703+
# EOF
704+
""",
705+
createdGauge);
706+
assertPrometheusText(
707+
"""
708+
# TYPE test3 gauge
709+
test3 3.0
710+
""",
711+
createdGauge);
712+
assertPrometheusTextWithoutCreated(
713+
"""
714+
# TYPE test3 gauge
715+
test3 3.0
716+
""",
717+
createdGauge);
718+
assertPrometheusProtobuf(
719+
"name: \"test3\" type: GAUGE metric { gauge { value: 3.0 } }", createdGauge);
720+
721+
GaugeSnapshot totalGauge =
722+
GaugeSnapshot.builder()
723+
.name("test6.total")
724+
.dataPoint(GaugeDataPointSnapshot.builder().value(6).build())
725+
.build();
726+
assertOpenMetricsText(
727+
"""
728+
# TYPE U__test6_2e_total gauge
729+
U__test6_2e_total 6.0
730+
# EOF
731+
""",
732+
totalGauge);
733+
assertPrometheusText(
734+
"""
735+
# TYPE test6 gauge
736+
test6 6.0
737+
""",
738+
totalGauge);
739+
assertPrometheusTextWithoutCreated(
740+
"""
741+
# TYPE test6 gauge
742+
test6 6.0
743+
""",
744+
totalGauge);
745+
assertPrometheusProtobuf(
746+
"name: \"test6\" type: GAUGE metric { gauge { value: 6.0 } }", totalGauge);
747+
}
748+
669749
@Test
670750
void testGaugeUTF8() throws IOException {
671751
String prometheusText =
@@ -3088,9 +3168,14 @@ private void assertOpenMetricsTextWithoutCreated(String expected, MetricSnapshot
30883168
}
30893169

30903170
private void assertPrometheusText(String expected, MetricSnapshot snapshot) throws IOException {
3171+
assertPrometheusText(expected, snapshot, EscapingScheme.UNDERSCORE_ESCAPING);
3172+
}
3173+
3174+
private void assertPrometheusText(
3175+
String expected, MetricSnapshot snapshot, EscapingScheme escapingScheme) throws IOException {
30913176
ByteArrayOutputStream out = new ByteArrayOutputStream();
30923177
getPrometheusWriter(PrometheusTextFormatWriter.builder().setIncludeCreatedTimestamps(true))
3093-
.write(out, MetricSnapshots.of(snapshot), EscapingScheme.UNDERSCORE_ESCAPING);
3178+
.write(out, MetricSnapshots.of(snapshot), escapingScheme);
30943179
assertThat(out).hasToString(expected);
30953180
}
30963181

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ public static String getOriginalMetadataName(MetricMetadata metadata, EscapingSc
113113
}
114114
}
115115

116+
public static String getLegacyGaugeName(
117+
MetricMetadata metadata, String rawOriginalName, EscapingScheme scheme) {
118+
String legacyGaugeBaseName = stripLegacyGaugeSuffix(rawOriginalName);
119+
if (legacyGaugeBaseName != null) {
120+
return PrometheusNaming.escapeName(legacyGaugeBaseName, scheme);
121+
}
122+
return getMetadataName(metadata, scheme);
123+
}
124+
125+
@Nullable
126+
private static String stripLegacyGaugeSuffix(String rawOriginalName) {
127+
if (rawOriginalName.endsWith(".created")) {
128+
return rawOriginalName.substring(0, rawOriginalName.length() - ".created".length());
129+
}
130+
if (rawOriginalName.endsWith(".total")) {
131+
return rawOriginalName.substring(0, rawOriginalName.length() - ".total".length());
132+
}
133+
return null;
134+
}
135+
116136
public static Labels escapeLabels(Labels labels, EscapingScheme scheme) {
117137
Labels.Builder outLabelsBuilder = Labels.builder();
118138

0 commit comments

Comments
 (0)