diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index b3a52180514..31d83d93f41 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -278,6 +278,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A49299 src\Shared\TagAndValueTransformer.cs = src\Shared\TagAndValueTransformer.cs src\Shared\TagTransformer.cs = src\Shared\TagTransformer.cs src\Shared\TagTransformerJsonHelper.cs = src\Shared\TagTransformerJsonHelper.cs + src\Shared\ThreadSafeRandom.cs = src\Shared\ThreadSafeRandom.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DiagnosticSourceInstrumentation", "DiagnosticSourceInstrumentation", "{28F3EC79-660C-4659-8B73-F90DC1173316}" diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 4899f0d923e..4d7b777d12e 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -188,30 +188,44 @@ public override ExportResult Export(in Batch batch) } var exemplarString = new StringBuilder(); - foreach (var exemplar in metricPoint.GetExemplars()) + if (metricPoint.TryGetExemplars(out var exemplars)) { - if (exemplar.Timestamp != default) + foreach (ref readonly var exemplar in exemplars) { - exemplarString.Append("Value: "); - exemplarString.Append(exemplar.DoubleValue); - exemplarString.Append(" Timestamp: "); + exemplarString.Append("Timestamp: "); exemplarString.Append(exemplar.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); - exemplarString.Append(" TraceId: "); - exemplarString.Append(exemplar.TraceId); - exemplarString.Append(" SpanId: "); - exemplarString.Append(exemplar.SpanId); + if (metricType.IsDouble()) + { + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.DoubleValue); + } + else if (metricType.IsLong()) + { + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.LongValue); + } - if (exemplar.FilteredTags != null && exemplar.FilteredTags.Count > 0) + if (exemplar.TraceId.HasValue) { - exemplarString.Append(" Filtered Tags : "); + exemplarString.Append(" TraceId: "); + exemplarString.Append(exemplar.TraceId.Value.ToHexString()); + exemplarString.Append(" SpanId: "); + exemplarString.Append(exemplar.SpanId.Value.ToHexString()); + } - foreach (var tag in exemplar.FilteredTags) + bool appendedTagString = false; + foreach (var tag in exemplar.FilteredTags) + { + if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) + if (!appendedTagString) { - exemplarString.Append(result); - exemplarString.Append(' '); + exemplarString.Append(" Filtered Tags : "); + appendedTagString = true; } + + exemplarString.Append(result); + exemplarString.Append(' '); } } @@ -239,7 +253,7 @@ public override ExportResult Export(in Batch batch) { msg.AppendLine(); msg.AppendLine("Exemplars"); - msg.Append(exemplarString.ToString()); + msg.Append(exemplarString); } this.WriteLine(msg.ToString()); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index d93105e3dfb..b3db8c18027 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Diagnostics; using System.Runtime.CompilerServices; using Google.Protobuf; using Google.Protobuf.Collections; @@ -267,37 +268,12 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) } } - var exemplars = metricPoint.GetExemplars(); - foreach (var examplar in exemplars) + if (metricPoint.TryGetExemplars(out var exemplars)) { - if (examplar.Timestamp != default) + foreach (ref readonly var exemplar in exemplars) { - byte[] traceIdBytes = new byte[16]; - examplar.TraceId?.CopyTo(traceIdBytes); - - byte[] spanIdBytes = new byte[8]; - examplar.SpanId?.CopyTo(spanIdBytes); - - var otlpExemplar = new OtlpMetrics.Exemplar - { - TimeUnixNano = (ulong)examplar.Timestamp.ToUnixTimeNanoseconds(), - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - AsDouble = examplar.DoubleValue, - }; - - if (examplar.FilteredTags != null) - { - foreach (var tag in examplar.FilteredTags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - otlpExemplar.FilteredAttributes.Add(result); - } - } - } - - dataPoint.Exemplars.Add(otlpExemplar); + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); } } @@ -379,51 +355,48 @@ private static void AddScopeAttributes(IEnumerable> } } - /* - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) + private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + where T : struct { - var otlpExemplar = new OtlpMetrics.Exemplar(); - - if (exemplar.Value is double doubleValue) + var otlpExemplar = new OtlpMetrics.Exemplar { - otlpExemplar.AsDouble = doubleValue; - } - else if (exemplar.Value is long longValue) + TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(), + }; + + if (exemplar.TraceId.HasValue) { - otlpExemplar.AsInt = longValue; + byte[] traceIdBytes = new byte[16]; + exemplar.TraceId.Value.CopyTo(traceIdBytes); + + byte[] spanIdBytes = new byte[8]; + exemplar.SpanId.Value.CopyTo(spanIdBytes); + + otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); } - else + + if (typeof(T) == typeof(long)) { - // TODO: Determine how we want to handle exceptions here. - // Do we want to just skip this exemplar and move on? - // Should we skip recording the whole metric? - throw new ArgumentException(); + otlpExemplar.AsInt = (long)(object)value; } - - otlpExemplar.TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(); - - // TODO: Do the TagEnumerationState thing. - foreach (var tag in exemplar.FilteredTags) + else if (typeof(T) == typeof(double)) { - otlpExemplar.FilteredAttributes.Add(tag.ToOtlpAttribute()); + otlpExemplar.AsDouble = (double)(object)value; } - - if (exemplar.TraceId != default) + else { - byte[] traceIdBytes = new byte[16]; - exemplar.TraceId.CopyTo(traceIdBytes); - otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + Debug.Fail("Unexpected type"); + otlpExemplar.AsDouble = Convert.ToDouble(value); } - if (exemplar.SpanId != default) + foreach (var tag in exemplar.FilteredTags) { - byte[] spanIdBytes = new byte[8]; - exemplar.SpanId.CopyTo(spanIdBytes); - otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + otlpExemplar.FilteredAttributes.Add(result); + } } return otlpExemplar; } - */ } diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index 16832495101..c6122728cd3 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -12,16 +12,38 @@ OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void OpenTelemetry.Metrics.Exemplar OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double OpenTelemetry.Metrics.Exemplar.Exemplar() -> void +OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.Metrics.Exemplar.LongValue.get -> long OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! +OpenTelemetry.Metrics.ExemplarMeasurement +OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void +OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> +OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T +OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection? exemplars) -> bool OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Current.get -> OpenTelemetry.Metrics.Exemplar +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.GetEnumerator() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.MaximumCount.get -> int +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.ReadOnlyExemplarCollection() -> void OpenTelemetry.Metrics.TraceBasedExemplarFilter OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Enumerator() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.ReadOnlyFilteredTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.MaximumCount.get -> int +OpenTelemetry.ReadOnlyFilteredTagCollection.ReadOnlyFilteredTagCollection() -> void static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! @@ -38,7 +60,6 @@ static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTele static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>? override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 6260596c78c..a3da443e0ca 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -42,6 +42,11 @@ [IMetricsListener](https://learn.microsoft.com/dotNet/api/microsoft.extensions.diagnostics.metrics.imetricslistener). ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) +* **Experimental (pre-release builds only):** `Exemplar` and `ExemplarReservoir` + APIs have been updated to match the OpenTelemetry Specification and to achieve + better throughput under load. + ([#5364](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5364)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index a5d603589ad..c97995b92d9 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -11,11 +11,14 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { + internal readonly HashSet? TagKeysInteresting; internal readonly bool OutputDelta; internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; internal readonly int CardinalityLimit; internal readonly bool EmitOverflowAttribute; internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; + internal readonly ExemplarFilteringHelper ExemplarFilter; + internal readonly Func? ExemplarReservoirFactory; internal long DroppedMeasurements = 0; private static readonly string MetricPointCapHitFixMessage = "Consider opting in for the experimental SDK feature to emit all the throttled metrics under the overflow attribute by setting env variable OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE = true. You could also modify instrumentation to reduce the number of unique key/value pair combinations. Or use Views to drop unwanted tags. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."; @@ -24,7 +27,6 @@ internal sealed class AggregatorStore private readonly object lockZeroTags = new(); private readonly object lockOverflowTag = new(); - private readonly HashSet? tagKeysInteresting; private readonly int tagsKeysInterestingCount; // This holds the reclaimed MetricPoints that are available for reuse. @@ -43,7 +45,6 @@ internal sealed class AggregatorStore private readonly int exponentialHistogramMaxScale; private readonly UpdateLongDelegate updateLongCallback; private readonly UpdateDoubleDelegate updateDoubleCallback; - private readonly ExemplarFilter exemplarFilter; private readonly Func[], int, int> lookupAggregatorStore; private int metricPointIndex = 0; @@ -59,7 +60,8 @@ internal AggregatorStore( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null) + ExemplarFilter? exemplarFilter = null, + Func? exemplarReservoirFactory = null) { this.name = metricStreamIdentity.InstrumentName; this.CardinalityLimit = cardinalityLimit; @@ -73,7 +75,7 @@ internal AggregatorStore( this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize; this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; this.StartTimeExclusive = DateTimeOffset.UtcNow; - this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + this.ExemplarReservoirFactory = exemplarReservoirFactory; if (metricStreamIdentity.TagKeys == null) { this.updateLongCallback = this.UpdateLong; @@ -84,7 +86,7 @@ internal AggregatorStore( this.updateLongCallback = this.UpdateLongCustomTags; this.updateDoubleCallback = this.UpdateDoubleCustomTags; var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); - this.tagKeysInteresting = hs; + this.TagKeysInteresting = hs; this.tagsKeysInterestingCount = hs.Count; } @@ -125,6 +127,8 @@ internal AggregatorStore( { this.lookupAggregatorStore = this.LookupAggregatorStore; } + + this.ExemplarFilter = new(exemplarFilter ?? DefaultExemplarFilter); } private delegate void UpdateLongDelegate(long value, ReadOnlySpan> tags); @@ -138,11 +142,7 @@ internal AggregatorStore( internal double[] HistogramBounds => this.histogramBounds; internal bool IsExemplarEnabled() - { - // Using this filter to indicate On/Off - // instead of another separate flag. - return this.exemplarFilter is not AlwaysOffExemplarFilter; - } + => this.ExemplarFilter.Enabled; internal void Update(long value, ReadOnlySpan> tags) { @@ -186,14 +186,7 @@ internal void SnapshotDelta(int indexSnapshot) continue; } - if (this.IsExemplarEnabled()) - { - metricPoint.TakeSnapshotWithExemplar(outputDelta: true); - } - else - { - metricPoint.TakeSnapshot(outputDelta: true); - } + metricPoint.TakeSnapshot(outputDelta: true); this.currentMetricPointBatch[this.batchSize] = i; this.batchSize++; @@ -211,14 +204,7 @@ internal void SnapshotDeltaWithMetricPointReclaim() ref var metricPointWithNoTags = ref this.metricPoints[0]; if (metricPointWithNoTags.MetricPointStatus != MetricPointStatus.NoCollectPending) { - if (this.IsExemplarEnabled()) - { - metricPointWithNoTags.TakeSnapshotWithExemplar(outputDelta: true); - } - else - { - metricPointWithNoTags.TakeSnapshot(outputDelta: true); - } + metricPointWithNoTags.TakeSnapshot(outputDelta: true); this.currentMetricPointBatch[this.batchSize] = 0; this.batchSize++; @@ -234,14 +220,7 @@ internal void SnapshotDeltaWithMetricPointReclaim() ref var metricPointForOverflow = ref this.metricPoints[1]; if (metricPointForOverflow.MetricPointStatus != MetricPointStatus.NoCollectPending) { - if (this.IsExemplarEnabled()) - { - metricPointForOverflow.TakeSnapshotWithExemplar(outputDelta: true); - } - else - { - metricPointForOverflow.TakeSnapshot(outputDelta: true); - } + metricPointForOverflow.TakeSnapshot(outputDelta: true); this.currentMetricPointBatch[this.batchSize] = 1; this.batchSize++; @@ -299,14 +278,7 @@ internal void SnapshotDeltaWithMetricPointReclaim() continue; } - if (this.IsExemplarEnabled()) - { - metricPoint.TakeSnapshotWithExemplar(outputDelta: true); - } - else - { - metricPoint.TakeSnapshot(outputDelta: true); - } + metricPoint.TakeSnapshot(outputDelta: true); this.currentMetricPointBatch[this.batchSize] = i; this.batchSize++; @@ -328,14 +300,7 @@ internal void SnapshotCumulative(int indexSnapshot) continue; } - if (this.IsExemplarEnabled()) - { - metricPoint.TakeSnapshotWithExemplar(outputDelta: false); - } - else - { - metricPoint.TakeSnapshot(outputDelta: false); - } + metricPoint.TakeSnapshot(outputDelta: false); this.currentMetricPointBatch[this.batchSize] = i; this.batchSize++; @@ -377,11 +342,24 @@ private void InitializeZeroTagPointIfNotInitialized() if (this.OutputDelta) { var lookupData = new LookupData(0, Tags.EmptyTags, Tags.EmptyTags); - this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + this.metricPoints[0] = new MetricPoint( + this, + this.aggType, + tagKeysAndValues: null, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale, + lookupData); } else { - this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + this.metricPoints[0] = new MetricPoint( + this, + this.aggType, + tagKeysAndValues: null, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale); } this.zeroTagMetricPointInitialized = true; @@ -405,11 +383,24 @@ private void InitializeOverflowTagPointIfNotInitialized() if (this.OutputDelta) { var lookupData = new LookupData(1, tags, tags); - this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + this.metricPoints[1] = new MetricPoint( + this, + this.aggType, + keyValuePairs, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale, + lookupData); } else { - this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + this.metricPoints[1] = new MetricPoint( + this, + this.aggType, + keyValuePairs, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale); } this.overflowTagMetricPointInitialized = true; @@ -478,7 +469,13 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu } ref var metricPoint = ref this.metricPoints[aggregatorIndex]; - metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + metricPoint = new MetricPoint( + this, + this.aggType, + sortedTags.KeyValuePairs, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale); // Add to dictionary *after* initializing MetricPoint // as other threads can start writing to the @@ -527,7 +524,13 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu } ref var metricPoint = ref this.metricPoints[aggregatorIndex]; - metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + metricPoint = new MetricPoint( + this, + this.aggType, + givenTags.KeyValuePairs, + this.histogramBounds, + this.exponentialHistogramMaxSize, + this.exponentialHistogramMaxScale); // Add to dictionary *after* initializing MetricPoint // as other threads can start writing to the @@ -603,7 +606,14 @@ private int LookupAggregatorStoreForDeltaWithReclaim(KeyValuePair> if (this.EmitOverflowAttribute) { this.InitializeOverflowTagPointIfNotInitialized(); - this.metricPoints[1].Update(value); + this.metricPoints[1].Update(value, tags: default); return; } else @@ -945,16 +976,7 @@ private void UpdateLong(long value, ReadOnlySpan> } } - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) - { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); - } - else - { - this.metricPoints[index].Update(value); - } + this.metricPoints[index].Update(value, tags: default); } catch (Exception) { @@ -975,7 +997,7 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan ShouldSampleLong; + public readonly ShouldSampleFunc ShouldSampleDouble; + + public ExemplarFilteringHelper(ExemplarFilter exemplarFilter) + { + Debug.Assert(exemplarFilter != null, "exemplarFilter was null"); + + if (exemplarFilter is AlwaysOffExemplarFilter) + { + this.Enabled = false; + this.EarlySampleDecision = false; + this.ShouldSampleLong = static (_, _) => false; + this.ShouldSampleDouble = static (_, _) => false; + } + else if (exemplarFilter is AlwaysOnExemplarFilter) + { + this.Enabled = true; + this.EarlySampleDecision = true; + this.ShouldSampleLong = static (_, _) => true; + this.ShouldSampleDouble = static (_, _) => true; + } + else + { + this.Enabled = true; + this.EarlySampleDecision = null; + this.ShouldSampleLong = exemplarFilter!.ShouldSample; + this.ShouldSampleDouble = exemplarFilter.ShouldSample; + } + } + + internal delegate bool ShouldSampleFunc(T value, ReadOnlySpan> tags); + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs index 4f10f3e2527..41e4e32bdcf 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -8,92 +8,36 @@ namespace OpenTelemetry.Metrics; /// /// The AlignedHistogramBucketExemplarReservoir implementation. /// -internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir +/// +/// Specification: . +/// +internal sealed class AlignedHistogramBucketExemplarReservoir : FixedSizeExemplarReservoir { - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; - - public AlignedHistogramBucketExemplarReservoir(int length) - { - this.runningExemplars = new Exemplar[length + 1]; - this.tempExemplars = new Exemplar[length + 1]; - } - - public override void Offer(long value, ReadOnlySpan> tags, int index = default) - { - this.OfferAtBoundary(value, tags, index); - } - - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public AlignedHistogramBucketExemplarReservoir(int numberOfBuckets) + : base(numberOfBuckets) { - this.OfferAtBoundary(value, tags, index); } - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) + public override void Offer(in ExemplarMeasurement measurement) { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags!.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - } - } + Debug.Assert( + measurement.ExplicitBucketHistogramBucketIndex != -1, + "ExplicitBucketHistogramBucketIndex was -1"); - return this.tempExemplars; + this.UpdateExemplar( + measurement.ExplicitBucketHistogramBucketIndex, + in measurement); } - private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index) + public override void Offer(in ExemplarMeasurement measurement) { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } - - if (exemplar.FilteredTags == null) - { - exemplar.FilteredTags = new List>(tags.Length); - } - else - { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } + Debug.Assert( + measurement.ExplicitBucketHistogramBucketIndex != -1, + "ExplicitBucketHistogramBucketIndex was -1"); - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); - } + this.UpdateExemplar( + measurement.ExplicitBucketHistogramBucketIndex, + in measurement); } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index d635a1a4ad4..0f8ce59b942 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,13 +1,15 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; #if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif +using System.Runtime.CompilerServices; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER using OpenTelemetry.Internal; #endif -using System.Diagnostics; - namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES @@ -20,39 +22,175 @@ namespace OpenTelemetry.Metrics; #endif public #else -/// -/// Represents an Exemplar data. -/// -#pragma warning disable SA1623 // The property's documentation summary text should begin with: `Gets or sets` internal #endif struct Exemplar { + internal HashSet? ViewDefinedTagKeys; + + private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, Array.Empty>(), count: 0); + private int tagCount; + private KeyValuePair[]? tagStorage; + private MetricPointValueStorage valueStorage; + private int isCriticalSectionOccupied; + /// /// Gets the timestamp. /// - public DateTimeOffset Timestamp { get; internal set; } + public DateTimeOffset Timestamp { get; private set; } /// /// Gets the TraceId. /// - public ActivityTraceId? TraceId { get; internal set; } + public ActivityTraceId? TraceId { get; private set; } /// /// Gets the SpanId. /// - public ActivitySpanId? SpanId { get; internal set; } + public ActivitySpanId? SpanId { get; private set; } - // TODO: Leverage MetricPointValueStorage - // and allow double/long instead of double only. + /// + /// Gets the long value. + /// + public long LongValue + { + readonly get => this.valueStorage.AsLong; + private set => this.valueStorage.AsLong = value; + } /// /// Gets the double value. /// - public double DoubleValue { get; internal set; } + public double DoubleValue + { + readonly get => this.valueStorage.AsDouble; + private set => this.valueStorage.AsDouble = value; + } /// - /// Gets the FilteredTags (i.e any tags that were dropped during aggregation). + /// Gets the filtered tags. /// - public List>? FilteredTags { get; internal set; } + /// + /// Note: represents the set of tags which were + /// supplied at measurement but dropped due to filtering configured by a + /// view (). If view tag + /// filtering is not configured will be empty. + /// + public readonly ReadOnlyFilteredTagCollection FilteredTags + { + get + { + if (this.tagCount == 0) + { + return Empty; + } + else + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); + + return new(this.ViewDefinedTagKeys, this.tagStorage!, this.tagCount); + } + } + } + + internal void Update(in ExemplarMeasurement measurement) + where T : struct + { + if (Interlocked.Exchange(ref this.isCriticalSectionOccupied, 1) != 0) + { + // Some other thread is already writing, abort. + return; + } + + this.Timestamp = DateTimeOffset.UtcNow; + + if (typeof(T) == typeof(long)) + { + this.LongValue = (long)(object)measurement.Value; + } + else if (typeof(T) == typeof(double)) + { + this.DoubleValue = (double)(object)measurement.Value; + } + else + { + Debug.Fail("Invalid value type"); + this.DoubleValue = Convert.ToDouble(measurement.Value); + } + + var currentActivity = Activity.Current; + if (currentActivity != null) + { + this.TraceId = currentActivity.TraceId; + this.SpanId = currentActivity.SpanId; + } + else + { + this.TraceId = default; + this.SpanId = default; + } + + this.StoreRawTags(measurement.Tags); + + Interlocked.Exchange(ref this.isCriticalSectionOccupied, 0); + } + + internal void Reset() + { + this.Timestamp = default; + } + + internal readonly bool IsUpdated() + { + if (Interlocked.CompareExchange(ref Unsafe.AsRef(in this.isCriticalSectionOccupied), 0, 0) != 0) + { + this.WaitForUpdateToCompleteRare(); + return true; + } + + return this.Timestamp != default; + } + + internal readonly void Copy(ref Exemplar destination) + { + destination.Timestamp = this.Timestamp; + destination.TraceId = this.TraceId; + destination.SpanId = this.SpanId; + destination.valueStorage = this.valueStorage; + destination.ViewDefinedTagKeys = this.ViewDefinedTagKeys; + destination.tagCount = this.tagCount; + if (destination.tagCount > 0) + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); + + destination.tagStorage = new KeyValuePair[destination.tagCount]; + Array.Copy(this.tagStorage!, 0, destination.tagStorage, 0, destination.tagCount); + } + } + + private void StoreRawTags(ReadOnlySpan> tags) + { + this.tagCount = tags.Length; + if (tags.Length == 0) + { + return; + } + + if (this.tagStorage == null || this.tagStorage.Length < this.tagCount) + { + this.tagStorage = new KeyValuePair[this.tagCount]; + } + + tags.CopyTo(this.tagStorage); + } + + private readonly void WaitForUpdateToCompleteRare() + { + var spinWait = default(SpinWait); + do + { + spinWait.SpinOnce(); + } + while (Interlocked.CompareExchange(ref Unsafe.AsRef(in this.isCriticalSectionOccupied), 0, 0) != 0); + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs new file mode 100644 index 00000000000..3dcd4a8d9df --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Represents an Exemplar measurement. +/// +/// +/// Measurement type. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly ref struct ExemplarMeasurement + where T : struct +{ + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = -1; + } + + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags, + int explicitBucketHistogramIndex) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = explicitBucketHistogramIndex; + } + + /// + /// Gets the measurement value. + /// + public T Value { get; } + + /// + /// Gets the measurement tags. + /// + /// + /// Note: represents the full set of tags supplied at + /// measurement regardless of any filtering configured by a view (). + /// + public ReadOnlySpan> Tags { get; } + + internal int ExplicitBucketHistogramBucketIndex { get; } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index d5b944ff656..b8cf161e30b 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -8,33 +8,33 @@ namespace OpenTelemetry.Metrics; /// internal abstract class ExemplarReservoir { + /// + /// Gets a value indicating whether or not the should reset its state when performing + /// collection. + /// + public bool ResetOnCollect { get; private set; } + /// /// Offers measurement to the reservoir. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(long value, ReadOnlySpan> tags, int index = default); + /// . + public abstract void Offer(in ExemplarMeasurement measurement); /// /// Offers measurement to the reservoir. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(double value, ReadOnlySpan> tags, int index = default); + /// . + public abstract void Offer(in ExemplarMeasurement measurement); /// /// Collects all the exemplars accumulated by the Reservoir. /// - /// The actual tags that are part of the metric. Exemplars are - /// only expected to contain any filtered tags, so this will allow the reservoir - /// to prepare the filtered tags from all the tags it is given by doing the - /// equivalent of filtered tags = all tags - actual tags. - /// - /// Flag to indicate if the reservoir should be reset after this call. - /// Array of Exemplars. - public abstract Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset); + /// . + public abstract ReadOnlyExemplarCollection Collect(); + + internal virtual void Initialize(AggregatorStore aggregatorStore) + { + this.ResetOnCollect = aggregatorStore.OutputDelta; + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs new file mode 100644 index 00000000000..bf2c12a5963 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -0,0 +1,146 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#endif +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +internal abstract class FixedSizeExemplarReservoir : ExemplarReservoir +{ + private static readonly Exemplar[] NeverMatchExemplarBuffer = new Exemplar[0]; + private readonly Exemplar[] bufferA; + private readonly Exemplar[] bufferB; + private Exemplar[]? activeBuffer; + + protected FixedSizeExemplarReservoir(int capacity) + { + Guard.ThrowIfOutOfRange(capacity, min: 1); + + this.bufferA = new Exemplar[capacity]; + this.bufferB = new Exemplar[capacity]; + this.activeBuffer = this.bufferA; + this.Capacity = capacity; + } + + internal int Capacity { get; } + + /// + /// Collects all the exemplars accumulated by the Reservoir. + /// + /// . + public sealed override ReadOnlyExemplarCollection Collect() + { + var activeBuffer = Interlocked.Exchange(ref this.activeBuffer, null); + + Debug.Assert(activeBuffer != null, "activeBuffer was null"); + + var inactiveBuffer = activeBuffer == this.bufferA + ? this.bufferB + : this.bufferA; + + if (this.ResetOnCollect) + { + for (int i = 0; i < inactiveBuffer.Length; i++) + { + inactiveBuffer[i].Reset(); + } + + this.OnReset(); + } + else + { +#if NET6_0_OR_GREATER + var length = this.Capacity; + ref var inactive = ref MemoryMarshal.GetArrayDataReference(inactiveBuffer); + ref var active = ref MemoryMarshal.GetArrayDataReference(activeBuffer); + do + { + if (active.IsUpdated()) + { + active.Copy(ref inactive); + } + + inactive = ref Unsafe.Add(ref inactive, 1); + active = ref Unsafe.Add(ref active, 1); + } + while (--length > 0); +#else + for (int i = 0; i < activeBuffer!.Length; i++) + { + ref var active = ref activeBuffer[i]; + if (active.IsUpdated()) + { + active.Copy(ref inactiveBuffer[i]); + } + } +#endif + } + + Interlocked.Exchange(ref this.activeBuffer, inactiveBuffer); + + return new(activeBuffer!); + } + + internal sealed override void Initialize(AggregatorStore aggregatorStore) + { + var viewDefinedTagKeys = aggregatorStore.TagKeysInteresting; + +#if NET6_0_OR_GREATER + var length = this.bufferA.Length; + ref var a = ref MemoryMarshal.GetArrayDataReference(this.bufferA); + ref var b = ref MemoryMarshal.GetArrayDataReference(this.bufferB); + do + { + a.ViewDefinedTagKeys = viewDefinedTagKeys; + b.ViewDefinedTagKeys = viewDefinedTagKeys; + a = ref Unsafe.Add(ref a, 1); + b = ref Unsafe.Add(ref b, 1); + } + while (--length > 0); +#else + for (int i = 0; + i < this.bufferA.Length && i < this.bufferB.Length; + i++) + { + this.bufferA[i].ViewDefinedTagKeys = viewDefinedTagKeys; + this.bufferB[i].ViewDefinedTagKeys = viewDefinedTagKeys; + } +#endif + + base.Initialize(aggregatorStore); + } + + protected virtual void OnReset() + { + } + + protected void UpdateExemplar(int exemplarIndex, in ExemplarMeasurement measurement) + where T : struct + { + var activeBuffer = Interlocked.CompareExchange(ref this.activeBuffer, null, NeverMatchExemplarBuffer) + ?? this.AcquireActiveBufferRare(); + + activeBuffer[exemplarIndex].Update(in measurement); + } + + private Exemplar[] AcquireActiveBufferRare() + { + // Note: We reach here if performing a write while racing with collect. + + Exemplar[]? activeBuffer; + + var spinWait = default(SpinWait); + do + { + spinWait.SpinOnce(); + } + while ((activeBuffer = Interlocked.CompareExchange(ref this.activeBuffer, null, NeverMatchExemplarBuffer)) == null); + + return activeBuffer; + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs new file mode 100644 index 00000000000..6d58aed7d38 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of s. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyExemplarCollection +{ + private readonly Exemplar[] exemplars; + + internal ReadOnlyExemplarCollection(Exemplar[] exemplars) + { + Debug.Assert(exemplars != null, "exemplars was null"); + + this.exemplars = exemplars!; + } + + /// + /// Gets the maximum number of s in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// which s in the collection received updates. + /// + public int MaximumCount => this.exemplars.Length; + + /// + /// Returns an enumerator that iterates through the s. + /// + /// . + public Enumerator GetEnumerator() + => new(this.exemplars); + + internal ReadOnlyExemplarCollection Copy() + { + var exemplarCopies = new Exemplar[this.exemplars.Length]; + + int i = 0; + foreach (ref readonly var exemplar in this) + { + exemplar.Copy(ref exemplarCopies[i++]); + } + + return new ReadOnlyExemplarCollection(exemplarCopies); + } + + internal IReadOnlyList ToReadOnlyList() + { + var list = new List(this.MaximumCount); + + foreach (var item in this) + { + list.Add(item); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator + { + private readonly Exemplar[] exemplars; + private int index; + + internal Enumerator(Exemplar[] exemplars) + { + this.exemplars = exemplars; + this.index = -1; + } + + /// + /// Gets the at the current position of the enumerator. + /// + public readonly ref readonly Exemplar Current + => ref this.exemplars[this.index]; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + while (true) + { + var index = ++this.index; + if (index < this.exemplars.Length) + { + if (!this.exemplars[index].IsUpdated()) + { + continue; + } + + return true; + } + + break; + } + + return false; + } + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs index 5324e7067d2..e9f65412448 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -1,140 +1,62 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics; /// /// The SimpleFixedSizeExemplarReservoir implementation. /// -internal sealed class SimpleFixedSizeExemplarReservoir : ExemplarReservoir +/// +/// Specification: . +/// +internal sealed class SimpleFixedSizeExemplarReservoir : FixedSizeExemplarReservoir { - private readonly int poolSize; - private readonly Random random; - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; + private int measurementsSeen; - private long measurementsSeen; - - public SimpleFixedSizeExemplarReservoir(int poolSize) + public SimpleFixedSizeExemplarReservoir() + : this(Environment.ProcessorCount) { - this.poolSize = poolSize; - this.runningExemplars = new Exemplar[poolSize]; - this.tempExemplars = new Exemplar[poolSize]; - this.measurementsSeen = 0; - this.random = new Random(); } - public override void Offer(long value, ReadOnlySpan> tags, int index = default) + public SimpleFixedSizeExemplarReservoir(int capacity) + : base(capacity) { - this.Offer(value, tags); } - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.Offer(value, tags); + this.Offer(in measurement); } - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) + public override void Offer(in ExemplarMeasurement measurement) { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags!.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - } - } - - // Reset internal state irrespective of temporality. - // This ensures incoming measurements have fair chance - // of making it to the reservoir. - this.measurementsSeen = 0; - - return this.tempExemplars; + this.Offer(in measurement); } - private void Offer(double value, ReadOnlySpan> tags) + protected override void OnReset() { - if (this.measurementsSeen < this.poolSize) - { - ref var exemplar = ref this.runningExemplars[this.measurementsSeen]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); - } - else - { - // TODO: RandomNext64 is only available in .NET 6 or newer. - int upperBound = 0; - unchecked - { - upperBound = (int)this.measurementsSeen; - } - - var index = this.random.Next(0, upperBound); - if (index < this.poolSize) - { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); - } - } - - this.measurementsSeen++; + Interlocked.Exchange(ref this.measurementsSeen, 0); } - private void StoreTags(ref Exemplar exemplar, ReadOnlySpan> tags) + private void Offer(in ExemplarMeasurement measurement) + where T : struct { - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } + var currentMeasurement = Interlocked.Increment(ref this.measurementsSeen) - 1; - if (exemplar.FilteredTags == null) + if (currentMeasurement < this.Capacity) { - exemplar.FilteredTags = new List>(tags.Length); + this.UpdateExemplar(currentMeasurement, in measurement); } else { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } - - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); + int index = ThreadSafeRandom.Next(0, currentMeasurement); + if (index < this.Capacity) + { + this.UpdateExemplar(index, in measurement); + } } } } diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index cfd6b4e3463..ca82f8d4eaa 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -49,7 +49,8 @@ internal Metric( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null) + ExemplarFilter? exemplarFilter = null, + Func? exemplarReservoirFactory = null) { this.InstrumentIdentity = instrumentIdentity; @@ -155,7 +156,15 @@ internal Metric( throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - this.AggregatorStore = new AggregatorStore(instrumentIdentity, aggType, temporality, cardinalityLimit, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); + this.AggregatorStore = new AggregatorStore( + instrumentIdentity, + aggType, + temporality, + cardinalityLimit, + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints, + exemplarFilter, + exemplarReservoirFactory); this.Temporality = temporality; } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 38ee0b18c86..00a5369f5c3 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -24,8 +25,6 @@ public struct MetricPoint // that its using is already reclaimed, this helps avoid sorting of the tags for adding a new Dictionary entry. internal LookupData? LookupData; - private const int DefaultSimpleReservoirPoolSize = 1; - private readonly AggregatorStore aggregatorStore; private readonly AggregationType aggType; @@ -62,15 +61,27 @@ internal MetricPoint( this.ReferenceCount = 1; this.LookupData = lookupData; - ExemplarReservoir? reservoir = null; + var isExemplarEnabled = aggregatorStore!.IsExemplarEnabled(); + + ExemplarReservoir? reservoir; + try + { + reservoir = aggregatorStore.ExemplarReservoirFactory?.Invoke(); + } + catch + { + // todo: Log + reservoir = null; + } + if (this.aggType == AggregationType.HistogramWithBuckets || this.aggType == AggregationType.HistogramWithMinMaxBuckets) { this.mpComponents = new MetricPointOptionalComponents(); this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); - if (aggregatorStore!.IsExemplarEnabled()) + if (isExemplarEnabled) { - reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds!.Length); + reservoir ??= new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds!.Length + 1); } } else if (this.aggType == AggregationType.Histogram || @@ -90,9 +101,9 @@ internal MetricPoint( this.mpComponents = null; } - if (aggregatorStore!.IsExemplarEnabled() && reservoir == null) + if (isExemplarEnabled && reservoir == null) { - reservoir = new SimpleFixedSizeExemplarReservoir(DefaultSimpleReservoirPoolSize); + reservoir = new SimpleFixedSizeExemplarReservoir(); } if (reservoir != null) @@ -102,6 +113,8 @@ internal MetricPoint( this.mpComponents = new MetricPointOptionalComponents(); } + reservoir.Initialize(aggregatorStore); + this.mpComponents.ExemplarReservoir = reservoir; } @@ -346,21 +359,18 @@ public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) /// Gets the exemplars associated with the metric point. /// /// - /// . + /// . + /// if exemplars exist; otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] public #else - /// - /// Gets the exemplars associated with the metric point. - /// - /// . [MethodImpl(MethodImplOptions.AggressiveInlining)] internal #endif - readonly Exemplar[] GetExemplars() + readonly bool TryGetExemplars([NotNullWhen(true)] out ReadOnlyExemplarCollection? exemplars) { - // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents?.Exemplars ?? Array.Empty(); + exemplars = this.mpComponents?.Exemplars; + return exemplars.HasValue; } internal readonly MetricPoint Copy() @@ -370,210 +380,114 @@ internal readonly MetricPoint Copy() return copy; } - internal void Update(long number) + internal void Update(long number, ReadOnlySpan> tags) { - switch (this.aggType) - { - case AggregationType.LongSumIncomingDelta: - { - Interlocked.Add(ref this.runningValue.AsLong, number); - break; - } - - case AggregationType.LongSumIncomingCumulative: - { - Interlocked.Exchange(ref this.runningValue.AsLong, number); - break; - } + this.Update(number, out var explicitBucketHistogramBucketIndex); - case AggregationType.LongGauge: - { - Interlocked.Exchange(ref this.runningValue.AsLong, number); - break; - } - - case AggregationType.Histogram: - { - this.UpdateHistogram((double)number); - break; - } + var exemplarFilter = this.aggregatorStore.ExemplarFilter; + if (exemplarFilter.EarlySampleDecision ?? exemplarFilter.ShouldSampleLong(number, tags)) + { + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); - case AggregationType.HistogramWithMinMax: - { - this.UpdateHistogramWithMinMax((double)number); - break; - } + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, explicitBucketHistogramBucketIndex)); + } - case AggregationType.HistogramWithBuckets: - { - this.UpdateHistogramWithBuckets((double)number); - break; - } + this.CompleteUpdate(); + } - case AggregationType.HistogramWithMinMaxBuckets: - { - this.UpdateHistogramWithBucketsAndMinMax((double)number); - break; - } + internal void Update(double number, ReadOnlySpan> tags) + { + this.Update(number, out var explicitBucketHistogramBucketIndex); - case AggregationType.Base2ExponentialHistogram: - { - this.UpdateBase2ExponentialHistogram((double)number); - break; - } + var exemplarFilter = this.aggregatorStore.ExemplarFilter; + if (exemplarFilter.EarlySampleDecision ?? exemplarFilter.ShouldSampleDouble(number, tags)) + { + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); - case AggregationType.Base2ExponentialHistogramWithMinMax: - { - this.UpdateBase2ExponentialHistogramWithMinMax((double)number); - break; - } + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, explicitBucketHistogramBucketIndex)); } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; + this.CompleteUpdate(); + } - if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) + internal void TakeSnapshot(bool outputDelta) + { + this.Snapshot(outputDelta); + + var exemplarReservoir = this.mpComponents?.ExemplarReservoir; + if (exemplarReservoir != null) { - Interlocked.Decrement(ref this.ReferenceCount); + this.mpComponents!.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); } } - internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Update(long number, out int explicitBucketHistogramBucketIndex) { - Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); - switch (this.aggType) { case AggregationType.LongSumIncomingDelta: { - this.mpComponents!.AcquireLock(); - - unchecked - { - this.runningValue.AsLong += number; - } - - if (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(number, tags); - } - - this.mpComponents.ReleaseLock(); - + Interlocked.Add(ref this.runningValue.AsLong, number); break; } case AggregationType.LongSumIncomingCumulative: { - this.mpComponents!.AcquireLock(); - - this.runningValue.AsLong = number; - - if (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(number, tags); - } - - this.mpComponents.ReleaseLock(); - + Interlocked.Exchange(ref this.runningValue.AsLong, number); break; } case AggregationType.LongGauge: { - this.mpComponents!.AcquireLock(); - - this.runningValue.AsLong = number; - - if (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(number, tags); - } - - this.mpComponents.ReleaseLock(); - + Interlocked.Exchange(ref this.runningValue.AsLong, number); break; } case AggregationType.Histogram: { - this.UpdateHistogram((double)number, tags, reportExemplar: true, isSampled); + this.UpdateHistogram((double)number); break; } case AggregationType.HistogramWithMinMax: { - this.UpdateHistogramWithMinMax((double)number, tags, reportExemplar: true, isSampled); + this.UpdateHistogramWithMinMax((double)number); break; } case AggregationType.HistogramWithBuckets: { - this.UpdateHistogramWithBuckets((double)number, tags, reportExemplar: true, isSampled); - break; + explicitBucketHistogramBucketIndex = this.UpdateHistogramWithBuckets((double)number); + return; } case AggregationType.HistogramWithMinMaxBuckets: { - this.UpdateHistogramWithBucketsAndMinMax((double)number, tags, reportExemplar: true, isSampled); - break; + explicitBucketHistogramBucketIndex = this.UpdateHistogramWithBucketsAndMinMax((double)number); + return; } case AggregationType.Base2ExponentialHistogram: { - this.UpdateBase2ExponentialHistogram((double)number, tags, true); + this.UpdateBase2ExponentialHistogram((double)number); break; } case AggregationType.Base2ExponentialHistogramWithMinMax: { - this.UpdateBase2ExponentialHistogramWithMinMax((double)number, tags, true); + this.UpdateBase2ExponentialHistogramWithMinMax((double)number); break; } } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; - - if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) - { - Interlocked.Decrement(ref this.ReferenceCount); - } + explicitBucketHistogramBucketIndex = -1; } - internal void Update(double number) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Update(double number, out int explicitBucketHistogramBucketIndex) { switch (this.aggType) { @@ -609,14 +523,14 @@ internal void Update(double number) case AggregationType.HistogramWithBuckets: { - this.UpdateHistogramWithBuckets(number); - break; + explicitBucketHistogramBucketIndex = this.UpdateHistogramWithBuckets(number); + return; } case AggregationType.HistogramWithMinMaxBuckets: { - this.UpdateHistogramWithBucketsAndMinMax(number); - break; + explicitBucketHistogramBucketIndex = this.UpdateHistogramWithBucketsAndMinMax(number); + return; } case AggregationType.Base2ExponentialHistogram: @@ -632,137 +546,144 @@ internal void Update(double number) } } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; + explicitBucketHistogramBucketIndex = -1; + } - if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) + private void UpdateHistogram(double number) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets!; + + this.mpComponents.AcquireLock(); + + unchecked { - Interlocked.Decrement(ref this.ReferenceCount); + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; } + + this.mpComponents.ReleaseLock(); } - internal void UpdateWithExemplar(double number, ReadOnlySpan> tags, bool isSampled) + private void UpdateHistogramWithMinMax(double number) { - Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - switch (this.aggType) + var histogramBuckets = this.mpComponents!.HistogramBuckets!; + + this.mpComponents.AcquireLock(); + + unchecked { - case AggregationType.DoubleSumIncomingDelta: - { - this.mpComponents!.AcquireLock(); + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); + histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); + } - unchecked - { - this.runningValue.AsDouble += number; - } + this.mpComponents.ReleaseLock(); + } - if (isSampled) - { - Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + private int UpdateHistogramWithBuckets(double number) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets 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(number, tags); - } + var histogramBuckets = this.mpComponents!.HistogramBuckets; - this.mpComponents.ReleaseLock(); + int i = histogramBuckets!.FindBucketIndex(number); - break; - } + this.mpComponents.AcquireLock(); - case AggregationType.DoubleSumIncomingCumulative: - { - this.mpComponents!.AcquireLock(); + unchecked + { + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.BucketCounts[i].RunningValue++; + } - unchecked - { - this.runningValue.AsDouble = number; - } + this.mpComponents.ReleaseLock(); - if (isSampled) - { - Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + return 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(number, tags); - } + private int UpdateHistogramWithBucketsAndMinMax(double number) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "histogramBuckets was null"); - this.mpComponents.ReleaseLock(); + var histogramBuckets = this.mpComponents!.HistogramBuckets; - break; - } + int i = histogramBuckets!.FindBucketIndex(number); - case AggregationType.DoubleGauge: - { - this.mpComponents!.AcquireLock(); + this.mpComponents.AcquireLock(); - unchecked - { - this.runningValue.AsDouble = number; - } + unchecked + { + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.BucketCounts[i].RunningValue++; - if (isSampled) - { - Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); + histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); + } - // TODO: Need to ensure that the lock is always released. - // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. - this.mpComponents.ExemplarReservoir!.Offer(number, tags); - } + this.mpComponents.ReleaseLock(); - this.mpComponents.ReleaseLock(); + return i; + } - break; - } + private void UpdateBase2ExponentialHistogram(double number) + { + if (number < 0) + { + return; + } - case AggregationType.Histogram: - { - this.UpdateHistogram(number, tags, reportExemplar: true, isSampled); - break; - } + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - case AggregationType.HistogramWithMinMax: - { - this.UpdateHistogramWithMinMax(number, tags, reportExemplar: true, isSampled); - break; - } + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; - case AggregationType.HistogramWithBuckets: - { - this.UpdateHistogramWithBuckets(number, tags, reportExemplar: true, isSampled); - break; - } + this.mpComponents.AcquireLock(); - case AggregationType.HistogramWithMinMaxBuckets: - { - this.UpdateHistogramWithBucketsAndMinMax(number, tags, reportExemplar: true, isSampled); - break; - } + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + histogram.Record(number); + } - case AggregationType.Base2ExponentialHistogram: - { - this.UpdateBase2ExponentialHistogram(number, tags, true); - break; - } + this.mpComponents.ReleaseLock(); + } - case AggregationType.Base2ExponentialHistogramWithMinMax: - { - this.UpdateBase2ExponentialHistogramWithMinMax(number, tags, true); - break; - } + private void UpdateBase2ExponentialHistogramWithMinMax(double number) + { + if (number < 0) + { + return; + } + + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; + + this.mpComponents.AcquireLock(); + + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + histogram.Record(number); + + histogram.RunningMin = Math.Min(histogram.RunningMin, number); + histogram.RunningMax = Math.Max(histogram.RunningMax, number); } + this.mpComponents.ReleaseLock(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CompleteUpdate() + { // There is a race with Snapshot: // Update() updates the value // Snapshot snapshots the value @@ -782,7 +703,7 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) - { - Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - - var histogramBuckets = this.mpComponents!.HistogramBuckets!; - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += 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(number, tags); - } - - this.mpComponents.ReleaseLock(); - } - - private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) - { - Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - - var histogramBuckets = this.mpComponents!.HistogramBuckets!; - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - 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(number, tags); - } - - this.mpComponents.ReleaseLock(); - } - - private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) - { - Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - - var histogramBuckets = this.mpComponents!.HistogramBuckets; - - int i = histogramBuckets!.FindBucketIndex(number); - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - histogramBuckets.BucketCounts[i].RunningValue++; - - 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(number, tags, i); - } - } - - this.mpComponents.ReleaseLock(); - } - - private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) - { - Debug.Assert(this.mpComponents?.HistogramBuckets != null, "histogramBuckets was null"); - - var histogramBuckets = this.mpComponents!.HistogramBuckets; - - int i = histogramBuckets!.FindBucketIndex(number); - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - histogramBuckets.BucketCounts[i].RunningValue++; - - 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(number, tags, i); - } - - histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); - histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); - } - - 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 - { - if (number < 0) - { - return; - } - - Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogram.RunningSum += number; - histogram.Record(number); - } - - 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 - { - if (number < 0) - { - return; - } - - Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; - - this.mpComponents.AcquireLock(); - - unchecked - { - this.runningValue.AsLong++; - histogram.RunningSum += number; - histogram.Record(number); - - histogram.RunningMin = Math.Min(histogram.RunningMin, number); - histogram.RunningMax = Math.Max(histogram.RunningMax, number); - } - - this.mpComponents.ReleaseLock(); - } - [MethodImpl(MethodImplOptions.NoInlining)] private readonly void ThrowNotSupportedMetricTypeException(string methodName) { diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index f028b2add56..84511b1b549 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -20,7 +20,7 @@ internal sealed class MetricPointOptionalComponents public ExemplarReservoir? ExemplarReservoir; - public Exemplar[]? Exemplars; + public ReadOnlyExemplarCollection? Exemplars; private int isCriticalSectionOccupied = 0; @@ -30,14 +30,9 @@ public MetricPointOptionalComponents Copy() { HistogramBuckets = this.HistogramBuckets?.Copy(), Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy(), + Exemplars = this.Exemplars?.Copy(), }; - if (this.Exemplars != null) - { - copy.Exemplars = new Exemplar[this.Exemplars.Length]; - Array.Copy(this.Exemplars, copy.Exemplars, this.Exemplars.Length); - } - return copy; } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 9f3b6fa10d2..6b78958e6a5 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -147,14 +147,14 @@ internal virtual List AddMetricWithViews(Instrument instrument, List? ExemplarReservoirFactory { get; set; } + internal string[]? CopiedTagKeys { get; private set; } internal int? ViewId { get; set; } diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj index 754627add78..d6765d63589 100644 --- a/src/OpenTelemetry/OpenTelemetry.csproj +++ b/src/OpenTelemetry/OpenTelemetry.csproj @@ -21,6 +21,7 @@ + diff --git a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs new file mode 100644 index 00000000000..d48e24e40e6 --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of tag key/value pairs which returns a filtered +/// subset of tags when enumerated. +/// +// Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to +// prevent accidental boxing. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyFilteredTagCollection +{ + private readonly HashSet? excludedKeys; + private readonly KeyValuePair[] tags; + private readonly int count; + + internal ReadOnlyFilteredTagCollection( + HashSet? excludedKeys, + KeyValuePair[] tags, + int count) + { + Debug.Assert(tags != null, "tags was null"); + Debug.Assert(count <= tags!.Length, "count was invalid"); + + this.excludedKeys = excludedKeys; + this.tags = tags; + this.count = count; + } + + /// + /// Gets the maximum number of tags in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// the filter. + /// + public int MaximumCount => this.count; + + /// + /// Returns an enumerator that iterates through the tags. + /// + /// . + public Enumerator GetEnumerator() => new(this); + + internal IReadOnlyList> ToReadOnlyList() + { + var list = new List>(this.MaximumCount); + + foreach (var item in this) + { + list.Add(item); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly ReadOnlyFilteredTagCollection source; + private int index; + + internal Enumerator(ReadOnlyFilteredTagCollection source) + { + this.source = source; + this.index = -1; + this.Current = default; + } + + /// + /// Gets the tag at the current position of the enumerator. + /// + public KeyValuePair Current { readonly get; private set; } + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + while (true) + { + int index = ++this.index; + if (index < this.source.MaximumCount) + { + var item = this.source.tags[index]; + + if (this.source.excludedKeys?.Contains(item.Key) == true) + { + continue; + } + + this.Current = item; + return true; + } + + break; + } + + return false; + } + } +} diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index 3c7dc59d770..423ccc1f1a6 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -47,7 +47,7 @@ internal Enumerator(ReadOnlyTagCollection source) /// /// Gets the tag at the current position of the enumerator. /// - public KeyValuePair Current { get; private set; } + public KeyValuePair Current { readonly get; private set; } /// /// Advances the enumerator to the next element of the 10, <=20 - histogramPoint.Update(11); - histogramPoint.Update(19); + histogramPoint.Update(11, tags: default); + histogramPoint.Update(19, tags: default); histogramPoint.TakeSnapshot(true); @@ -132,12 +132,12 @@ public void HistogramBinaryBucketTest() var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); // Act - histogramPoint.Update(-1); - histogramPoint.Update(boundaries[0]); - histogramPoint.Update(boundaries[boundaries.Length - 1]); + histogramPoint.Update(-1, tags: default); + histogramPoint.Update(boundaries[0], tags: default); + histogramPoint.Update(boundaries[boundaries.Length - 1], tags: default); for (var i = 0.5; i < boundaries.Length; i++) { - histogramPoint.Update(i); + histogramPoint.Update(i, tags: default); } histogramPoint.TakeSnapshot(true); @@ -164,13 +164,13 @@ public void HistogramWithOnlySumCount() var boundaries = Array.Empty(); var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - histogramPoint.Update(-10); - histogramPoint.Update(0); - histogramPoint.Update(1); - histogramPoint.Update(9); - histogramPoint.Update(10); - histogramPoint.Update(11); - histogramPoint.Update(19); + histogramPoint.Update(-10, tags: default); + histogramPoint.Update(0, tags: default); + histogramPoint.Update(1, tags: default); + histogramPoint.Update(9, tags: default); + histogramPoint.Update(10, tags: default); + histogramPoint.Update(11, tags: default); + histogramPoint.Update(19, tags: default); histogramPoint.TakeSnapshot(true); @@ -501,7 +501,7 @@ private static void HistogramUpdateThread(object obj) for (int i = 0; i < 10; ++i) { - args.HistogramPoint.Update(10); + args.HistogramPoint.Update(10, tags: default); } Interlocked.Increment(ref args.ThreadsFinishedAllUpdatesCount); diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index b5fd844f43c..ebd8afdb0bf 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -1,23 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + 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; - } [Theory] [InlineData(MetricReaderTemporalityPreference.Cumulative)] @@ -33,15 +28,21 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView( + "testCounter", + new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(2, false, null); foreach (var value in measurementValues) { - counter.Add(value); + counter.Add(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -49,14 +50,9 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - // TODO: Modify the test to better test cumulative. - // In cumulative, where SimpleFixedSizeExemplarReservoir's size is - // more than the count of new measurements, it is possible - // that the exemplar value is for a measurement that was recorded in the prior - // cycle. The current ValidateExemplars() does not handle this case. - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false); + var exemplars = GetExemplars(metricPoint.Value); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); exportedItems.Clear(); @@ -64,12 +60,11 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) { - var act = new Activity("test").Start(); - counter.Add(value); - act.Stop(); + using var act = new Activity("test").Start(); + counter.Add(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -77,12 +72,26 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) 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); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(3, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(measurementValues).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); } - [Fact] - public void TestExemplarsHistogram() + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality) { DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); @@ -90,18 +99,30 @@ public void TestExemplarsHistogram() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); + 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 + { + Boundaries = buckets, + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = buckets + /* 2000 is here to test overflow measurement */ + .Concat(new double[] { 2000 }) + .Select(b => (Value: b, ExpectTraceId: false)) + .ToArray(); foreach (var value in measurementValues) { - histogram.Record(value); + histogram.Record(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -110,7 +131,7 @@ public void TestExemplarsHistogram() 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); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); exportedItems.Clear(); @@ -118,11 +139,11 @@ public void TestExemplarsHistogram() Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray(); + foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - histogram.Record(value); + histogram.Record(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -130,8 +151,20 @@ public void TestExemplarsHistogram() 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); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(11, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(measurementValues).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); } [Fact] @@ -152,10 +185,14 @@ public void TestExemplarsFilterTags() metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(10, false, null); foreach (var value in measurementValues) { - histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1")); + histogram.Record( + value.Value, + new("key1", "value1"), + new("key2", "value1"), + new("key3", "value1")); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -164,36 +201,54 @@ public void TestExemplarsFilterTags() 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); + Assert.NotEqual(0, exemplar.FilteredTags.MaximumCount); + + var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); + + Assert.Contains(new("key2", "value1"), filteredTags); + Assert.Contains(new("key3", "value1"), filteredTags); } } - private static double[] GenerateRandomValues(int count) + private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( + int count, + bool expectTraceId, + (double Value, bool ExpectTraceId)[]? previousValues) { var random = new Random(); - var values = new double[count]; + var values = new (double, bool)[count]; for (int i = 0; i < count; i++) { - values[i] = random.NextDouble(); + var nextValue = random.NextDouble(); + if (values.Any(m => m.Item1 == nextValue) + || previousValues?.Any(m => m.Value == nextValue) == true) + { + i--; + continue; + } + + values[i] = (nextValue, expectTraceId); } return values; } - private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists) + private static void ValidateExemplars( + IReadOnlyList exemplars, + DateTimeOffset startTime, + DateTimeOffset endTime, + (double Value, bool ExpectTraceId)[] measurementValues) { - 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.Equal(0, exemplar.FilteredTags.MaximumCount); + + var measurement = measurementValues.FirstOrDefault(v => v.Value == exemplar.DoubleValue); + Assert.NotEqual(default, measurement); + if (measurement.ExpectTraceId) { Assert.NotEqual(default, exemplar.TraceId); Assert.NotEqual(default, exemplar.SpanId); diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 0e5c1e1e53f..6d18bed47de 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -233,9 +233,14 @@ public IDisposable BuildMeterProvider( #endif } - internal static Exemplar[] GetExemplars(MetricPoint mp) + internal static IReadOnlyList GetExemplars(MetricPoint mp) { - return mp.GetExemplars().Where(exemplar => exemplar.Timestamp != default).ToArray(); + if (mp.TryGetExemplars(out var exemplars)) + { + return exemplars.Value.ToReadOnlyList(); + } + + return Array.Empty(); } #if BUILDING_HOSTING_TESTS