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";
}