From a53d01fc53d015bb1d558491a19c85c0d28c7e3b Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 15:56:47 -0800 Subject: [PATCH 1/4] Support exemplars with exponential histograms. --- src/OpenTelemetry/CHANGELOG.md | 4 ++ src/OpenTelemetry/Metrics/MetricPoint.cs | 78 +++++++++++++++--------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 5cfbd35be90..2326b91c6bc 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -50,6 +50,10 @@ metrics exporters which support exemplars. ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) +* **Experimental (pre-release builds only):** Added support for exemplars when + using Base2 Exponential Bucket Histogram Aggregation via the View API. + ([#XXXX](https://github.com/open-telemetry/opentelemetry-dotnet/pull/XXXX)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 8e76e83778c..d9a3eef3a98 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -97,6 +97,10 @@ internal MetricPoint( { this.mpComponents = new MetricPointOptionalComponents(); this.mpComponents.Base2ExponentialBucketHistogram = new Base2ExponentialBucketHistogram(exponentialHistogramMaxSize, exponentialHistogramMaxScale); + if (isExemplarEnabled && reservoir == null) + { + reservoir = new SimpleFixedSizeExemplarReservoir(Math.Min(20, exponentialHistogramMaxSize)); + } } else { @@ -558,13 +562,13 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags, i)); - } + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, i)); } this.mpComponents.ReleaseLock(); @@ -1403,26 +1409,24 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan(number, tags, i)); - } - histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); } + if (reportExemplar && isSampled) + { + Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, i)); + } + this.mpComponents.ReleaseLock(); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) { if (number < 0) { @@ -1442,12 +1446,20 @@ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan(number, tags)); + } + this.mpComponents.ReleaseLock(); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) { if (number < 0) { @@ -1470,6 +1482,16 @@ private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySp histogram.RunningMax = Math.Max(histogram.RunningMax, number); } + if (reportExemplar && isSampled) + { + Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags)); + } + this.mpComponents.ReleaseLock(); } From 77b96f784c3dc7862c8d1147d69300d188d05cfe Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 22:34:05 -0800 Subject: [PATCH 2/4] Improve test coverage. --- .../Metrics/MetricExemplarTests.cs | 564 ++++++++++++++++-- .../Metrics/MetricTestsBase.cs | 2 +- 2 files changed, 505 insertions(+), 61 deletions(-) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index 885e1e725fd..b828d9aee74 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -23,17 +23,24 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var counter = meter.CreateCounter("testCounter"); + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView( - "testCounter", - new MetricStreamConfiguration + .AddView(i => + { + if (i.Name.StartsWith("testCounter")) { - ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), - }) + return new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + + return null; + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; @@ -42,17 +49,14 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) var measurementValues = GenerateRandomValues(2, false, null); foreach (var value in measurementValues) { - counter.Add(value.Value); + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); + ValidateFirstPhase("testCounterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testCounterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); exportedItems.Clear(); @@ -64,55 +68,206 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - counter.Add(value.Value); + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); + ValidateSecondPhase("testCounterDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.DoubleValue); + ValidateSecondPhase("testCounterLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.LongValue); - if (temporality == MetricReaderTemporalityPreference.Cumulative) + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) { - // Current design: - // First collect we saw Exemplar A & B - // Second collect we saw Exemplar C but B remained in the reservoir - Assert.Equal(2, exemplars.Count); - secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1).Take(1)).ToArray(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, getExemplarValueFunc); } - else + + void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues, + Func getExemplarValueFunc) { - Assert.Single(exemplars); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + // Current design: + // First collect we saw Exemplar A & B + // Second collect we saw Exemplar C but B remained in the reservoir + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, getExemplarValueFunc); } + } - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsObservable(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + (double Value, bool ExpectTraceId)[] measurementValues = new (double Value, bool ExpectTraceId)[] + { + (18D, false), + (19D, false), + }; + + int measurementIndex = 0; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var gaugeDouble = meter.CreateObservableGauge("testGaugeDouble", () => measurementValues[measurementIndex].Value); + var gaugeLong = meter.CreateObservableGauge("testGaugeLong", () => (long)measurementValues[measurementIndex].Value); + var counterDouble = meter.CreateObservableCounter("counterDouble", () => measurementValues[measurementIndex].Value); + var counterLong = meter.CreateObservableCounter("counterLong", () => (long)measurementValues[measurementIndex].Value); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + if (i.Name.StartsWith("testGauge")) + { + return new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + + return null; + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + ValidateFirstPhase("counterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("counterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + exportedItems.Clear(); + + measurementIndex++; + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateSecondPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Take(1), getExemplarValueFunc); + } + + static void ValidateSecondPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + // Note: Gauges are only observed when collection happens. For + // Cumulative & Delta the behavior will be the same. We will record the + // single measurement each time as the only exemplar. + + Assert.Single(exemplars); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Skip(1), getExemplarValueFunc); + } } [Theory] [InlineData(MetricReaderTemporalityPreference.Cumulative)] [InlineData(MetricReaderTemporalityPreference.Delta)] - public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality) + public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference temporality) { DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var histogram = meter.CreateHistogram("testHistogram"); + var histogramWithBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithBucketsAndMinMaxDouble"); + var histogramWithBucketsDouble = meter.CreateHistogram("histogramWithBucketsDouble"); + var histogramWithBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithBucketsAndMinMaxLong"); + var histogramWithBucketsLong = meter.CreateHistogram("histogramWithBucketsLong"); var buckets = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView( - "testHistogram", - new ExplicitBucketHistogramConfiguration + .AddView(i => + { + if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + }; + } + else { - Boundaries = buckets, - }) + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + RecordMinMax = false, + }; + } + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; @@ -125,17 +280,18 @@ public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality .ToArray(); foreach (var value in measurementValues) { - histogram.Record(value.Value); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsLong", testStartTime, exportedItems, measurementValues); exportedItems.Clear(); @@ -147,28 +303,314 @@ public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - histogram.Record(value.Value); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); + ValidateScondPhase("histogramWithBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); - if (temporality == MetricReaderTemporalityPreference.Cumulative) + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) { - Assert.Equal(11, exemplars.Count); - secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1)).ToArray(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); } - else + + static void ValidateScondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) { - Assert.Single(exemplars); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(11, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); } + } - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var histogramWithoutBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxDouble"); + var histogramWithoutBucketsDouble = meter.CreateHistogram("histogramWithoutBucketsDouble"); + var histogramWithoutBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxLong"); + var histogramWithoutBucketsLong = meter.CreateHistogram("histogramWithoutBucketsLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + RecordMinMax = false, + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(2, false, null); + foreach (var value in measurementValues) + { + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } + + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var exponentialHistogramWithMinMaxDouble = meter.CreateHistogram("exponentialHistogramWithMinMaxDouble"); + var exponentialHistogramDouble = meter.CreateHistogram("exponentialHistogramDouble"); + var exponentialHistogramWithMinMaxLong = meter.CreateHistogram("exponentialHistogramWithMinMaxLong"); + var exponentialHistogramLong = meter.CreateHistogram("exponentialHistogramLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + if (i.Name.StartsWith("exponentialHistogramWithMinMax")) + { + return new Base2ExponentialBucketHistogramConfiguration(); + } + else + { + return new Base2ExponentialBucketHistogramConfiguration() + { + RecordMinMax = false, + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(20, false, null); + foreach (var value in measurementValues) + { + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("exponentialHistogramWithMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramWithMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("exponentialHistogramWithMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramWithMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } + + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(20, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(19)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } } [Fact] @@ -226,9 +668,9 @@ private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( var values = new (double, bool)[count]; for (int i = 0; i < count; i++) { - var nextValue = random.NextDouble(); - if (values.Any(m => m.Item1 == nextValue) - || previousValues?.Any(m => m.Value == nextValue) == true) + var nextValue = random.NextDouble() * 100_000; + if (values.Any(m => m.Item1 == nextValue || m.Item1 == (long)nextValue) + || previousValues?.Any(m => m.Value == nextValue || m.Value == (long)nextValue) == true) { i--; continue; @@ -244,7 +686,8 @@ private static void ValidateExemplars( IReadOnlyList exemplars, DateTimeOffset startTime, DateTimeOffset endTime, - (double Value, bool ExpectTraceId)[] measurementValues) + IEnumerable<(double Value, bool ExpectTraceId)> measurementValues, + Func getExemplarValueFunc) { int count = 0; @@ -253,7 +696,8 @@ private static void ValidateExemplars( Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); Assert.Equal(0, exemplar.FilteredTags.MaximumCount); - var measurement = measurementValues.FirstOrDefault(v => v.Value == exemplar.DoubleValue); + var measurement = measurementValues.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar) + || (long)v.Value == getExemplarValueFunc(exemplar)); Assert.NotEqual(default, measurement); if (measurement.ExpectTraceId) { @@ -269,6 +713,6 @@ private static void ValidateExemplars( count++; } - Assert.Equal(measurementValues.Length, count); + Assert.Equal(measurementValues.Count(), count); } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 6d18bed47de..11c90c8d899 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -159,7 +159,7 @@ public static int GetNumberOfMetricPoints(List metrics) return count; } - public static MetricPoint? GetFirstMetricPoint(List metrics) + public static MetricPoint? GetFirstMetricPoint(IEnumerable metrics) { foreach (var metric in metrics) { From 6a029b6cd68991c60444afe9aa6b252c6c04a788 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 22:41:09 -0800 Subject: [PATCH 3/4] CHANGELOG patch. --- src/OpenTelemetry/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 2326b91c6bc..755d94e0b3b 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -51,8 +51,9 @@ ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) * **Experimental (pre-release builds only):** Added support for exemplars when - using Base2 Exponential Bucket Histogram Aggregation via the View API. - ([#XXXX](https://github.com/open-telemetry/opentelemetry-dotnet/pull/XXXX)) + using Base2 Exponential Bucket Histogram Aggregation configured via the View + API. + ([#5396](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5396)) ## 1.7.0 From 5f97f420e733bce8d9708ba6846784e87c5ce083 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 28 Feb 2024 09:48:55 -0800 Subject: [PATCH 4/4] Code review. --- .../Metrics/MetricExemplarTests.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index b828d9aee74..e1dd5effe54 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -154,18 +154,6 @@ public void TestExemplarsObservable(MetricReaderTemporalityPreference temporalit using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView(i => - { - if (i.Name.StartsWith("testGauge")) - { - return new MetricStreamConfiguration - { - ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), - }; - } - - return null; - }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality;