diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 28cdb947fd9..203a7d12a3a 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -312,6 +312,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental-apis", "experi docs\diagnostics\experimental-apis\OTEL1001.md = docs\diagnostics\experimental-apis\OTEL1001.md docs\diagnostics\experimental-apis\OTEL1002.md = docs\diagnostics\experimental-apis\OTEL1002.md docs\diagnostics\experimental-apis\OTEL1003.md = docs\diagnostics\experimental-apis\OTEL1003.md + docs\diagnostics\experimental-apis\OTEL1004.md = docs\diagnostics\experimental-apis\OTEL1004.md docs\diagnostics\experimental-apis\README.md = docs\diagnostics\experimental-apis\README.md EndProjectSection EndProject diff --git a/build/Common.props b/build/Common.props index 63d25bd9306..7c1c5b5a473 100644 --- a/build/Common.props +++ b/build/Common.props @@ -10,7 +10,7 @@ enable enable - $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1003 + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1003;OTEL1004 diff --git a/docs/diagnostics/experimental-apis/OTEL1002.md b/docs/diagnostics/experimental-apis/OTEL1002.md index 8371f0d3bcd..f6cf0ca0a65 100644 --- a/docs/diagnostics/experimental-apis/OTEL1002.md +++ b/docs/diagnostics/experimental-apis/OTEL1002.md @@ -4,12 +4,12 @@ This is an Experimental API diagnostic covering the following APIs: -* `AlwaysOnExemplarFilter` -* `AlwaysOffExemplarFilter` * `Exemplar` -* `ExemplarFilter` +* `ExemplarFilterType` * `MeterProviderBuilder.SetExemplarFilter` extension method -* `TraceBasedExemplarFilter` +* `ReadOnlyExemplarCollection` +* `ReadOnlyFilteredTagCollection` +* `MetricPoint.TryGetExemplars` Experimental APIs may be changed or removed in the future. diff --git a/docs/diagnostics/experimental-apis/OTEL1004.md b/docs/diagnostics/experimental-apis/OTEL1004.md new file mode 100644 index 00000000000..543d9437bac --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1004.md @@ -0,0 +1,62 @@ +# OpenTelemetry .NET Diagnostic: OTEL1004 + +## Overview + +This is an Experimental API diagnostic covering the following APIs: + +* `ExemplarReservoir` +* `FixedSizeExemplarReservoir` +* `ExemplarMeasurement` +* `MetricStreamConfiguration.ExemplarReservoirFactory.get` +* `MetricStreamConfiguration.ExemplarReservoirFactory.set` + +Experimental APIs may be changed or removed in the future. + +## Details + +The OpenTelemetry Specification defines an [ExemplarReservoir +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarreservoir) +and a mechanism for configuring `ExemplarReservoir` via the [View +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#stream-configuration) +in the Metrics SDK. + +From the specification: + +> The SDK MUST provide a mechanism for SDK users to provide their own +> ExemplarReservoir implementation. This extension MUST be configurable on a +> metric View, although individual reservoirs MUST still be instantiated per +> metric-timeseries... + +We are exposing these APIs experimentally for the following reasons: + +* `FixedSizeExemplarReservoir` is not part of the spec. It is meant to help + custom reservoir authors and takes care of correctly creating & updating + `struct Exemplar`s (managing tag filtering when views are used), handles + `Exemplar` collection, and ensures all operations are safe to be called + concurrency (spec requirement). We want to see if this is helpful and meets + the needs of users. + +* There is currently no way to use + `MetricStreamConfiguration.ExemplarReservoirFactory` to switch a metric to a + different built-in reservoir (`AlignedHistogramBucketExemplarReservoir` or + `SimpleFixedSizeExemplarReservoir`). This is something supported by the spec + but we want to understand the use cases and needs before exposing these types. + Also it seems the default reservoirs may change. + +* There is currently no way to get access to the bucket index inside a reservoir + when a measurement is recorded against a histogram with explicit bounds. The + spec says the reservoir should calculate this given the + definition/configuration of the bounds but the SDK has already done this + computation. It seems unncessarily complicated to expose the configuration and + wasteful to do the work twice. We want to understand the types of algorithms + which users will want to implement before exposing something. + +**TL;DR** We want to gather feedback on the usability of the API and for the +need(s) in general for custom reservoirs before exposing a stable API. + + diff --git a/docs/diagnostics/experimental-apis/README.md b/docs/diagnostics/experimental-apis/README.md index a5d527de3ba..69b3eda837c 100644 --- a/docs/diagnostics/experimental-apis/README.md +++ b/docs/diagnostics/experimental-apis/README.md @@ -39,6 +39,12 @@ Description: MetricStreamConfiguration CardinalityLimit Support Details: [OTEL1003](./OTEL1003.md) +### OTEL1004 + +Description: ExemplarReservoir Support + +Details: [OTEL1004](./OTEL1004.md) + ## Inactive Experimental APIs which have been released stable or removed: diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index 21f0f976c54..00031ea690d 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -25,8 +25,12 @@ OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T OpenTelemetry.Metrics.ExemplarReservoir -OpenTelemetry.Metrics.ExemplarReservoir.ExemplarReservoir() -> void OpenTelemetry.Metrics.ExemplarReservoir.ResetOnCollect.get -> bool +OpenTelemetry.Metrics.FixedSizeExemplarReservoir +OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Capacity.get -> int +OpenTelemetry.Metrics.FixedSizeExemplarReservoir.FixedSizeExemplarReservoir(int capacity) -> void +OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void +OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection exemplars) -> bool OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void @@ -46,6 +50,7 @@ OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Enumerator() -> void OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.MoveNext() -> bool OpenTelemetry.ReadOnlyFilteredTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator OpenTelemetry.ReadOnlyFilteredTagCollection.ReadOnlyFilteredTagCollection() -> void +override sealed OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Collect() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection 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! @@ -63,3 +68,4 @@ static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.Log static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! +virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 2b20ac5927d..17b69e13e6c 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -12,6 +12,12 @@ which could have led to a measurement being dropped. ([#5546](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5546)) +* **Experimental (pre-release builds only):** Exposed + `FixedSizeExemplarReservoir` as a public API to support custom implementations + of `ExemplarReservoir` which may be configured using the + `ExemplarReservoirFactory` property on the View API. + ([#5558](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5558)) + ## 1.8.1 Released 2024-Apr-17 diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs index 8c86753e7a2..eb3882e6e08 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -12,10 +12,10 @@ namespace OpenTelemetry.Metrics; /// /// Represents an Exemplar measurement. /// -/// +/// /// Measurement type. #if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +[Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index 7098181e6a2..4eb800921f1 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -13,12 +13,12 @@ namespace OpenTelemetry.Metrics; /// ExemplarReservoir base implementation and contract. /// /// -/// +/// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. /// Specification: . /// #if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +[Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else @@ -26,6 +26,13 @@ namespace OpenTelemetry.Metrics; #endif abstract class ExemplarReservoir { + // Note: This constructor is internal because we don't allow custom + // ExemplarReservoir implementations to be based directly on the base class + // only FixedSizeExemplarReservoir. + internal ExemplarReservoir() + { + } + /// /// Gets a value indicating whether or not the should reset its state when performing diff --git a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs index 16fcb4f1ebd..dc929e725ed 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -1,17 +1,44 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics; -internal abstract class FixedSizeExemplarReservoir : ExemplarReservoir +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// An implementation which contains a fixed +/// number of s. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + abstract class FixedSizeExemplarReservoir : ExemplarReservoir { private readonly Exemplar[] runningExemplars; private readonly Exemplar[] snapshotExemplars; + /// + /// Initializes a new instance of the class. + /// + /// The capacity (number of s) + /// to be contained in the reservoir. +#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable protected FixedSizeExemplarReservoir(int capacity) +#pragma warning restore RS0022 // Constructor make noninheritable base class inheritable { + // Note: RS0022 is suppressed because we do want to allow custom + // ExemplarReservoir implementations to be created by deriving from + // FixedSizeExemplarReservoir. + Guard.ThrowIfOutOfRange(capacity, min: 1); this.runningExemplars = new Exemplar[capacity]; @@ -19,7 +46,11 @@ protected FixedSizeExemplarReservoir(int capacity) this.Capacity = capacity; } - internal int Capacity { get; } + /// + /// Gets the capacity (number of s) contained in the + /// reservoir. + /// + public int Capacity { get; } /// /// Collects all the exemplars accumulated by the Reservoir. @@ -56,12 +87,48 @@ internal sealed override void Initialize(AggregatorStore aggregatorStore) base.Initialize(aggregatorStore); } + internal void UpdateExemplar( + int exemplarIndex, + in ExemplarMeasurement measurement) + where T : struct + { + this.runningExemplars[exemplarIndex].Update(in measurement); + } + + /// + /// Fired when has finished before returning a . + /// + /// + /// Note: This method is typically used to reset the state of reservoirs and + /// is called regardless of the value of . + /// protected virtual void OnCollected() { } - protected void UpdateExemplar(int exemplarIndex, in ExemplarMeasurement measurement) - where T : struct + /// + /// Updates the stored in the reservoir at the + /// specified index with an . + /// + /// Index of the to update. + /// . + protected void UpdateExemplar( + int exemplarIndex, + in ExemplarMeasurement measurement) + { + this.runningExemplars[exemplarIndex].Update(in measurement); + } + + /// + /// Updates the stored in the reservoir at the + /// specified index with an . + /// + /// Index of the to update. + /// . + protected void UpdateExemplar( + int exemplarIndex, + in ExemplarMeasurement measurement) { this.runningExemplars[exemplarIndex].Update(in measurement); } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 5f07f10c485..adf0136d3c4 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -2,6 +2,9 @@ // 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; using OpenTelemetry.Internal; @@ -370,6 +373,9 @@ public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) /// . /// if exemplars exist; otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif public #else [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs index cc48817cc7b..578049f329f 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs @@ -144,7 +144,7 @@ public string[]? TagKeys /// when storing s. /// /// - /// + /// /// Note: Returning from the factory function will /// result in the default being chosen by /// the SDK based on the type of metric. @@ -152,7 +152,7 @@ public string[]? TagKeys /// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#stream-configuration"/>. /// #if NET8_0_OR_GREATER - [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] + [Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public Func? ExemplarReservoirFactory { get; set; } #else diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs index b772ea53652..e758d26700b 100644 --- a/src/Shared/DiagnosticDefinitions.cs +++ b/src/Shared/DiagnosticDefinitions.cs @@ -13,4 +13,5 @@ internal static class DiagnosticDefinitions public const string LogsBridgeExperimentalApi = "OTEL1001"; public const string ExemplarExperimentalApi = "OTEL1002"; public const string CardinalityLimitExperimentalApi = "OTEL1003"; + public const string ExemplarReservoirExperimentalApi = "OTEL1004"; }