From 42c593d8c2a99bc90afef1e555507747af7f4a98 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 10:49:44 -0800 Subject: [PATCH] [sdk-metrics] Exemplar spec improvements (#5386) --- .../ConsoleMetricExporter.cs | 44 ++++-- .../Implementation/MetricItemExtensions.cs | 93 +++++------- .../Experimental/PublicAPI.Unshipped.txt | 27 +++- src/OpenTelemetry/CHANGELOG.md | 8 + src/OpenTelemetry/Metrics/AggregatorStore.cs | 8 +- ...AlignedHistogramBucketExemplarReservoir.cs | 88 ++--------- .../Metrics/Exemplar/Exemplar.cs | 138 ++++++++++++++++-- .../Metrics/Exemplar/ExemplarMeasurement.cs | 62 ++++++++ .../Metrics/Exemplar/ExemplarReservoir.cs | 46 +++--- .../Exemplar/FixedSizeExemplarReservoir.cs | 91 ++++++++++++ .../Exemplar/ReadOnlyExemplarCollection.cs | 125 ++++++++++++++++ .../SimpleFixedSizeExemplarReservoir.cs | 120 +++------------ src/OpenTelemetry/Metrics/MetricPoint.cs | 65 +++++---- .../Metrics/MetricPointOptionalComponents.cs | 9 +- .../ReadOnlyFilteredTagCollection.cs | 120 +++++++++++++++ src/OpenTelemetry/ReadOnlyTagCollection.cs | 21 +-- .../Metrics/MetricExemplarTests.cs | 13 +- .../Metrics/MetricTestsBase.cs | 9 +- 18 files changed, 730 insertions(+), 357 deletions(-) create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs create mode 100644 src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 4899f0d923e..f3f4b0ba5f9 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 != default) { - exemplarString.Append(" Filtered Tags : "); + exemplarString.Append(" TraceId: "); + exemplarString.Append(exemplar.TraceId.ToHexString()); + exemplarString.Append(" SpanId: "); + exemplarString.Append(exemplar.SpanId.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(' '); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index d93105e3dfb..53276e617e0 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 != default) { - otlpExemplar.AsInt = longValue; + byte[] traceIdBytes = new byte[16]; + exemplar.TraceId.CopyTo(traceIdBytes); + + byte[] spanIdBytes = new byte[8]; + exemplar.SpanId.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..b530cdf98f6 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -12,16 +12,36 @@ OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void OpenTelemetry.Metrics.Exemplar OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? +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.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.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.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 +58,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..5cfbd35be90 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -42,6 +42,14 @@ [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):** The `Exemplar.FilteredTags` + property now returns a `ReadOnlyFilteredTagCollection` instance and the + `Exemplar.LongValue` property has been added. The `MetricPoint.GetExemplars` + method has been replaced by `MetricPoint.TryGetExemplars` which outputs a + `ReadOnlyExemplarCollection` instance. These are **breaking changes** for + metrics exporters which support exemplars. + ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index a5d603589ad..fa4cefc7221 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -11,6 +11,7 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { + internal readonly HashSet? TagKeysInteresting; internal readonly bool OutputDelta; internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; internal readonly int CardinalityLimit; @@ -24,7 +25,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. @@ -84,7 +84,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; } @@ -1122,9 +1122,9 @@ private int FindMetricAggregatorsCustomTag(ReadOnlySpan /// The AlignedHistogramBucketExemplarReservoir implementation. /// -internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir +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) + public AlignedHistogramBucketExemplarReservoir(int numberOfBuckets) + : base(numberOfBuckets + 1) { - this.OfferAtBoundary(value, tags, index); } - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.OfferAtBoundary(value, tags, index); + Debug.Fail("AlignedHistogramBucketExemplarReservoir shouldn't be used with long values"); } - 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; - } - } - - return this.tempExemplars; - } - - private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index) - { - 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..b3a862aa528 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,13 +1,12 @@ // 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 -using System.Diagnostics; - namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES @@ -20,39 +19,150 @@ 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; + /// /// Gets the timestamp. /// - public DateTimeOffset Timestamp { get; internal set; } + public DateTimeOffset Timestamp { readonly get; private set; } /// /// Gets the TraceId. /// - public ActivityTraceId? TraceId { get; internal set; } + public ActivityTraceId TraceId { readonly get; private set; } /// /// Gets the SpanId. /// - public ActivitySpanId? SpanId { get; internal set; } + public ActivitySpanId SpanId { readonly 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 + { + 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); + } + + internal void Reset() + { + this.Timestamp = default; + } + + internal readonly bool IsUpdated() + { + 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); + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs new file mode 100644 index 00000000000..fa1c50b98d5 --- /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..1a19719bbfa 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -9,32 +9,38 @@ namespace OpenTelemetry.Metrics; internal abstract class ExemplarReservoir { /// - /// Offers measurement to the reservoir. + /// Gets a value indicating whether or not the should reset its state when performing + /// collection. /// - /// 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); + /// + /// Note: is set to for + /// s using delta aggregation temporality and for s using cumulative + /// aggregation temporality. + /// + public bool ResetOnCollect { get; private set; } /// - /// Offers measurement to the reservoir. + /// Offers a 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); + + /// + /// Offers a measurement to the reservoir. + /// + /// . + 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..3d7057f85d0 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +internal abstract class FixedSizeExemplarReservoir : ExemplarReservoir +{ + private readonly Exemplar[] runningExemplars; + private readonly Exemplar[] snapshotExemplars; + + protected FixedSizeExemplarReservoir(int capacity) + { + Guard.ThrowIfOutOfRange(capacity, min: 1); + + this.runningExemplars = new Exemplar[capacity]; + this.snapshotExemplars = new Exemplar[capacity]; + this.Capacity = capacity; + } + + internal int Capacity { get; } + + /// + /// Collects all the exemplars accumulated by the Reservoir. + /// + /// . + public sealed override ReadOnlyExemplarCollection Collect() + { + var runningExemplars = this.runningExemplars; + + if (this.ResetOnCollect) + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + running.Reset(); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + else + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + + this.OnCollected(); + + return new(this.snapshotExemplars); + } + + internal sealed override void Initialize(AggregatorStore aggregatorStore) + { + var viewDefinedTagKeys = aggregatorStore.TagKeysInteresting; + + for (int i = 0; i < this.runningExemplars.Length; i++) + { + this.runningExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + this.snapshotExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + } + + base.Initialize(aggregatorStore); + } + + protected virtual void OnCollected() + { + } + + protected void UpdateExemplar(int exemplarIndex, in ExemplarMeasurement measurement) + where T : struct + { + this.runningExemplars[exemplarIndex].Update(in measurement); + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs new file mode 100644 index 00000000000..781789184a8 --- /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. + /// + internal 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() + { + var exemplars = this.exemplars; + + while (true) + { + var index = ++this.index; + if (index < exemplars.Length) + { + if (!exemplars[index].IsUpdated()) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs index 5324e7067d2..930b9647bb5 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -1,140 +1,56 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; - namespace OpenTelemetry.Metrics; /// /// The SimpleFixedSizeExemplarReservoir implementation. /// -internal sealed class SimpleFixedSizeExemplarReservoir : ExemplarReservoir +internal sealed class SimpleFixedSizeExemplarReservoir : FixedSizeExemplarReservoir { - private readonly int poolSize; - private readonly Random random; - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; + private readonly Random random = new(); - private long measurementsSeen; + private int measurementsSeen; public SimpleFixedSizeExemplarReservoir(int poolSize) + : base(poolSize) { - 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 override void Offer(in ExemplarMeasurement measurement) { - this.Offer(value, tags); + this.Offer(in measurement); } - 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) + protected override void OnCollected() { - 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; } - private void Offer(double value, ReadOnlySpan> tags) + private void Offer(in ExemplarMeasurement measurement) + where T : struct { - if (this.measurementsSeen < this.poolSize) + var measurementNumber = this.measurementsSeen++; + + if (measurementNumber < this.Capacity) { - 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); + this.UpdateExemplar(measurementNumber, in measurement); } 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) + var index = this.random.Next(0, measurementNumber); + if (index < this.Capacity) { - 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.UpdateExemplar(index, in measurement); } } - - this.measurementsSeen++; - } - - private void StoreTags(ref Exemplar exemplar, ReadOnlySpan> tags) - { - 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(); - } - - // 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); - } } } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 38ee0b18c86..65a62c3eb4a 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; @@ -102,6 +103,8 @@ internal MetricPoint( this.mpComponents = new MetricPointOptionalComponents(); } + reservoir.Initialize(aggregatorStore); + this.mpComponents.ExemplarReservoir = reservoir; } @@ -346,21 +349,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() @@ -469,7 +469,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -489,7 +490,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -509,7 +511,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -672,7 +675,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -695,7 +699,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -718,7 +723,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -885,8 +891,6 @@ internal void TakeSnapshot(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -941,7 +945,6 @@ internal void TakeSnapshot(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1058,7 +1061,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsLong = this.runningValue.AsLong; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1082,7 +1085,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsDouble = this.runningValue.AsDouble; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1095,7 +1098,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsLong = this.runningValue.AsLong; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1108,7 +1111,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsDouble = this.runningValue.AsDouble; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1134,7 +1137,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; @@ -1160,7 +1163,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningSum = 0; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1191,7 +1194,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1220,7 +1223,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningMax = double.NegativeInfinity; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1306,7 +1309,8 @@ private void UpdateHistogram(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -1334,7 +1338,8 @@ private void UpdateHistogramWithMinMax(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -1362,7 +1367,8 @@ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan(number, tags, i)); } } @@ -1391,7 +1397,8 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan(number, tags, i)); } histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); 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/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs new file mode 100644 index 00000000000..924b6e36f04 --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -0,0 +1,120 @@ +// 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. + /// + internal 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; + } + + /// + /// Gets the tag at the current position of the enumerator. + /// + public readonly KeyValuePair Current + => this.source.tags[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) + { + int index = ++this.index; + if (index < this.source.MaximumCount) + { + if (this.source.excludedKeys?.Contains(this.source.tags[index].Key) ?? false) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index 3c7dc59d770..f8582e1af99 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -40,14 +40,14 @@ public struct Enumerator internal Enumerator(ReadOnlyTagCollection source) { this.source = source; - this.index = 0; - this.Current = default; + this.index = -1; } /// /// Gets the tag at the current position of the enumerator. /// - public KeyValuePair Current { get; private set; } + public readonly KeyValuePair Current + => this.source.KeyAndValues[this.index]; /// /// Advances the enumerator to the next element of the if the enumerator has passed the end of the /// collection. - public bool MoveNext() - { - int index = this.index; - - if (index < this.source.Count) - { - this.Current = this.source.KeyAndValues[index]; - - this.index++; - return true; - } - - return false; - } + public bool MoveNext() => ++this.index < this.source.Count; } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index b5fd844f43c..356e4ac8acb 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -167,9 +167,12 @@ public void TestExemplarsFilterTags() 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); } } @@ -185,14 +188,14 @@ private static double[] GenerateRandomValues(int count) 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[] 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); + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); if (traceContextExists) { Assert.NotEqual(default, exemplar.TraceId); 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