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..6dd0bbd31 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 @@ -21,6 +21,7 @@ import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; import io.prometheus.metrics.model.snapshots.Quantiles; import io.prometheus.metrics.model.snapshots.SnapshotEscaper; import io.prometheus.metrics.model.snapshots.StateSetSnapshot; @@ -82,7 +83,7 @@ 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(), null, Metrics.MetricType.GAUGE, scheme, true); } else if (snapshot instanceof HistogramSnapshot) { HistogramSnapshot histogram = (HistogramSnapshot) snapshot; for (HistogramSnapshot.HistogramDataPointSnapshot data : histogram.getDataPoints()) { @@ -290,25 +291,53 @@ private void setMetadataUnlessEmpty( @Nullable String nameSuffix, Metrics.MetricType type, EscapingScheme scheme) { + setMetadataUnlessEmpty(builder, metadata, nameSuffix, type, scheme, false); + } + + private void setMetadataUnlessEmpty( + Metrics.MetricFamily.Builder builder, + MetricMetadata metadata, + @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, nameSuffix, scheme, normalizeLegacyGaugeName)); if (metadata.getHelp() != null) { builder.setHelp(metadata.getHelp()); } builder.setType(type); } + private String resolveMetricFamilyName( + MetricMetadata metadata, + @Nullable String nameSuffix, + EscapingScheme scheme, + boolean normalizeLegacyGaugeName) { + if (normalizeLegacyGaugeName) { + String originalName = metadata.getOriginalName(); + if (originalName.endsWith(".created")) { + return PrometheusNaming.escapeName( + originalName.substring(0, originalName.length() - ".created".length()), scheme); + } + if (originalName.endsWith(".total")) { + return PrometheusNaming.escapeName( + originalName.substring(0, originalName.length() - ".total".length()), 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..ba0b653c0 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,28 @@ void testDifferentMetrics_producesSeparateMetricFamilies() throws IOException { assertThat(gaugeFamily.getMetric(0).getGauge().getValue()).isEqualTo(50.0); } + @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 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 b40dcfdf2..af4300c94 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 @@ -192,9 +192,10 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "", "gauge", metadata, scheme); + String gaugeName = resolveLegacyGaugeName(metadata, scheme); + writeMetadataWithFullName(writer, gaugeName, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -475,6 +476,19 @@ private static String resolveBaseName(String fullName, String suffix) { return fullName; } + private static String resolveLegacyGaugeName(MetricMetadata metadata, EscapingScheme scheme) { + String originalName = metadata.getOriginalName(); + if (originalName.endsWith(".created")) { + return PrometheusNaming.escapeName( + originalName.substring(0, originalName.length() - ".created".length()), scheme); + } + if (originalName.endsWith(".total")) { + return PrometheusNaming.escapeName( + originalName.substring(0, originalName.length() - ".total".length()), scheme); + } + return getMetadataName(metadata, scheme); + } + private void writeEscapedHelp(Writer writer, String s) throws IOException { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); 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..3deeb6b37 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,63 @@ void testGaugeWithDots() throws IOException { assertPrometheusProtobuf(prometheusProtobuf, gauge); } + @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 =