From d1b0517d1ce3e5053ea6dd9d12bccbedb9b560ae Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 17 Mar 2026 08:48:20 +0000 Subject: [PATCH 1/2] feat: OM2 writer outputs names as provided, no suffix appending The OM2 writer now uses expositionBaseName instead of appending _total (counters) or unit suffixes. The _info suffix is enforced per the OM2 spec (MUST). Tests updated to verify OM2-specific output rather than asserting identity with OM1. Signed-off-by: Gregor Zeitlinger --- .../OpenMetrics2TextFormatWriter.java | 118 +++++++++--------- .../OpenMetrics2TextFormatWriterTest.java | 60 +++++++-- 2 files changed, 110 insertions(+), 68 deletions(-) diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 0e03a112a..53df3dc49 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -6,7 +6,7 @@ import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp; -import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; +import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; import io.prometheus.metrics.config.EscapingScheme; @@ -40,9 +40,9 @@ import javax.annotation.Nullable; /** - * Write the OpenMetrics 2.0 text format. This is currently a skeleton implementation that produces - * identical output to OpenMetrics 1.0, with infrastructure for future OM2 features. This is - * experimental and subject to change as the OpenMetrics * 2.0 specification evolves. */ @@ -171,22 +171,24 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "counter", metadata, scheme); + // OM2: use the name as provided by the user, no _total appending + String counterName = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, counterName, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); + writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, counterName, data, scheme); } } private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "gauge", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -199,20 +201,21 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); + String name = getExpositionBaseMetadataName(metadata, scheme); if (snapshot.isGaugeHistogram()) { - writeMetadata(writer, "gaugehistogram", metadata, scheme); + writeMetadataWithName(writer, name, "gaugehistogram", metadata); writeClassicHistogramBuckets( - writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); + writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); } else { - writeMetadata(writer, "histogram", metadata, scheme); + writeMetadataWithName(writer, name, "histogram", metadata); writeClassicHistogramBuckets( - writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); + writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme); } } private void writeClassicHistogramBuckets( Writer writer, - MetricMetadata metadata, + String name, String countSuffix, String sumSuffix, List dataList, @@ -225,13 +228,7 @@ private void writeClassicHistogramBuckets( for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - "_bucket", - data.getLabels(), - scheme, - "le", - buckets.getUpperBound(i)); + writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); Exemplar exemplar; if (i == 0) { @@ -243,9 +240,9 @@ private void writeClassicHistogramBuckets( } // In OpenMetrics format, histogram _count and _sum are either both present or both absent. if (data.hasCount() && data.hasSum()) { - writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme); + writeCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme); } - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, name, data, scheme); } } @@ -263,12 +260,13 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); + String name = getExpositionBaseMetadataName(metadata, scheme); for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { continue; } if (!metadataWritten) { - writeMetadata(writer, "summary", metadata, scheme); + writeMetadataWithName(writer, name, "summary", metadata); metadataWritten = true; } Exemplars exemplars = data.getExemplars(); @@ -280,13 +278,7 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem int exemplarIndex = 1; for (Quantile quantile : data.getQuantiles()) { writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - null, - data.getLabels(), - scheme, - "quantile", - quantile.getQuantile()); + writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { exemplarIndex = (exemplarIndex + 1) % exemplars.size(); @@ -296,18 +288,20 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem } } // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. - writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme); - writeCreated(writer, metadata, data, scheme); + writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme); + writeCreated(writer, name, data, scheme); } } private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "info", metadata, scheme); + // OM2 spec: Info MetricFamily name MUST end in _info + String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info"); + String baseName = removeSuffix(infoName, "_info"); + writeMetadataWithName(writer, baseName, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme); + writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); writer.write("1"); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } @@ -316,10 +310,11 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "stateset", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "stateset", metadata); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { for (int i = 0; i < data.size(); i++) { - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write('{'); Labels labels = data.getLabels(); for (int j = 0; j < labels.size(); j++) { @@ -334,7 +329,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch if (!labels.isEmpty()) { writer.write(","); } - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write("=\""); writeEscapedString(writer, data.getName(i)); writer.write("\"} "); @@ -351,9 +346,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "unknown", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "unknown", metadata); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -365,7 +361,7 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem private void writeCountAndSum( Writer writer, - MetricMetadata metadata, + String name, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, @@ -373,8 +369,7 @@ private void writeCountAndSum( EscapingScheme scheme) throws IOException { if (data.hasCount()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme); writeLong(writer, data.getCount()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); @@ -383,19 +378,17 @@ private void writeCountAndSum( } } if (data.hasSum()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } } private void writeCreated( - Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) + Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme) throws IOException { if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme); + writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme); writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); if (data.hasScrapeTimestamp()) { writer.write(' '); @@ -466,27 +459,40 @@ private void writeScrapeTimestampAndExemplar( writer.write('\n'); } - private void writeMetadata( - Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) - throws IOException { + private void writeMetadataWithName( + Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getHelp()); writer.write('\n'); } } + + private static String ensureSuffix(String name, String suffix) { + if (name.endsWith(suffix)) { + return name; + } + return name + suffix; + } + + private static String removeSuffix(String name, String suffix) { + if (name.endsWith(suffix)) { + return name.substring(0, name.length() - suffix.length()); + } + return name; + } } diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java index 25f3d9b3d..780f69708 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java @@ -87,7 +87,7 @@ void testGetOpenMetrics2Properties() { } @Test - void testOutputIdenticalToOM1ForCounter() throws IOException { + void testCounterNoTotalSuffix() throws IOException { MetricSnapshots snapshots = MetricSnapshots.of( CounterSnapshot.builder() @@ -101,10 +101,37 @@ void testOutputIdenticalToOM1ForCounter() throws IOException { .build()) .build()); - String om1Output = writeWithOM1(snapshots); String om2Output = writeWithOM2(snapshots); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: name as provided, no _total appending + assertThat(om2Output) + .isEqualTo( + "# TYPE my_counter_seconds counter\n" + + "# UNIT my_counter_seconds seconds\n" + + "# HELP my_counter_seconds Test counter\n" + + "my_counter_seconds{method=\"GET\"} 42.0\n" + + "# EOF\n"); + } + + @Test + void testCounterWithTotalSuffix() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + CounterSnapshot.builder() + .name("requests_total") + .help("Total requests") + .dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder().value(100.0).build()) + .build()); + + String om2Output = writeWithOM2(snapshots); + + // OM2: preserves _total if user provided it + assertThat(om2Output) + .isEqualTo( + "# TYPE requests_total counter\n" + + "# HELP requests_total Total requests\n" + + "requests_total 100.0\n" + + "# EOF\n"); } @Test @@ -220,7 +247,7 @@ void testOutputIdenticalToOM1ForStateSet() throws IOException { } @Test - void testOutputIdenticalToOM1WithExemplars() throws IOException { + void testCounterWithExemplars() throws IOException { Exemplar exemplar = Exemplar.builder() .value(100.0) @@ -241,14 +268,20 @@ void testOutputIdenticalToOM1WithExemplars() throws IOException { .build()) .build()); - String om1Output = writeWithOM1(snapshots); String om2Output = writeWithOM2(snapshots); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: no _total, but exemplar is preserved + assertThat(om2Output) + .isEqualTo( + "# TYPE requests counter\n" + + "# HELP requests Total requests\n" + + "requests 1000.0 # {span_id=\"12345\",trace_id=\"abcde\"}" + + " 100.0 1672850685.829\n" + + "# EOF\n"); } @Test - void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException { + void testCounterWithCreatedTimestamps() throws IOException { MetricSnapshots snapshots = MetricSnapshots.of( CounterSnapshot.builder() @@ -261,16 +294,19 @@ void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException { .build()) .build()); - OpenMetricsTextFormatWriter om1Writer = - OpenMetricsTextFormatWriter.builder().setCreatedTimestampsEnabled(true).build(); - OpenMetrics2TextFormatWriter om2Writer = OpenMetrics2TextFormatWriter.builder().setCreatedTimestampsEnabled(true).build(); - String om1Output = write(snapshots, om1Writer); String om2Output = write(snapshots, om2Writer); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: no _total, _created uses the counter name directly + assertThat(om2Output) + .isEqualTo( + "# TYPE my_counter counter\n" + + "# HELP my_counter Test counter\n" + + "my_counter 42.0\n" + + "my_counter_created 1672850385.800\n" + + "# EOF\n"); } @Test From 9389e8d1ae46ba98b34abbe48479fb300e877972 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 20 Mar 2026 12:44:07 +0000 Subject: [PATCH 2/2] fix: OM2 Info TYPE/HELP lines use full name including _info OM2 spec requires metric name to match MetricFamily name. The Info writer was stripping _info for TYPE/HELP lines (OM1 convention) while keeping it on data lines. Signed-off-by: Gregor Zeitlinger --- .../OpenMetrics2TextFormatWriter.java | 13 +++---------- .../OpenMetrics2TextFormatWriterTest.java | 11 ++++++++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 53df3dc49..02f59b527 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -296,10 +296,10 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - // OM2 spec: Info MetricFamily name MUST end in _info + // OM2 spec: Info MetricFamily name MUST end in _info. + // In OM2, TYPE/HELP use the same name as the data lines. String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info"); - String baseName = removeSuffix(infoName, "_info"); - writeMetadataWithName(writer, baseName, "info", metadata); + writeMetadataWithName(writer, infoName, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); writer.write("1"); @@ -488,11 +488,4 @@ private static String ensureSuffix(String name, String suffix) { } return name + suffix; } - - private static String removeSuffix(String name, String suffix) { - if (name.endsWith(suffix)) { - return name.substring(0, name.length() - suffix.length()); - } - return name; - } } diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java index 780f69708..efa803db0 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java @@ -207,7 +207,7 @@ void testOutputIdenticalToOM1ForSummary() throws IOException { } @Test - void testOutputIdenticalToOM1ForInfo() throws IOException { + void testInfoHelpNameMatchesMeterName() throws IOException { MetricSnapshots snapshots = MetricSnapshots.of( InfoSnapshot.builder() @@ -219,10 +219,15 @@ void testOutputIdenticalToOM1ForInfo() throws IOException { .build()) .build()); - String om1Output = writeWithOM1(snapshots); String om2Output = writeWithOM2(snapshots); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: TYPE/HELP use the full name including _info (help name == meter name) + assertThat(om2Output) + .isEqualTo( + "# TYPE my_info info\n" + + "# HELP my_info Test info\n" + + "my_info{platform=\"linux\",version=\"1.0\"} 1\n" + + "# EOF\n"); } @Test