Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fb90431
[OpenTelemetry] Drop non-finite histogram values
martincostello Sep 15, 2026
a3ef8ad
[OpenTelemetry] Update CHANGELOG
martincostello Sep 15, 2026
47c5453
Merge branch 'main' into drop-nonfinite-from-views
martincostello Sep 16, 2026
7256ade
[OpenTelemetry] Address feedback
martincostello Sep 17, 2026
c8a54e9
[OpenTelemetry] Address feedback
martincostello Sep 17, 2026
09de2b6
Merge branch 'main' into drop-nonfinite-from-views
martincostello Sep 18, 2026
95b5cd3
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 2, 2026
6a129f5
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 7, 2026
fe7b759
Merge branch 'main' into drop-nonfinite-from-views
matt-hensley Oct 7, 2026
7baf43c
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 20, 2026
013c29e
[OpenTelemetry] Update CHANGELOG
martincostello Oct 20, 2026
967485a
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 21, 2026
c8178a7
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 22, 2026
5c54bc9
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 23, 2026
be996ce
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 23, 2026
61704c6
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 23, 2026
e5d2e6d
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 26, 2026
3519e6d
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 28, 2026
460fde0
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 28, 2026
8a02a6f
[OpenTelemetry] Update CHANGELOG
martincostello Oct 28, 2026
e8cd0c4
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 30, 2026
6372ee2
Merge branch 'main' into drop-nonfinite-from-views
martincostello Oct 30, 2026
8ff8ddf
[OpenTelemetry] Address feedback.
martincostello Nov 1, 2026
c6671f5
Merge branch 'main' into drop-nonfinite-from-views
martincostello Nov 2, 2026
5d18da4
Merge branch 'main' into drop-nonfinite-from-views
martincostello Nov 3, 2026
3a74d3c
Merge branch 'main' into drop-nonfinite-from-views
martincostello Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Notes](../../RELEASENOTES.md).
initialized provider state.
([#7761](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7761))

* Non-finite histogram measurements (`NaN`, `+Inf`, `-Inf`) are now dropped
rather than recorded, so they can no longer permanently corrupt the sum,
min, and max of a cumulative histogram stream. This aligns the explicit-bucket
and exponential histograms with the metrics SDK specification.
([#7759](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7759))

## 1.18.0

Released 2026-Aug-21
Expand Down
18 changes: 18 additions & 0 deletions src/OpenTelemetry/Metrics/AggregatorStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal sealed class AggregatorStore
private readonly MetricPoint[] metricPoints;
private readonly int[] currentMetricPointBatch;
private readonly AggregationType aggType;
private readonly bool isHistogramAggregation;
private readonly HistogramExplicitBounds histogramExplicitBounds;
private readonly int exponentialHistogramMaxSize;
private readonly int exponentialHistogramMaxScale;
Expand Down Expand Up @@ -93,6 +94,15 @@ internal AggregatorStore(
this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale;
this.StartTimeExclusive = DateTimeOffset.UtcNow;
this.ExemplarReservoirFactory = exemplarReservoirFactory;

this.isHistogramAggregation = aggType is
AggregationType.Histogram
or AggregationType.HistogramWithMinMax
or AggregationType.HistogramWithBuckets
or AggregationType.HistogramWithMinMaxBuckets
or AggregationType.Base2ExponentialHistogram
or AggregationType.Base2ExponentialHistogramWithMinMax;

if (metricStreamIdentity.TagKeys != null)
{
this.updateLongCallback = this.UpdateLongCustomTags;
Expand Down Expand Up @@ -213,6 +223,14 @@ internal void Update(long value, ReadOnlySpan<KeyValuePair<string, object?>> tag

internal void Update(double value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
{
if (this.isHistogramAggregation && (double.IsNaN(value) || double.IsInfinity(value)))
{
// Reject non-finite histogram measurements before tag lookup: the value is always
// dropped, so a unique tag set carrying only non-finite values must not consume a
// cardinality slot that a subsequent, valid tag set could need.
return;
}

try
{
this.updateDoubleCallback(value, tags);
Expand Down
38 changes: 36 additions & 2 deletions src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,13 @@ internal void NullifyMetricPointState()

private void UpdateHistogram(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (double.IsNaN(number) || double.IsInfinity(number))
{
// Non-finite values would poison RunningSum for the lifetime of a cumulative stream.
this.CompleteUpdateWithoutMeasurement();
return;
Comment thread
martincostello marked this conversation as resolved.
Resolved
}

var histogramBuckets = this.mpComponents!.HistogramBuckets!;

this.mpComponents.AcquireLock();
Expand All @@ -966,6 +973,14 @@ private void UpdateHistogram(double number, ReadOnlySpan<KeyValuePair<string, ob

private void UpdateHistogramWithMinMax(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (double.IsNaN(number) || double.IsInfinity(number))
{
// Non-finite values would poison RunningSum/RunningMin/RunningMax for the lifetime of
// a cumulative stream.
this.CompleteUpdateWithoutMeasurement();
return;
}

var histogramBuckets = this.mpComponents!.HistogramBuckets!;

this.mpComponents.AcquireLock();
Expand All @@ -988,6 +1003,13 @@ private void UpdateHistogramWithMinMax(double number, ReadOnlySpan<KeyValuePair<

private void UpdateHistogramWithBuckets(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (double.IsNaN(number) || double.IsInfinity(number))
{
// Non-finite values would poison RunningSum for the lifetime of a cumulative stream.
this.CompleteUpdateWithoutMeasurement();
return;
}

var histogramBuckets = this.mpComponents!.HistogramBuckets!;

var bucketIndex = histogramBuckets.FindBucketIndex(number);
Expand All @@ -1010,6 +1032,14 @@ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan<KeyValuePair

private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (double.IsNaN(number) || double.IsInfinity(number))
{
// Non-finite values would poison RunningSum/RunningMin/RunningMax for the lifetime of
// a cumulative stream.
this.CompleteUpdateWithoutMeasurement();
return;
}

var histogramBuckets = this.mpComponents!.HistogramBuckets!;

var bucketIndex = histogramBuckets.FindBucketIndex(number);
Expand All @@ -1035,8 +1065,10 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan<Key

private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (number < 0)
if (number < 0 || double.IsNaN(number) || double.IsInfinity(number))
{
// Negatives are undefined for exponential histograms; non-finite values would also
// poison RunningSum/RunningMin/RunningMax for the lifetime of a cumulative stream.
Comment thread
martincostello marked this conversation as resolved.
Resolved
this.CompleteUpdateWithoutMeasurement();
return;
}
Expand All @@ -1061,8 +1093,10 @@ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan<KeyValu

private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan<KeyValuePair<string, object?>> tags = default, bool offerExemplar = false)
{
if (number < 0)
if (number < 0 || double.IsNaN(number) || double.IsInfinity(number))
{
// Negatives are undefined for exponential histograms; non-finite values would also
// poison RunningSum/RunningMin/RunningMax for the lifetime of a cumulative stream.
this.CompleteUpdateWithoutMeasurement();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ test_histogram_bucket{otel_scope_name="HistogramInfinities",le="2500"} 1
test_histogram_bucket{otel_scope_name="HistogramInfinities",le="5000"} 1
test_histogram_bucket{otel_scope_name="HistogramInfinities",le="7500"} 1
test_histogram_bucket{otel_scope_name="HistogramInfinities",le="10000"} 1
test_histogram_bucket{otel_scope_name="HistogramInfinities",le="+Inf"} 3
test_histogram_sum{otel_scope_name="HistogramInfinities"} +Inf
test_histogram_count{otel_scope_name="HistogramInfinities"} 3
test_histogram_bucket{otel_scope_name="HistogramInfinities",le="+Inf"} 1
test_histogram_sum{otel_scope_name="HistogramInfinities"} 18
test_histogram_count{otel_scope_name="HistogramInfinities"} 1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ test_histogram_bucket{otel_scope_name="HistogramNaN",le="2500"} 1
test_histogram_bucket{otel_scope_name="HistogramNaN",le="5000"} 1
test_histogram_bucket{otel_scope_name="HistogramNaN",le="7500"} 1
test_histogram_bucket{otel_scope_name="HistogramNaN",le="10000"} 1
test_histogram_bucket{otel_scope_name="HistogramNaN",le="+Inf"} 3
test_histogram_sum{otel_scope_name="HistogramNaN"} NaN
test_histogram_count{otel_scope_name="HistogramNaN"} 3
test_histogram_bucket{otel_scope_name="HistogramNaN",le="+Inf"} 1
test_histogram_sum{otel_scope_name="HistogramNaN"} 18
test_histogram_count{otel_scope_name="HistogramNaN"} 1
75 changes: 75 additions & 0 deletions test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,81 @@ public AggregatorTests()

public static TheoryData<HistogramBoundaryTestCase> HistogramInfinityBoundariesTestCases => HistogramBoundaryTestCase.HistogramInfinityBoundariesTestCases();

public static TheoryData<int, double, bool> NonFiniteHistogramMeasurementTestCases()
{
var data = new TheoryData<int, double, bool>();

// Typed as an integer as AggregationType is internal
AggregationType[] aggregationTypes =
[
AggregationType.Histogram,
AggregationType.HistogramWithMinMax,
AggregationType.HistogramWithBuckets,
AggregationType.HistogramWithMinMaxBuckets,
AggregationType.Base2ExponentialHistogram,
AggregationType.Base2ExponentialHistogramWithMinMax,
];

double[] nonFiniteValues = [double.NaN, double.PositiveInfinity, double.NegativeInfinity];

foreach (var aggregationType in aggregationTypes)
{
foreach (var nonFinite in nonFiniteValues)
{
data.Add((int)aggregationType, nonFinite, false);
data.Add((int)aggregationType, nonFinite, true);
}
}

return data;
}

[Theory]
[MemberData(nameof(NonFiniteHistogramMeasurementTestCases))]
public void NonFiniteMeasurementIsIgnoredByHistogram(int aggregationTypeValue, double nonFinite, bool outputDelta)
{
var aggregationType = (AggregationType)aggregationTypeValue;
var boundaries = new HistogramExplicitBounds(Metric.DefaultHistogramBounds);
var temporality = outputDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative;
var aggregatorStore = new AggregatorStore(MetricStreamIdentity, aggregationType, temporality, 1024);
var histogramPoint = new MetricPoint(aggregatorStore, aggregationType, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale);

histogramPoint.Update(1);
histogramPoint.Update(nonFinite);
histogramPoint.Update(2);
histogramPoint.Update(3);

histogramPoint.TakeSnapshot(outputDelta);

// The non-finite measurement is dropped: only the three finite values are counted...
Assert.Equal(3, histogramPoint.GetHistogramCount());

// ...and the sum is exactly 1 + 2 + 3, not poisoned to NaN/Infinity.
Assert.Equal(6, histogramPoint.GetHistogramSum());

if (aggregationType is AggregationType.HistogramWithMinMax or
AggregationType.HistogramWithMinMaxBuckets or
AggregationType.Base2ExponentialHistogramWithMinMax)
{
// Min/max are not poisoned to NaN/Infinity either.
Assert.True(histogramPoint.TryGetHistogramMinMaxValues(out var min, out var max));
Assert.Equal(1, min);
Assert.Equal(3, max);
}

if (aggregationType is AggregationType.HistogramWithBuckets or AggregationType.HistogramWithMinMaxBuckets)
{
// The non-finite measurement was not distributed into any explicit bucket.
var bucketedCount = 0L;
foreach (var bucket in histogramPoint.GetHistogramBuckets())
{
bucketedCount += bucket.BucketCount;
}

Assert.Equal(3, bucketedCount);
}
}

[Fact]
public void HistogramDistributeToAllBucketsDefault()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,51 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT
Assert.Equal(12505, overflowMetricPoint.GetHistogramSum());
}
}

[Theory]
[InlineData(MetricReaderTemporalityPreference.Delta)]
[InlineData(MetricReaderTemporalityPreference.Cumulative)]
public void NonFiniteHistogramMeasurementsDoNotConsumeCardinality(MetricReaderTemporalityPreference temporalityPreference)
{
var exportedItems = new List<Metric>();

var meter = new Meter(Utils.GetCurrentMethodName());
Comment thread
martincostello marked this conversation as resolved.
Resolved
var histogram = meter.CreateHistogram<double>("TestHistogram");

using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddView(histogram.Name, new ExplicitBucketHistogramConfiguration { CardinalityLimit = 3 })
.AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference)
.Build();

histogram.Record(double.NaN, new KeyValuePair<string, object?>("Key", 1));
histogram.Record(double.PositiveInfinity, new KeyValuePair<string, object?>("Key", 2));
histogram.Record(double.NegativeInfinity, new KeyValuePair<string, object?>("Key", 3));

histogram.Record(42, new KeyValuePair<string, object?>("Key", 4));
histogram.Record(42, new KeyValuePair<string, object?>("Key", 5));
histogram.Record(42, new KeyValuePair<string, object?>("Key", 6));

meterProvider.ForceFlush();

Assert.Single(exportedItems);
var metric = exportedItems[0];

var metricPoints = new List<MetricPoint>();
foreach (ref readonly var mp in metric.GetMetricPoints())
{
metricPoints.Add(mp);
}

// The non-finite measurements never occupied a MetricPoint, so no overflow occurred.
Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow");

var taggedMetricPoints = metricPoints.Where(mp => mp.Tags.Count != 0).ToList();
Assert.Equal(3, taggedMetricPoints.Count);
foreach (var mp in taggedMetricPoints)
{
Assert.Equal(1, mp.GetHistogramCount());
Assert.Equal(42, mp.GetHistogramSum());
}
}
}
3 changes: 1 addition & 2 deletions test/OpenTelemetry.Tests/Metrics/MetricTestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ internal static class MetricTestData
public static TheoryData<double[], HistogramConfiguration, double, double> ValidHistogramMinMax => new()
{
{ [-10.0, 0.0, 1.0, 9.0, 10.0, 11.0, 19.0], new HistogramConfiguration(), -10.0, 19.0 },
{ [double.NegativeInfinity], new HistogramConfiguration(), double.NegativeInfinity, double.NegativeInfinity },
{ [double.NegativeInfinity, 0.0, double.PositiveInfinity], new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity },
{ [double.NegativeInfinity, 0.0, double.PositiveInfinity], new HistogramConfiguration(), 0.0, 0.0 },
Comment thread
martincostello marked this conversation as resolved.
Resolved
{ [1.0], new HistogramConfiguration(), 1.0, 1.0 },
{ [5.0, 100.0, 4.0, 101.0, -2.0, 97.0], new ExplicitBucketHistogramConfiguration { Boundaries = [10.0, 20.0] }, -2.0, 101.0 },
{ [5.0, 100.0, 4.0, 101.0, -2.0, 97.0], new Base2ExponentialBucketHistogramConfiguration(), 4.0, 101.0 },
Expand Down
Loading