Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ Notes](../../RELEASENOTES.md).
([#7219](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7219))

* Fix Prometheus/OpenMetrics serialization to emit metric and label names
containing `:` and `_` instead of dropping them and prefixing leading digits.
containing `_` instead of dropping them and prefixing leading digits.
Invalid characters are replaced with `_` instead of being dropped.
([#7209](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7209))

* Add `escaping=underscores` to the `Accept` header handling for content
negotiation so OpenMetrics are handled correctly.
([#7209](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7209))

* Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket
thresholds are present.
([#7221](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7221))

## 1.15.3-beta.1

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,18 @@ Notes](../../RELEASENOTES.md).
([#7167](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7167))

* Fix Prometheus/OpenMetrics serialization to emit metric and label names
containing `:` and `_` instead of dropping them and prefixing leading digits.
containing and `_` instead of dropping them and prefixing leading digits.
Invalid characters are replaced with `_` instead of being dropped.
([#7209](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7209))

* Add `escaping=underscores` to the `Accept` header handling for content
negotiation so OpenMetrics are handled correctly.
([#7209](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7209))

* Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket
thresholds are present.
([#7221](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7221))

## 1.15.3-beta.1

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,17 @@ public static int WriteMetric(
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var tags = metricPoint.Tags;
var hasNegativeBucketBounds = false;
var previousBound = double.NegativeInfinity;

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);
Expand All @@ -120,27 +126,32 @@ public static int WriteMetric(
previousBound = histogramMeasurement.ExplicitBound;
}

// Histogram sum
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested);
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, openMetricsRequested);

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, openMetricsRequested);
// Histogram count
cursor = WriteMetricName(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested);

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;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,35 @@ public void HistogramOneDimensionWithScopeVersion()
Encoding.UTF8.GetString(buffer, 0, cursor));
}

[Fact]
public void HistogramWithNegativeBucketBoundsOmitsSumAndCountWithOpenMetricsFormat()
{
var buffer = new byte[85000];
var metrics = new List<Metric>();

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<double>("test_histogram");
histogram.Record(-0.5, new KeyValuePair<string, object?>("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()
{
Expand Down
Loading