From 68e1bd2a022726ed48bfc488698ff8ac973c69c5 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 7 Oct 2026 15:16:24 +0530 Subject: [PATCH 1/2] feat: preserve metric suffixes in OpenMetrics 2.0 Signed-off-by: Arnab Nandy --- docs/content/config/config.md | 5 ++- docs/content/exporters/openmetrics2.md | 41 +++++++++++-------- docs/content/getting-started/metric-types.md | 6 +-- .../config/OpenMetrics2Properties.java | 34 +++++++++++++-- .../config/OpenMetrics2PropertiesTest.java | 14 ++++++- .../config/PrometheusPropertiesTest.java | 2 + .../OpenMetrics2TextFormatWriterTest.java | 32 ++++++++++++--- .../OpenMetrics2TextFormatWriter.java | 37 +++++++++++------ .../OpenMetricsTextFormatWriter.java | 9 +++- .../OpenMetrics2TextFormatWriterTest.java | 15 ++++--- 10 files changed, 143 insertions(+), 52 deletions(-) diff --git a/docs/content/config/config.md b/docs/content/config/config.md index d3110939f7..6fde5cd6e6 100644 --- a/docs/content/config/config.md +++ b/docs/content/config/config.md @@ -146,10 +146,11 @@ This works for all Metrics properties. | io.prometheus.openmetrics2.composite_values | [OpenMetrics2Properties.getCompositeValues()]() | (1) | | io.prometheus.openmetrics2.exemplar_compliance | [OpenMetrics2Properties.getExemplarCompliance()]() | (1) | | io.prometheus.openmetrics2.native_histograms | [OpenMetrics2Properties.getNativeHistograms()]() | (1) | +| io.prometheus.openmetrics2.suffixes | [OpenMetrics2Properties.getSuffixes()]() | (1) | (1) Boolean value, `true` or `false`. `enabled=true` switches OpenMetrics responses to the OM2 -writer, preserving metric names as written by the application. The other OM2 properties remain -opt-in. All OpenMetrics 2.0 flags are experimental and default to `false`. +writer. The `suffixes` property defaults to `true` to preserve OM1 series names. The other OM2 +properties remain opt-in and default to `false`. All OpenMetrics 2.0 flags are experimental. ## Exporter Filter Properties diff --git a/docs/content/exporters/openmetrics2.md b/docs/content/exporters/openmetrics2.md index 607273881d..6db44c5cda 100644 --- a/docs/content/exporters/openmetrics2.md +++ b/docs/content/exporters/openmetrics2.md @@ -35,7 +35,7 @@ only need to configure the sub-flags you want. With `enabled=true` alone: - OpenMetrics requests use the OM2 writer. -- Metric names are preserved as written by the application. +- Counter and unit suffixes are appended so that series names remain compatible with OM1. - Optional OM2 features such as `composite_values`, `exemplar_compliance`, and `native_histograms` remain off. @@ -56,37 +56,43 @@ PrometheusProperties properties = PrometheusProperties.builder() ## Naming Behavior -OpenMetrics 2.0 removes OM1 suffix rewriting. +By default, the OpenMetrics 2.0 writer keeps OM1 suffix behavior so that switching formats does not +rename existing series: -- Counters do not get `_total` appended automatically. -- Units do not get appended automatically. -- Info metrics still end in `_info` because that is required by the spec. +- Counters get `_total` appended when it is missing. +- Unit suffixes are appended when they are missing. +- Existing suffixes are not duplicated. +- Info metrics end in `_info` because that is required by the spec. Examples: -| Metric builder input | OM1 output | OM2 output | -| ---------------------------------- | ----------------- | -------------- | -| `Counter("events")` | `events_total` | `events` | -| `Counter("events_total")` | `events_total` | `events_total` | -| `Counter("req").unit(BYTES)` | `req_bytes_total` | `req` | -| `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | `req_bytes` | -| `Info("target")` | `target_info` | `target_info` | +| Metric builder input | OM1 and default OM2 output | OM2 with `suffixes=false` | +| ---------------------------------- | -------------------------- | ------------------------- | +| `Counter("events")` | `events_total` | `events` | +| `Counter("events_total")` | `events_total` | `events_total` | +| `Counter("req").unit(BYTES)` | `req_bytes_total` | `req` | +| `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | `req_bytes` | +| `Info("target")` | `target_info` | `target_info` | -This means OpenMetrics 2.0 does not apply OM1 suffix behavior such as appending `_total` or unit -suffixes, while the legacy OpenMetrics 1.0 and Prometheus text formats keep that existing suffix -behavior. +To emit metric names exactly as written by the application, set: + +```properties +io.prometheus.openmetrics2.suffixes=false +``` ## Feature Flags -All OpenMetrics 2.0 flags default to `false`. +OpenMetrics 2.0 feature flags default to `false`, except `suffixes`, which defaults to `true` to +preserve series names when migrating from OM1. | Property | Effect | | ------------------------------------------------ | -------------------------------------------------------------------------------------- | -| `io.prometheus.openmetrics2.enabled` | Metric names are preserved as written by the application. | +| `io.prometheus.openmetrics2.enabled` | Enable the OpenMetrics 2.0 writer. | | `io.prometheus.openmetrics2.content_negotiation` | Apply OM2 behavior only when the scraper requests `version=2.0.0`. | | `io.prometheus.openmetrics2.composite_values` | Emit histograms, summaries, and gauge histograms as single composite lines with `st@`. | | `io.prometheus.openmetrics2.exemplar_compliance` | Emit only OM2-compliant exemplars with timestamps. | | `io.prometheus.openmetrics2.native_histograms` | Emit OM2 native histogram text fields. | +| `io.prometheus.openmetrics2.suffixes` | Append counter and unit suffixes to preserve OM1 series names. | Enable all flags at once: @@ -104,6 +110,7 @@ io.prometheus.openmetrics2.content_negotiation=true io.prometheus.openmetrics2.composite_values=true io.prometheus.openmetrics2.exemplar_compliance=true io.prometheus.openmetrics2.native_histograms=true +io.prometheus.openmetrics2.suffixes=true ``` ## Content Negotiation diff --git a/docs/content/getting-started/metric-types.md b/docs/content/getting-started/metric-types.md index 7522051077..6cb1af3df3 100644 --- a/docs/content/getting-started/metric-types.md +++ b/docs/content/getting-started/metric-types.md @@ -41,9 +41,9 @@ For the default OpenMetrics 1.0 and Prometheus text formats, counters are expose `_total` suffix. You can name a counter either `service_time_seconds` or `service_time_seconds_total`; the exposed name will be `service_time_seconds_total` in both cases. -The experimental OpenMetrics 2.0 writer behaves differently: It preserves metric names instead of -appending `_total` or unit suffixes automatically. In OpenMetrics 2.0, `_total` is recommended for -counters, but not enforced by the Java client. +The experimental OpenMetrics 2.0 writer appends `_total` and unit suffixes by default so that +switching from OpenMetrics 1.0 does not rename existing series. Set +`io.prometheus.openmetrics2.suffixes=false` to preserve metric names exactly as written instead. ## Gauge diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java index 1f5080f055..e651524c5c 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java @@ -16,24 +16,28 @@ public class OpenMetrics2Properties { private static final String COMPOSITE_VALUES = "composite_values"; private static final String EXEMPLAR_COMPLIANCE = "exemplar_compliance"; private static final String NATIVE_HISTOGRAMS = "native_histograms"; + private static final String SUFFIXES = "suffixes"; @Nullable private final Boolean enabled; @Nullable private final Boolean contentNegotiation; @Nullable private final Boolean compositeValues; @Nullable private final Boolean exemplarCompliance; @Nullable private final Boolean nativeHistograms; + @Nullable private final Boolean suffixes; private OpenMetrics2Properties( @Nullable Boolean enabled, @Nullable Boolean contentNegotiation, @Nullable Boolean compositeValues, @Nullable Boolean exemplarCompliance, - @Nullable Boolean nativeHistograms) { + @Nullable Boolean nativeHistograms, + @Nullable Boolean suffixes) { this.enabled = enabled; this.contentNegotiation = contentNegotiation; this.compositeValues = compositeValues; this.exemplarCompliance = exemplarCompliance; this.nativeHistograms = nativeHistograms; + this.suffixes = suffixes; } /** @@ -64,6 +68,11 @@ public boolean getNativeHistograms() { return nativeHistograms != null && nativeHistograms; } + /** Append unit and type suffixes to metric names. Default is {@code true}. */ + public boolean getSuffixes() { + return suffixes == null || suffixes; + } + /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -75,8 +84,14 @@ static OpenMetrics2Properties load(PropertySource propertySource) Boolean compositeValues = Util.loadBoolean(PREFIX, COMPOSITE_VALUES, propertySource); Boolean exemplarCompliance = Util.loadBoolean(PREFIX, EXEMPLAR_COMPLIANCE, propertySource); Boolean nativeHistograms = Util.loadBoolean(PREFIX, NATIVE_HISTOGRAMS, propertySource); + Boolean suffixes = Util.loadBoolean(PREFIX, SUFFIXES, propertySource); return new OpenMetrics2Properties( - enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); + enabled, + contentNegotiation, + compositeValues, + exemplarCompliance, + nativeHistograms, + suffixes); } public static Builder builder() { @@ -90,6 +105,7 @@ public static class Builder { @Nullable private Boolean compositeValues; @Nullable private Boolean exemplarCompliance; @Nullable private Boolean nativeHistograms; + @Nullable private Boolean suffixes; private Builder() {} @@ -123,6 +139,12 @@ public Builder nativeHistograms(boolean nativeHistograms) { return this; } + /** See {@link #getSuffixes()} */ + public Builder suffixes(boolean suffixes) { + this.suffixes = suffixes; + return this; + } + /** Enable all OpenMetrics 2.0 features */ public Builder enableAll() { this.enabled = true; @@ -130,12 +152,18 @@ public Builder enableAll() { this.compositeValues = true; this.exemplarCompliance = true; this.nativeHistograms = true; + this.suffixes = true; return this; } public OpenMetrics2Properties build() { return new OpenMetrics2Properties( - enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); + enabled, + contentNegotiation, + compositeValues, + exemplarCompliance, + nativeHistograms, + suffixes); } } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java index 0546a138fc..88682115c3 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java @@ -24,12 +24,15 @@ void load() { "io.prometheus.openmetrics2.exemplar_compliance", "true", "io.prometheus.openmetrics2.native_histograms", - "true"))); + "true", + "io.prometheus.openmetrics2.suffixes", + "false"))); assertThat(properties.getEnabled()).isTrue(); assertThat(properties.getContentNegotiation()).isTrue(); assertThat(properties.getCompositeValues()).isTrue(); assertThat(properties.getExemplarCompliance()).isTrue(); assertThat(properties.getNativeHistograms()).isTrue(); + assertThat(properties.getSuffixes()).isFalse(); } @Test @@ -68,6 +71,10 @@ void loadInvalidValue() { new HashMap<>( Map.of("io.prometheus.openmetrics2.native_histograms", "invalid")))) .withMessage("io.prometheus.openmetrics2.native_histograms: Expecting 'true' or 'false'."); + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> load(new HashMap<>(Map.of("io.prometheus.openmetrics2.suffixes", "invalid")))) + .withMessage("io.prometheus.openmetrics2.suffixes: Expecting 'true' or 'false'."); } private static OpenMetrics2Properties load(Map map) { @@ -85,12 +92,14 @@ void builder() { .compositeValues(false) .exemplarCompliance(true) .nativeHistograms(false) + .suffixes(false) .build(); assertThat(properties.getEnabled()).isTrue(); assertThat(properties.getContentNegotiation()).isTrue(); assertThat(properties.getCompositeValues()).isFalse(); assertThat(properties.getExemplarCompliance()).isTrue(); assertThat(properties.getNativeHistograms()).isFalse(); + assertThat(properties.getSuffixes()).isFalse(); } @Test @@ -101,6 +110,7 @@ void builderEnableAll() { assertThat(properties.getCompositeValues()).isTrue(); assertThat(properties.getExemplarCompliance()).isTrue(); assertThat(properties.getNativeHistograms()).isTrue(); + assertThat(properties.getSuffixes()).isTrue(); } @Test @@ -111,6 +121,7 @@ void defaultValues() { assertThat(properties.getCompositeValues()).isFalse(); assertThat(properties.getExemplarCompliance()).isFalse(); assertThat(properties.getNativeHistograms()).isFalse(); + assertThat(properties.getSuffixes()).isTrue(); } @Test @@ -121,5 +132,6 @@ void partialConfiguration() { assertThat(properties.getCompositeValues()).isTrue(); assertThat(properties.getExemplarCompliance()).isFalse(); assertThat(properties.getNativeHistograms()).isFalse(); + assertThat(properties.getSuffixes()).isTrue(); } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 2c6e11daff..ae5dedbd19 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -181,10 +181,12 @@ void testOpenMetrics2PropertiesLoading() { properties.put("io.prometheus.openmetrics2.composite_values", "false"); properties.put("io.prometheus.openmetrics2.exemplar_compliance", "true"); properties.put("io.prometheus.openmetrics2.native_histograms", "false"); + properties.put("io.prometheus.openmetrics2.suffixes", "false"); PrometheusProperties config = PrometheusPropertiesLoader.load(properties); assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue(); assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse(); assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isTrue(); assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getSuffixes()).isFalse(); } } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java index 38ba672709..2193875e5b 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java @@ -16,6 +16,21 @@ class OpenMetrics2TextFormatWriterTest { + @Test + void suffixesAreEnabledByDefault() throws IOException { + Counter counter = Counter.builder().name("requests").unit(Unit.BYTES).build(); + counter.inc(); + + String output = + writeWithWriter( + MetricSnapshots.of(counter.collect()), OpenMetrics2TextFormatWriter.create()); + + assertThat(output) + .contains("# TYPE requests_bytes_total counter\n") + .contains("# UNIT requests_bytes_total bytes\n") + .containsPattern("(?m)^requests_bytes_total 1\\.0 st@\\d+\\.\\d{3}$"); + } + @Test void counterPreservesOriginalNameWhenUnitIsConfigured() throws IOException { Counter counter = @@ -93,23 +108,30 @@ void nativeHistogramPreservesOriginalNameWhenUnitIsConfigured() throws IOExcepti } private String writeWithOM1(MetricSnapshots snapshots) throws IOException { - return write(snapshots, OpenMetricsTextFormatWriter.create()); + return writeWithWriter(snapshots, OpenMetricsTextFormatWriter.create()); } private String writeWithOM2(MetricSnapshots snapshots) throws IOException { - return write(snapshots, OpenMetrics2TextFormatWriter.create()); + OpenMetrics2TextFormatWriter writer = + OpenMetrics2TextFormatWriter.builder() + .setOpenMetrics2Properties(OpenMetrics2Properties.builder().suffixes(false).build()) + .build(); + return writeWithWriter(snapshots, writer); } private String writeWithNativeHistograms(MetricSnapshots snapshots) throws IOException { OpenMetrics2TextFormatWriter writer = OpenMetrics2TextFormatWriter.builder() .setOpenMetrics2Properties( - OpenMetrics2Properties.builder().nativeHistograms(true).build()) + OpenMetrics2Properties.builder() + .nativeHistograms(true) + .suffixes(false) + .build()) .build(); - return write(snapshots, writer); + return writeWithWriter(snapshots, writer); } - private String write(MetricSnapshots snapshots, ExpositionFormatWriter writer) + private String writeWithWriter(MetricSnapshots snapshots, ExpositionFormatWriter writer) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); writer.write(out, snapshots, EscapingScheme.ALLOW_UTF8); 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 f400ecce0f..696bb1f317 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,6 +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.getExpositionBaseMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getOriginalMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; @@ -40,9 +41,10 @@ import javax.annotation.Nullable; /** - * Write the OpenMetrics 2.0 text format. Unlike the OM1 writer, this writer outputs metric names as - * provided by the user, without appending {@code _total} or unit suffixes. The {@code _info} suffix - * is enforced per the OM2 spec (MUST). This is experimental and subject to change as the OpenMetrics * 2.0 specification evolves. */ @@ -174,8 +176,10 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - // OM2: use the original name, no _total or unit suffix appending. - String counterName = getOriginalMetadataName(metadata, scheme); + String counterName = getMetricName(metadata, scheme); + if (openMetrics2Properties.getSuffixes()) { + counterName = ensureSuffix(counterName, "_total"); + } writeMetadataWithName(writer, counterName, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); @@ -196,7 +200,7 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - String name = getOriginalMetadataName(metadata, scheme); + String name = getMetricName(metadata, scheme); writeMetadataWithName(writer, name, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, name, null, data.getLabels(), scheme); @@ -214,7 +218,7 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS boolean compositeHistogram = openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms(); MetricMetadata metadata = snapshot.getMetadata(); - String name = getOriginalMetadataName(metadata, scheme); + String name = getMetricName(metadata, scheme); if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) { writeClassicHistogram(writer, name, snapshot, scheme); return; @@ -478,14 +482,14 @@ private boolean writeNativeBucketSpan(Writer writer, int offset, int length, boo private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) throws IOException { + MetricMetadata metadata = snapshot.getMetadata(); + String name = getMetricName(metadata, scheme); if (!openMetrics2Properties.getCompositeValues() && !openMetrics2Properties.getExemplarCompliance()) { - om1Writer.writeSummary(writer, snapshot, scheme); + om1Writer.writeSummary(writer, snapshot, scheme, name); return; } boolean metadataWritten = false; - MetricMetadata metadata = snapshot.getMetadata(); - String name = getOriginalMetadataName(metadata, scheme); for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { continue; @@ -552,7 +556,7 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche MetricMetadata metadata = snapshot.getMetadata(); // OM2 spec: Info MetricFamily name MUST end in _info. // In OM2, TYPE/HELP use the same name as the data lines. - String infoName = ensureSuffix(getOriginalMetadataName(metadata, scheme), "_info"); + String infoName = ensureSuffix(getMetricName(metadata, scheme), "_info"); writeMetadataWithName(writer, infoName, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); @@ -564,7 +568,7 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - String name = getOriginalMetadataName(metadata, scheme); + String name = getMetricName(metadata, scheme); writeMetadataWithName(writer, name, "stateset", metadata); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { for (int i = 0; i < data.size(); i++) { @@ -600,7 +604,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - String name = getOriginalMetadataName(metadata, scheme); + String name = getMetricName(metadata, scheme); writeMetadataWithName(writer, name, "unknown", metadata); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, name, null, data.getLabels(), scheme); @@ -714,6 +718,13 @@ private void writeMetadataWithName( } } + private String getMetricName(MetricMetadata metadata, EscapingScheme scheme) { + if (openMetrics2Properties.getSuffixes()) { + return getExpositionBaseMetadataName(metadata, scheme); + } + return getOriginalMetadataName(metadata, scheme); + } + private static String ensureSuffix(String name, String suffix) { if (name.endsWith(suffix)) { return name; diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 02614a5610..7e8f1a98a9 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -234,9 +234,14 @@ private ClassicHistogramBuckets getClassicBuckets( void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) throws IOException { + writeSummary(writer, snapshot, scheme, getMetadataName(snapshot.getMetadata(), scheme)); + } + + void writeSummary( + Writer writer, SummarySnapshot snapshot, EscapingScheme scheme, String name) + throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); - String name = getMetadataName(metadata, scheme); String countName = name + "_count"; String sumName = name + "_sum"; for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { @@ -244,7 +249,7 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme continue; } if (!metadataWritten) { - writeMetadata(writer, "summary", metadata, scheme); + writeMetadataWithName(writer, name, "summary", metadata); metadataWritten = true; } Exemplars exemplars = data.getExemplars(); 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 83c815c00a..1e9b721c4d 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 @@ -273,14 +273,14 @@ void testCounterStartTimestampWithDefaultWriter() throws IOException { .build()) .build()); - String om2Output = writeWithOM2(snapshots); + String om2Output = write(snapshots, OpenMetrics2TextFormatWriter.create()); - // OM2: no _total, start timestamp uses st@ inline. + // OM2 appends _total by default and emits the start timestamp inline. assertThat(om2Output) .isEqualTo( - "# TYPE my_counter counter\n" - + "# HELP my_counter Test counter\n" - + "my_counter 42.0 st@1672850385.800\n" + "# TYPE my_counter_total counter\n" + + "# HELP my_counter_total Test counter\n" + + "my_counter_total 42.0 st@1672850385.800\n" + "# EOF\n"); } @@ -834,7 +834,10 @@ private String writeWithOM1(MetricSnapshots snapshots) throws IOException { } private String writeWithOM2(MetricSnapshots snapshots) throws IOException { - OpenMetrics2TextFormatWriter writer = OpenMetrics2TextFormatWriter.create(); + OpenMetrics2TextFormatWriter writer = + OpenMetrics2TextFormatWriter.builder() + .setOpenMetrics2Properties(OpenMetrics2Properties.builder().suffixes(false).build()) + .build(); return write(snapshots, writer); } From c88ee73ea161c7a3b625d4116d670b70d5a0bead Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 7 Oct 2026 20:07:02 +0530 Subject: [PATCH 2/2] fix: address OpenMetrics suffix CI failures Signed-off-by: Arnab Nandy --- .../current_vs_latest/prometheus-metrics-config.txt | 6 ++++++ .../core/metrics/OpenMetrics2TextFormatWriterTest.java | 5 +---- .../exporter/common/PrometheusScrapeHandlerTest.java | 2 +- .../expositionformats/OpenMetricsTextFormatWriter.java | 3 +-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/prometheus-metrics-config.txt b/docs/apidiffs/current_vs_latest/prometheus-metrics-config.txt index 85091e003e..d83c50781b 100644 --- a/docs/apidiffs/current_vs_latest/prometheus-metrics-config.txt +++ b/docs/apidiffs/current_vs_latest/prometheus-metrics-config.txt @@ -2,4 +2,10 @@ Comparing source compatibility of prometheus-metrics-config-1.9.1-SNAPSHOT.jar a *** MODIFIED ENUM: PUBLIC FINAL io.prometheus.metrics.config.EscapingScheme (compatible) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 +++ NEW METHOD: PUBLIC(+) STATIC(+) io.prometheus.metrics.config.EscapingScheme fromAcceptHeader(java.lang.String, io.prometheus.metrics.config.EscapingScheme) +*** MODIFIED CLASS: PUBLIC io.prometheus.metrics.config.OpenMetrics2Properties (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) boolean getSuffixes() +*** MODIFIED CLASS: PUBLIC STATIC io.prometheus.metrics.config.OpenMetrics2Properties$Builder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.config.OpenMetrics2Properties$Builder suffixes(boolean) diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java index 2193875e5b..5d4aac1d38 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/OpenMetrics2TextFormatWriterTest.java @@ -123,10 +123,7 @@ private String writeWithNativeHistograms(MetricSnapshots snapshots) throws IOExc OpenMetrics2TextFormatWriter writer = OpenMetrics2TextFormatWriter.builder() .setOpenMetrics2Properties( - OpenMetrics2Properties.builder() - .nativeHistograms(true) - .suffixes(false) - .build()) + OpenMetrics2Properties.builder().nativeHistograms(true).suffixes(false).build()) .build(); return writeWithWriter(snapshots, writer); } diff --git a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java index 5e823360f8..1f20b965ee 100644 --- a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java +++ b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java @@ -200,7 +200,7 @@ void testOpenMetrics2DoesNotEscapeUtf8NamesByDefault() throws IOException { for (boolean contentNegotiation : new boolean[] {true, false}) { String body = scrapeUtf8Counter(contentNegotiation, "application/openmetrics-text;version=2.0.0"); - assertThat(body).contains("\"my.counter\"").doesNotContain("my_counter"); + assertThat(body).contains("\"my.counter_total\"").doesNotContain("my_counter"); } } diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 7e8f1a98a9..3cfce80a8e 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -237,8 +237,7 @@ void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme writeSummary(writer, snapshot, scheme, getMetadataName(snapshot.getMetadata(), scheme)); } - void writeSummary( - Writer writer, SummarySnapshot snapshot, EscapingScheme scheme, String name) + void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme, String name) throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata();