Skip to content

Commit 805dd6b

Browse files
VanyaChealanwest
andauthored
Clean up infinite explicit bucket bounds to prevent duplicate histogram bucket output (#6290)
Co-authored-by: Alan West <[email protected]>
1 parent 6828791 commit 805dd6b

File tree

4 files changed

+111
-0
lines changed

4 files changed

+111
-0
lines changed

src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class HistogramBuckets
3333

3434
internal HistogramBuckets(double[]? explicitBounds)
3535
{
36+
explicitBounds = CleanUpInfinitiesFromExplicitBounds(explicitBounds);
3637
this.ExplicitBounds = explicitBounds;
3738
this.findHistogramBucketIndex = this.FindBucketIndexLinear;
3839
if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch)
@@ -158,6 +159,9 @@ internal void Snapshot(bool outputDelta)
158159
}
159160
}
160161

162+
private static double[]? CleanUpInfinitiesFromExplicitBounds(double[]? explicitBounds) => explicitBounds
163+
?.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b)).ToArray();
164+
161165
/// <summary>
162166
/// Enumerates the elements of a <see cref="HistogramBuckets"/>.
163167
/// </summary>

test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
IMetricsBuilder/IMetricsListener API added at the host level in .NET 8
2424
instead of the direct lower-level MeterListener API added in .NET 6. -->
2525
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\AggregatorTests.cs" Link="Includes\Metrics\AggregatorTests.cs" />
26+
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\HistogramBoundaryTestCase.cs" Link="Includes\Metrics\HistogramBoundaryTestCase.cs" />
2627
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\HostingMeterProviderBuilder.cs" Link="Includes\Metrics\HostingMeterProviderBuilder.cs" />
2728
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\KnownHistogramBuckets.cs" Link="Includes\Metrics\KnownHistogramBuckets.cs" />
2829
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Metrics\MetricApiTests.cs" Link="Includes\Metrics\MetricApiTests.cs" />

test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public AggregatorTests()
2121
this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024);
2222
}
2323

24+
public static TheoryData<HistogramBoundaryTestCase> HistogramInfinityBoundariesTestCases => HistogramBoundaryTestCase.HistogramInfinityBoundariesTestCases();
25+
2426
[Fact]
2527
public void HistogramDistributeToAllBucketsDefault()
2628
{
@@ -450,6 +452,40 @@ internal void ExponentialMaxScaleConfigWorks(int? maxScale)
450452
Assert.Equal(expectedScale, metricPoint.GetExponentialHistogramData().Scale);
451453
}
452454

455+
[Theory]
456+
[MemberData(nameof(HistogramBoundaryTestCase.HistogramInfinityBoundariesTestCases))]
457+
internal void HistogramBucketBoundariesTest(HistogramBoundaryTestCase boundaryTestCase)
458+
{
459+
// Arrange
460+
var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaryTestCase.InputBoundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale);
461+
var expectedTotalBuckets = boundaryTestCase.ExpectedBucketCounts.Length;
462+
463+
// Act
464+
foreach (var value in boundaryTestCase.InputValues)
465+
{
466+
histogramPoint.Update(value);
467+
}
468+
469+
histogramPoint.TakeSnapshot(true);
470+
471+
// Assert
472+
var count = histogramPoint.GetHistogramCount();
473+
Assert.Equal(boundaryTestCase.InputValues.Length, count);
474+
475+
int bucketIndex = 0;
476+
int actualBucketCount = 0;
477+
478+
foreach (var histogramBucket in histogramPoint.GetHistogramBuckets())
479+
{
480+
Assert.Equal(boundaryTestCase.ExpectedBucketCounts[bucketIndex], histogramBucket.BucketCount);
481+
Assert.Equal(boundaryTestCase.ExpectedBucketBounds[bucketIndex], histogramBucket.ExplicitBound);
482+
bucketIndex++;
483+
actualBucketCount++;
484+
}
485+
486+
Assert.Equal(expectedTotalBuckets, actualBucketCount);
487+
}
488+
453489
private static void HistogramSnapshotThread(object? obj)
454490
{
455491
var args = obj as ThreadArguments;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Xunit;
5+
6+
namespace OpenTelemetry.Metrics.Tests;
7+
8+
#pragma warning disable CA1819 // Properties should not return arrays
9+
#pragma warning disable CA1515 // Consider making public types internal
10+
public class HistogramBoundaryTestCase(
11+
#pragma warning restore CA1515 // Consider making public types internal
12+
string testName,
13+
double[] inputBoundaries,
14+
double[] inputValues,
15+
long[] expectedBucketCounts,
16+
double[] expectedBucketBounds)
17+
{
18+
public double[] InputBoundaries { get; } = inputBoundaries;
19+
20+
public double[] InputValues { get; } = inputValues;
21+
22+
public long[] ExpectedBucketCounts { get; } = expectedBucketCounts;
23+
24+
public double[] ExpectedBucketBounds { get; } = expectedBucketBounds;
25+
26+
public string TestName { get; set; } = testName;
27+
28+
public static TheoryData<HistogramBoundaryTestCase> HistogramInfinityBoundariesTestCases()
29+
{
30+
var data = new TheoryData<HistogramBoundaryTestCase>
31+
{
32+
new(
33+
testName: "Custom boundaries with no infinity in explicit boundaries",
34+
inputBoundaries: [0, 10],
35+
inputValues: [-10, 0, 5, 10, 100],
36+
expectedBucketCounts: [2, 2, 1],
37+
expectedBucketBounds: [0, 10, double.PositiveInfinity]),
38+
39+
new(
40+
testName: "Custom boundaries with positive infinity",
41+
inputBoundaries: [0, double.PositiveInfinity],
42+
inputValues: [-10, 0, 10, 100],
43+
expectedBucketCounts: [2, 2],
44+
expectedBucketBounds: [0, double.PositiveInfinity]),
45+
46+
new(
47+
testName: "Custom boundaries with negative infinity",
48+
inputBoundaries: [double.NegativeInfinity, 0, 10],
49+
inputValues: [-100, -10, 0, 5, 10, 100],
50+
expectedBucketCounts: [3, 2, 1],
51+
expectedBucketBounds: [0, 10, double.PositiveInfinity]),
52+
53+
new(
54+
testName: "Custom boundaries with both infinities",
55+
inputBoundaries: [double.NegativeInfinity, 0, 10, double.PositiveInfinity],
56+
inputValues: [-100, -10, 0, 5, 10, 100],
57+
expectedBucketCounts: [3, 2, 1],
58+
expectedBucketBounds: [0, 10, double.PositiveInfinity]),
59+
60+
new(
61+
testName: "Custom boundaries with infinities only",
62+
inputBoundaries: [double.NegativeInfinity, double.PositiveInfinity],
63+
inputValues: [-10, 0, 10],
64+
expectedBucketCounts: [3],
65+
expectedBucketBounds: [double.PositiveInfinity]),
66+
};
67+
68+
return data;
69+
}
70+
}

0 commit comments

Comments
 (0)