From 2d9c38ad6554a3e6ee70af33f9d6fcb9248805cf Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 29 Apr 2026 20:58:05 +0100 Subject: [PATCH 1/3] [Exporter.Prometheus] Fix OpenMetrics histograms Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket thresholds are present. --- .../CHANGELOG.md | 4 ++ .../CHANGELOG.md | 4 ++ .../Internal/PrometheusSerializerExt.cs | 39 ++++++++++++------- .../PrometheusSerializerTests.cs | 29 ++++++++++++++ 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index e2171037531..56dc3b4c322 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -24,6 +24,10 @@ Notes](../../RELEASENOTES.md). correctly during Prometheus serialization. ([#7184](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7184)) +* Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket + thresholds are present. + ([#7220](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7220)) + ## 1.15.3-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index ba2c219214b..ee8449e31f8 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -36,6 +36,10 @@ Notes](../../RELEASENOTES.md). response caching. ([#7189](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7189)) +* Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket + thresholds are present. + ([#7220](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7220)) + ## 1.15.3-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 53be3e62334..b11082701b0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -62,10 +62,16 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { var tags = metricPoint.Tags; + var hasNegativeBucketBounds = false; long totalCount = 0; foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) { + if (openMetricsRequested && histogramMeasurement.ExplicitBound < 0) + { + hasNegativeBucketBounds = true; + } + totalCount += histogramMeasurement.BucketCount; cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); @@ -85,27 +91,32 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe buffer[cursor++] = ASCII_LINEFEED; } - // Histogram sum - cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); + if (!openMetricsRequested || !hasNegativeBucketBounds) + { + // OpenMetrics histograms with negative bucket thresholds MUST NOT expose + // _sum and therefore MUST NOT expose _count. + // See https://prometheus.io/docs/specs/om/open_metrics_spec/#histogram-1 + cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); - buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = unchecked((byte)' '); - cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); + cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); - buffer[cursor++] = ASCII_LINEFEED; + buffer[cursor++] = ASCII_LINEFEED; - // Histogram count - cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); + // Histogram count + cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); - buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); + cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); - buffer[cursor++] = ASCII_LINEFEED; + buffer[cursor++] = ASCII_LINEFEED; + } } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index af4087d88c6..d26db64b710 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -797,6 +797,35 @@ public void HistogramOneDimensionWithScopeVersion() Encoding.UTF8.GetString(buffer, 0, cursor)); } + [Fact] + public void HistogramWithNegativeBucketBoundsOmitsSumAndCountWithOpenMetricsFormat() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView(instrument => new ExplicitBucketHistogramConfiguration { Boundaries = [-1, 0, 1] }) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(-0.5, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true); + var output = Encoding.UTF8.GetString(buffer, 0, cursor); + + Assert.Contains($"test_histogram_bucket{{otel_scope_name=\"{Utils.GetCurrentMethodName()}\",x=\"1\",le=\"-1\"}} 0\n", output, StringComparison.Ordinal); + Assert.Contains($"test_histogram_bucket{{otel_scope_name=\"{Utils.GetCurrentMethodName()}\",x=\"1\",le=\"0\"}} 1\n", output, StringComparison.Ordinal); + Assert.Contains($"test_histogram_bucket{{otel_scope_name=\"{Utils.GetCurrentMethodName()}\",x=\"1\",le=\"1\"}} 1\n", output, StringComparison.Ordinal); + Assert.Contains($"test_histogram_bucket{{otel_scope_name=\"{Utils.GetCurrentMethodName()}\",x=\"1\",le=\"+Inf\"}} 1\n", output, StringComparison.Ordinal); + Assert.DoesNotContain("test_histogram_sum{", output, StringComparison.Ordinal); + Assert.DoesNotContain("test_histogram_count{", output, StringComparison.Ordinal); + } + [Fact] public void WriteAsciiStringNoEscapeWritesAsciiBytes() { From 831187d855ec961b243c56c3fa2c179abd34a90e Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 20:59:27 +0100 Subject: [PATCH 2/3] [Exporter.Prometheus] Fix CHANGELOGs Update PR number. --- src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md | 2 +- src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index 56dc3b4c322..3cb353cc117 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -26,7 +26,7 @@ Notes](../../RELEASENOTES.md). * Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket thresholds are present. - ([#7220](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7220)) + ([#7221](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7221)) ## 1.15.3-beta.1 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index ee8449e31f8..2b5beb26d68 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -38,7 +38,7 @@ Notes](../../RELEASENOTES.md). * Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket thresholds are present. - ([#7220](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7220)) + ([#7221](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7221)) ## 1.15.3-beta.1 From 7c58e783583c5385059e3a63227ec4781cd36da5 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 5 May 2026 12:30:58 +0100 Subject: [PATCH 3/3] [Exporter.Prometheus] Fix merge Fix-up merge with main. --- .../Internal/PrometheusSerializerExt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 5156e7888b0..38a2943a8b0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -144,7 +144,7 @@ public static int WriteMetric( // Histogram count cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested); buffer[cursor++] = unchecked((byte)' ');