diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
index 7a6c5164d07..a73abbb7f77 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
@@ -25,7 +25,6 @@
-
diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs
index 1eb976cc983..7f2732d3c0c 100644
--- a/src/OpenTelemetry/Metrics/AggregatorStore.cs
+++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs
@@ -374,7 +374,7 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using OpenTelemetry.Tests;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace OpenTelemetry.Metrics.Tests
+{
+ public class MetricExemplarTests : MetricTestsBase
+ {
+ private const int MaxTimeToAllowForFlush = 10000;
+ private readonly ITestOutputHelper output;
+
+ public MetricExemplarTests(ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ [Fact]
+ public void TestExemplarsCounter()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var counter = meter.CreateCounter("testCounter");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ counter.Add(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, false);
+
+ exportedItems.Clear();
+
+#if NETFRAMEWORK
+ Thread.Sleep(10); // Compensates for low resolution timing in netfx.
+#endif
+
+ measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ var act = new Activity("test").Start();
+ counter.Add(value);
+ act.Stop();
+ }
+
+ 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);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true);
+ }
+
+ [Fact]
+ public void TestExemplarsHistogram()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var histogram = meter.CreateHistogram("testHistogram");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ histogram.Record(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, false);
+
+ exportedItems.Clear();
+
+#if NETFRAMEWORK
+ Thread.Sleep(10); // Compensates for low resolution timing in netfx.
+#endif
+
+ measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ using var act = new Activity("test").Start();
+ histogram.Record(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);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true);
+ }
+
+ [Fact]
+ public void TestExemplarsFilterTags()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var histogram = meter.CreateHistogram("testHistogram");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } })
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1"));
+ }
+
+ 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);
+ Assert.NotNull(exemplars);
+ foreach (var exemplar in exemplars)
+ {
+ Assert.NotNull(exemplar.FilteredTags);
+ Assert.Contains(new("key2", "value1"), exemplar.FilteredTags);
+ Assert.Contains(new("key3", "value1"), exemplar.FilteredTags);
+ }
+ }
+
+ private static double[] GenerateRandomValues(int count)
+ {
+ var random = new Random();
+ var values = new double[count];
+ for (int i = 0; i < count; i++)
+ {
+ values[i] = random.NextDouble();
+ }
+
+ return values;
+ }
+
+ private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists)
+ {
+ Assert.NotNull(exemplars);
+ foreach (var exemplar in exemplars)
+ {
+ Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}");
+ Assert.Contains(exemplar.DoubleValue, measurementValues);
+ Assert.Null(exemplar.FilteredTags);
+ if (traceContextExists)
+ {
+ Assert.NotEqual(default, exemplar.TraceId);
+ Assert.NotEqual(default, exemplar.SpanId);
+ }
+ else
+ {
+ Assert.Equal(default, exemplar.TraceId);
+ Assert.Equal(default, exemplar.SpanId);
+ }
+ }
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
index f1d6b228d58..3b3312c3057 100644
--- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
+++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
@@ -122,4 +122,9 @@ public static void CheckTagsForNthMetricPoint(List metrics, List exemplar.Timestamp != default).ToArray();
+ }
}