diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java index 5e58275ca..9c3f74943 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java @@ -50,7 +50,10 @@ public String toDebugString(MetricSnapshots metricSnapshots, EscapingScheme esca for (MetricSnapshot s : merged) { MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, escapingScheme); if (!snapshot.getDataPoints().isEmpty()) { - stringBuilder.append(TextFormat.printer().printToString(convert(snapshot, escapingScheme))); + stringBuilder.append( + TextFormat.printer() + .printToString( + convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme))); } } return stringBuilder.toString(); @@ -64,12 +67,17 @@ public void write( for (MetricSnapshot s : merged) { MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, escapingScheme); if (!snapshot.getDataPoints().isEmpty()) { - convert(snapshot, escapingScheme).writeDelimitedTo(out); + convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme).writeDelimitedTo(out); } } } public Metrics.MetricFamily convert(MetricSnapshot snapshot, EscapingScheme scheme) { + return convert(snapshot, snapshot.getMetadata().getOriginalName(), scheme); + } + + private Metrics.MetricFamily convert( + MetricSnapshot snapshot, String rawOriginalName, EscapingScheme scheme) { Metrics.MetricFamily.Builder builder = Metrics.MetricFamily.newBuilder(); if (snapshot instanceof CounterSnapshot) { for (CounterDataPointSnapshot data : ((CounterSnapshot) snapshot).getDataPoints()) { @@ -82,7 +90,13 @@ public Metrics.MetricFamily convert(MetricSnapshot snapshot, EscapingScheme sche builder.addMetric(convert(data, scheme)); } setMetadataUnlessEmpty( - builder, snapshot.getMetadata(), null, Metrics.MetricType.GAUGE, scheme); + builder, + snapshot.getMetadata(), + rawOriginalName, + null, + Metrics.MetricType.GAUGE, + scheme, + true); } else if (snapshot instanceof HistogramSnapshot) { HistogramSnapshot histogram = (HistogramSnapshot) snapshot; for (HistogramSnapshot.HistogramDataPointSnapshot data : histogram.getDataPoints()) { @@ -290,25 +304,49 @@ private void setMetadataUnlessEmpty( @Nullable String nameSuffix, Metrics.MetricType type, EscapingScheme scheme) { + setMetadataUnlessEmpty( + builder, metadata, metadata.getOriginalName(), nameSuffix, type, scheme, false); + } + + private void setMetadataUnlessEmpty( + Metrics.MetricFamily.Builder builder, + MetricMetadata metadata, + String rawOriginalName, + @Nullable String nameSuffix, + Metrics.MetricType type, + EscapingScheme scheme, + boolean normalizeLegacyGaugeName) { if (builder.getMetricCount() == 0) { return; } - if (nameSuffix == null) { - builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme)); - } else { - String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme); - if (expositionBaseName.endsWith(nameSuffix)) { - builder.setName(expositionBaseName); - } else { - builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix); - } - } + builder.setName( + resolveMetricFamilyName( + metadata, rawOriginalName, nameSuffix, scheme, normalizeLegacyGaugeName)); if (metadata.getHelp() != null) { builder.setHelp(metadata.getHelp()); } builder.setType(type); } + private String resolveMetricFamilyName( + MetricMetadata metadata, + String rawOriginalName, + @Nullable String nameSuffix, + EscapingScheme scheme, + boolean normalizeLegacyGaugeName) { + if (normalizeLegacyGaugeName) { + return SnapshotEscaper.getLegacyGaugeName(metadata, rawOriginalName, scheme); + } + if (nameSuffix == null) { + return SnapshotEscaper.getMetadataName(metadata, scheme); + } + String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme); + if (expositionBaseName.endsWith(nameSuffix)) { + return expositionBaseName; + } + return SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix; + } + private long getNativeCount(HistogramSnapshot.HistogramDataPointSnapshot data) { if (data.hasCount()) { return data.getCount(); diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java index c5fc7bf34..553d84af5 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesProtobufTest.java @@ -239,6 +239,53 @@ void testDifferentMetrics_producesSeparateMetricFamilies() throws IOException { assertThat(gaugeFamily.getMetric(0).getGauge().getValue()).isEqualTo(50.0); } + @Test + void testLegacyGaugeNameWithAlternateEscapingSchemes_usesBaseName() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + GaugeSnapshot.builder() + .name("legacy.name.total") + .dataPoint(GaugeSnapshot.GaugeDataPointSnapshot.builder().value(7).build()) + .build()); + + assertThat(getSingleMetricFamilyName(snapshots, EscapingScheme.DOTS_ESCAPING)) + .isEqualTo("legacy_dot_name"); + assertThat(getSingleMetricFamilyName(snapshots, EscapingScheme.VALUE_ENCODING_ESCAPING)) + .isEqualTo("U__legacy_2e_name"); + } + + @Test + void testLegacyGaugeNameWithDotTotal_usesBaseName() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + GaugeSnapshot.builder() + .name("legacy.total") + .dataPoint(GaugeSnapshot.GaugeDataPointSnapshot.builder().value(7).build()) + .build()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl(); + writer.write(out, snapshots, EscapingScheme.UNDERSCORE_ESCAPING); + + List metricFamilies = parseProtobufOutput(out); + + assertThat(metricFamilies).hasSize(1); + Metrics.MetricFamily family = metricFamilies.get(0); + assertThat(family.getName()).isEqualTo("legacy"); + assertThat(family.getType()).isEqualTo(Metrics.MetricType.GAUGE); + assertThat(family.getMetricCount()).isEqualTo(1); + assertThat(family.getMetric(0).getGauge().getValue()).isEqualTo(7.0); + } + + private static String getSingleMetricFamilyName( + MetricSnapshots snapshots, EscapingScheme escapingScheme) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl(); + writer.write(out, snapshots, escapingScheme); + List metricFamilies = parseProtobufOutput(out); + assertThat(metricFamilies).hasSize(1); + return metricFamilies.get(0).getName(); + } + private static MetricSnapshots getMetricSnapshots() { PrometheusRegistry registry = new PrometheusRegistry(); diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index 29894b103..8d9643378 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -8,6 +8,7 @@ import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; +import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getLegacyGaugeName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; @@ -123,7 +124,7 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch if (snapshot instanceof CounterSnapshot) { writeCounter(writer, (CounterSnapshot) snapshot, scheme); } else if (snapshot instanceof GaugeSnapshot) { - writeGauge(writer, (GaugeSnapshot) snapshot, scheme); + writeGauge(writer, (GaugeSnapshot) snapshot, s.getMetadata().getOriginalName(), scheme); } else if (snapshot instanceof HistogramSnapshot) { writeHistogram(writer, (HistogramSnapshot) snapshot, scheme); } else if (snapshot instanceof SummarySnapshot) { @@ -189,13 +190,14 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem } } - private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) + private void writeGauge( + Writer writer, GaugeSnapshot snapshot, String rawOriginalName, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, null, "gauge", metadata, scheme); - String name = getMetadataName(metadata, scheme); + String gaugeName = getLegacyGaugeName(metadata, rawOriginalName, scheme); + writeMetadataWithFullName(writer, gaugeName, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, name, null, data.getLabels(), scheme); + writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index 454b3ef7b..c875b94b0 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -666,6 +666,86 @@ void testGaugeWithDots() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, gauge); } + @Test + void testGaugeReservedSuffixCompatibilityWithAlternateEscapingSchemes() throws IOException { + GaugeSnapshot totalGauge = + GaugeSnapshot.builder() + .name("legacy.name.total") + .dataPoint(GaugeDataPointSnapshot.builder().value(6).build()) + .build(); + assertPrometheusText( + """ + # TYPE legacy_dot_name gauge + legacy_dot_name 6.0 + """, + totalGauge, + EscapingScheme.DOTS_ESCAPING); + assertPrometheusText( + """ + # TYPE U__legacy_2e_name gauge + U__legacy_2e_name 6.0 + """, + totalGauge, + EscapingScheme.VALUE_ENCODING_ESCAPING); + } + + @Test + void testGaugeReservedSuffixCompatibilityOutsideOpenMetrics() throws IOException { + GaugeSnapshot createdGauge = + GaugeSnapshot.builder() + .name("test3.created") + .dataPoint(GaugeDataPointSnapshot.builder().value(3).build()) + .build(); + assertOpenMetricsText( + """ + # TYPE U__test3_2e_created gauge + U__test3_2e_created 3.0 + # EOF + """, + createdGauge); + assertPrometheusText( + """ + # TYPE test3 gauge + test3 3.0 + """, + createdGauge); + assertPrometheusTextWithoutCreated( + """ + # TYPE test3 gauge + test3 3.0 + """, + createdGauge); + assertPrometheusProtobuf( + "name: \"test3\" type: GAUGE metric { gauge { value: 3.0 } }", createdGauge); + + GaugeSnapshot totalGauge = + GaugeSnapshot.builder() + .name("test6.total") + .dataPoint(GaugeDataPointSnapshot.builder().value(6).build()) + .build(); + assertOpenMetricsText( + """ + # TYPE U__test6_2e_total gauge + U__test6_2e_total 6.0 + # EOF + """, + totalGauge); + assertPrometheusText( + """ + # TYPE test6 gauge + test6 6.0 + """, + totalGauge); + assertPrometheusTextWithoutCreated( + """ + # TYPE test6 gauge + test6 6.0 + """, + totalGauge); + assertPrometheusProtobuf( + "name: \"test6\" type: GAUGE metric { gauge { value: 6.0 } }", totalGauge); + } + @Test void testGaugeUTF8() throws IOException { String prometheusText = @@ -3088,9 +3168,14 @@ private void assertOpenMetricsTextWithoutCreated(String expected, MetricSnapshot } private void assertPrometheusText(String expected, MetricSnapshot snapshot) throws IOException { + assertPrometheusText(expected, snapshot, EscapingScheme.UNDERSCORE_ESCAPING); + } + + private void assertPrometheusText( + String expected, MetricSnapshot snapshot, EscapingScheme escapingScheme) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); getPrometheusWriter(PrometheusTextFormatWriter.builder().setIncludeCreatedTimestamps(true)) - .write(out, MetricSnapshots.of(snapshot), EscapingScheme.UNDERSCORE_ESCAPING); + .write(out, MetricSnapshots.of(snapshot), escapingScheme); assertThat(out).hasToString(expected); } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java index c3336af17..b8c887f01 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java @@ -113,6 +113,26 @@ public static String getOriginalMetadataName(MetricMetadata metadata, EscapingSc } } + public static String getLegacyGaugeName( + MetricMetadata metadata, String rawOriginalName, EscapingScheme scheme) { + String legacyGaugeBaseName = stripLegacyGaugeSuffix(rawOriginalName); + if (legacyGaugeBaseName != null) { + return PrometheusNaming.escapeName(legacyGaugeBaseName, scheme); + } + return getMetadataName(metadata, scheme); + } + + @Nullable + private static String stripLegacyGaugeSuffix(String rawOriginalName) { + if (rawOriginalName.endsWith(".created")) { + return rawOriginalName.substring(0, rawOriginalName.length() - ".created".length()); + } + if (rawOriginalName.endsWith(".total")) { + return rawOriginalName.substring(0, rawOriginalName.length() - ".total".length()); + } + return null; + } + public static Labels escapeLabels(Labels labels, EscapingScheme scheme) { Labels.Builder outLabelsBuilder = Labels.builder();