diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 1e802f55fd1..47b6536a5e5 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -17,6 +17,20 @@ Notes](../../RELEASENOTES.md). `9.0.0-rc.1.24431.7`. ([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853)) +* Added support in metrics for histogram bucket boundaries set via the .NET 9 + [InstrumentAdvice<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.instrumentadvice-1) + API. + + Note: With this change explicit bucket histogram boundary resolution will + apply in the following order: + + 1. View API + 2. Advice API + 3. SDK defaults + + See [#5854](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5854) + for details. + ## 1.9.0 Released 2024-Jun-14 diff --git a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs index 8ac0e8e10cf..a9918fb6eaa 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs @@ -22,7 +22,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me this.ViewId = metricStreamConfiguration?.ViewId; this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}"; this.TagKeys = metricStreamConfiguration?.CopiedTagKeys; - this.HistogramBucketBounds = (metricStreamConfiguration as ExplicitBucketHistogramConfiguration)?.CopiedBoundaries; + this.HistogramBucketBounds = GetExplicitBucketHistogramBounds(instrument, metricStreamConfiguration); this.ExponentialHistogramMaxSize = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxSize ?? 0; this.ExponentialHistogramMaxScale = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxScale ?? 0; this.HistogramRecordMinMax = (metricStreamConfiguration as HistogramConfiguration)?.RecordMinMax ?? true; @@ -150,6 +150,52 @@ public bool Equals(MetricStreamIdentity other) public override readonly int GetHashCode() => this.hashCode; + private static double[]? GetExplicitBucketHistogramBounds(Instrument instrument, MetricStreamConfiguration? metricStreamConfiguration) + { + if (metricStreamConfiguration is ExplicitBucketHistogramConfiguration explicitBucketHistogramConfiguration + && explicitBucketHistogramConfiguration.CopiedBoundaries != null) + { + return explicitBucketHistogramConfiguration.CopiedBoundaries; + } + + return instrument switch + { + Histogram longHistogram => GetExplicitBucketHistogramBoundsFromAdvice(longHistogram), + Histogram intHistogram => GetExplicitBucketHistogramBoundsFromAdvice(intHistogram), + Histogram shortHistogram => GetExplicitBucketHistogramBoundsFromAdvice(shortHistogram), + Histogram byteHistogram => GetExplicitBucketHistogramBoundsFromAdvice(byteHistogram), + Histogram floatHistogram => GetExplicitBucketHistogramBoundsFromAdvice(floatHistogram), + Histogram doubleHistogram => GetExplicitBucketHistogramBoundsFromAdvice(doubleHistogram), + _ => null, + }; + } + + private static double[]? GetExplicitBucketHistogramBoundsFromAdvice(Histogram histogram) + where T : struct + { + var adviceExplicitBucketBoundaries = histogram.Advice?.HistogramBucketBoundaries; + if (adviceExplicitBucketBoundaries == null) + { + return null; + } + + if (typeof(T) == typeof(double)) + { + return ((IReadOnlyList)adviceExplicitBucketBoundaries).ToArray(); + } + else + { + double[] explicitBucketBoundaries = new double[adviceExplicitBucketBoundaries.Count]; + + for (int i = 0; i < adviceExplicitBucketBoundaries.Count; i++) + { + explicitBucketBoundaries[i] = Convert.ToDouble(adviceExplicitBucketBoundaries[i]); + } + + return explicitBucketBoundaries; + } + } + private static bool HistogramBoundsEqual(double[]? bounds1, double[]? bounds2) { if (ReferenceEquals(bounds1, bounds2)) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index c1a0fca281b..11c048512ef 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -570,6 +570,177 @@ public void ViewToProduceCustomHistogramBound() Assert.Equal(boundaries.Length + 1, actualCount); } + [Fact] + public void HistogramWithAdviceBoundaries_HandlesAllTypes() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + int counter = 0; + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder.AddMeter(meter.Name); + builder.AddInMemoryExporter(exportedItems); + }); + + // Test cases for different histogram types + var histograms = new Instrument[] + { + meter.CreateHistogram("longHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List() { 10, 20 } }), + meter.CreateHistogram("intHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List() { 10, 20 } }), + meter.CreateHistogram("shortHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List() { 10, 20 } }), + meter.CreateHistogram("floatHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List() { 10.0F, 20.0F } }), + meter.CreateHistogram("doubleHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List() { 10.0, 20.0 } }), + }; + + foreach (var histogram in histograms) + { + exportedItems.Clear(); + + if (histogram is Histogram longHistogram) + { + longHistogram.Record(-10); + longHistogram.Record(9); + longHistogram.Record(19); + } + else if (histogram is Histogram intHistogram) + { + intHistogram.Record(-10); + intHistogram.Record(9); + intHistogram.Record(19); + counter++; + } + else if (histogram is Histogram shortHistogram) + { + shortHistogram.Record(-10); + shortHistogram.Record(9); + shortHistogram.Record(19); + counter++; + } + else if (histogram is Histogram floatHistogram) + { + floatHistogram.Record(-10.0F); + floatHistogram.Record(9.0F); + floatHistogram.Record(19.0F); + counter++; + } + else if (histogram is Histogram doubleHistogram) + { + doubleHistogram.Record(-10.0); + doubleHistogram.Record(9.0); + doubleHistogram.Record(19.0); + counter++; + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + var metricCustom = exportedItems[counter]; + + List metricPointsCustom = new List(); + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); + } + + Assert.Single(metricPointsCustom); + var histogramPoint = metricPointsCustom[0]; + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(18, sum); + Assert.Equal(3, count); + + var index = 0; + var actualCount = 0; + long[] expectedBucketCounts = new long[] { 2, 1, 0 }; + + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(3, actualCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HistogramWithAdviceBoundariesSpecifiedTests(bool useViewToOverride) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + IReadOnlyList adviceBoundaries = new List() { 5, 10, 20 }; + double[] viewBoundaries = new double[] { 10, 20 }; + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder.AddMeter(meter.Name); + + if (useViewToOverride) + { + builder.AddView("MyHistogram", new ExplicitBucketHistogramConfiguration { Boundaries = viewBoundaries }); + } + + builder.AddInMemoryExporter(exportedItems); + }); + + var histogram = meter.CreateHistogram( + "MyHistogram", + unit: null, + description: null, + tags: null, + new() + { + HistogramBucketBoundaries = adviceBoundaries, + }); + + histogram.Record(-10); + histogram.Record(0); + histogram.Record(1); + histogram.Record(9); + histogram.Record(10); + histogram.Record(11); + histogram.Record(19); + histogram.Record(22); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metricCustom = exportedItems[0]; + + Assert.Equal("MyHistogram", metricCustom.Name); + + List metricPointsCustom = new List(); + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); + } + + Assert.Single(metricPointsCustom); + var histogramPoint = metricPointsCustom[0]; + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(62, sum); + Assert.Equal(8, count); + + var index = 0; + var actualCount = 0; + long[] expectedBucketCounts = useViewToOverride ? new long[] { 5, 2, 1 } : new long[] { 3, 2, 2, 1 }; + + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(useViewToOverride ? viewBoundaries.Length + 1 : adviceBoundaries.Count + 1, actualCount); + } + [Fact] public void ViewToProduceExponentialHistogram() {