diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index f862133e31f..d5d71920bd0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -30,6 +30,11 @@ as it is mandated by the specification. ([#5316](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5268)) +* **Experimental (pre-release builds only):** Add support in + `OtlpMetricExporter` for emitting exemplars supplied on Counters, Gauges, and + ExponentialHistograms. + ([#5397](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5397)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 53276e617e0..fb9266cb613 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -158,6 +158,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsInt = metricPoint.GetSumLong(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } + } + sum.DataPoints.Add(dataPoint); } @@ -185,6 +195,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsDouble = metricPoint.GetSumDouble(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } + sum.DataPoints.Add(dataPoint); } @@ -206,6 +226,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsInt = metricPoint.GetGaugeLastValueLong(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } + } + gauge.DataPoints.Add(dataPoint); } @@ -227,6 +257,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } + gauge.DataPoints.Add(dataPoint); } @@ -320,7 +360,14 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) dataPoint.Positive.BucketCounts.Add((ulong)bucketCount); } - // TODO: exemplars. + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } histogram.DataPoints.Add(dataPoint); } @@ -333,29 +380,7 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) return otlpMetric; } - private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) - { - foreach (var tag in tags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } - - private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) - { - foreach (var tag in meterTags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } - - private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + internal static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) where T : struct { var otlpExemplar = new OtlpMetrics.Exemplar @@ -399,4 +424,26 @@ private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exempl return otlpExemplar; } + + private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + { + foreach (var tag in tags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } + } + } + + private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) + { + foreach (var tag in meterTags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } + } + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 85713222ea6..45479f92ae9 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -1,8 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Diagnostics.Metrics; using System.Reflection; +using Google.Protobuf; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -18,6 +20,12 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests { + private static readonly KeyValuePair[] KeyValues = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", 123), + }; + [Fact] public void TestAddOtlpExporter_SetsCorrectMetricReaderDefaults() { @@ -216,14 +224,17 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) [Theory] [InlineData("test_gauge", null, null, 123L, null)] [InlineData("test_gauge", null, null, null, 123.45)] + [InlineData("test_gauge", null, null, 123L, null, true)] + [InlineData("test_gauge", null, null, null, 123.45, true)] [InlineData("test_gauge", "description", "unit", 123L, null)] - public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue) + public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics) .Build(); @@ -277,29 +288,35 @@ public void TestGaugeToOtlpMetric(string name, string description, string unit, Assert.Empty(dataPoint.Attributes); - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateCounter(name, unit, description); @@ -366,31 +383,37 @@ public void TestCounterToOtlpMetric(string name, string description, string unit Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_counter", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateUpDownCounter(name, unit, description); @@ -457,24 +480,30 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -485,7 +514,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -587,31 +616,41 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (enableExemplars) + { + VerifyExemplars(null, 0, enableExemplars, d => d.Exemplars.Skip(1).FirstOrDefault(), dataPoint); + } } [Theory] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -690,7 +729,7 @@ public void TestHistogramToOtlpMetric(string name, string description, string un Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] @@ -737,14 +776,155 @@ public void TestTemporalityPreferenceConfiguration(string configValue, MetricRea Assert.Equal(expectedTemporality, temporality); } - private static IEnumerable> ToAttributes(object[] keysValues) + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) { - var keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray(); - var values = keysValues?.Where((_, index) => index % 2 != 0).ToArray(); + ActivitySource activitySource = null; + Activity activity = null; + TracerProvider tracerProvider = null; + + using var meter = new Meter(Utils.GetCurrentMethodName()); + + var exportedItems = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + return !enableTagFiltering + ? null + : new MetricStreamConfiguration + { + TagKeys = Array.Empty(), + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + if (enableTracing) + { + activitySource = new ActivitySource(Utils.GetCurrentMethodName()); + tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySource.Name) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + activity = activitySource.StartActivity("testActivity"); + } + + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); + + counterDouble.Add(1.18D, new KeyValuePair("key1", "value1")); + counterLong.Add(18L, new KeyValuePair("key1", "value1")); + + meterProvider.ForceFlush(); + + var counterDoubleMetric = exportedItems.FirstOrDefault(m => m.Name == counterDouble.Name); + var counterLongMetric = exportedItems.FirstOrDefault(m => m.Name == counterLong.Name); - for (var i = 0; keys != null && i < keys.Length; ++i) + Assert.NotNull(counterDoubleMetric); + Assert.NotNull(counterLongMetric); + + AssertExemplars(1.18D, counterDoubleMetric); + AssertExemplars(18L, counterLongMetric); + + activity?.Dispose(); + tracerProvider?.Dispose(); + activitySource?.Dispose(); + + void AssertExemplars(T value, Metric metric) + where T : struct + { + var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); + Assert.True(metricPointEnumerator.MoveNext()); + + ref readonly var metricPoint = ref metricPointEnumerator.Current; + + var result = metricPoint.TryGetExemplars(out var exemplars); + Assert.True(result); + + var exemplarEnumerator = exemplars.Value.GetEnumerator(); + Assert.True(exemplarEnumerator.MoveNext()); + + ref readonly var exemplar = ref exemplarEnumerator.Current; + + var otlpExemplar = MetricItemExtensions.ToOtlpExemplar(value, in exemplar); + Assert.NotNull(otlpExemplar); + + Assert.NotEqual(default, otlpExemplar.TimeUnixNano); + if (!enableTracing) + { + Assert.Equal(ByteString.Empty, otlpExemplar.TraceId); + Assert.Equal(ByteString.Empty, otlpExemplar.SpanId); + } + else + { + byte[] traceIdBytes = new byte[16]; + activity.TraceId.CopyTo(traceIdBytes); + + byte[] spanIdBytes = new byte[8]; + activity.SpanId.CopyTo(spanIdBytes); + + Assert.Equal(ByteString.CopyFrom(traceIdBytes), otlpExemplar.TraceId); + Assert.Equal(ByteString.CopyFrom(spanIdBytes), otlpExemplar.SpanId); + } + + if (typeof(T) == typeof(long)) + { + Assert.Equal((long)(object)value, exemplar.LongValue); + } + else if (typeof(T) == typeof(double)) + { + Assert.Equal((double)(object)value, exemplar.DoubleValue); + } + else + { + Debug.Fail("Unexpected type"); + } + + if (!enableTagFiltering) + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.False(tagEnumerator.MoveNext()); + } + else + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.True(tagEnumerator.MoveNext()); + + var tag = tagEnumerator.Current; + Assert.Equal("key1", tag.Key); + Assert.Equal("value1", tag.Value); + } + } + } + + private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) + { + var exemplar = getExemplarFunc(state); + + if (enableExemplars) + { + Assert.NotNull(exemplar); + Assert.NotEqual(default, exemplar.TimeUnixNano); + if (longValue.HasValue) + { + Assert.Equal(longValue.Value, exemplar.AsInt); + } + else + { + Assert.Equal(doubleValue.Value, exemplar.AsDouble); + } + } + else { - yield return new KeyValuePair(keys[i].ToString(), values[i]); + Assert.Null(exemplar); } } }