[sdk-metrics] ExemplarFilter updates to match latest specification (#5404)

This commit is contained in:
Mikel Blanchard 2024-03-01 11:42:47 -08:00 committed by GitHub
parent 42ecd73bd0
commit b754b13cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 320 additions and 226 deletions

View File

@ -412,26 +412,23 @@ exemplars.
#### ExemplarFilter
`ExemplarFilter` determines which measurements are eligible to become an
Exemplar. i.e. `ExemplarFilter` determines which measurements are offered to
`ExemplarReservoir`, which makes the final decision about whether the offered
measurement gets stored as an exemplar. They can be used to control the noise
and overhead associated with Exemplar collection.
`ExemplarFilter` determines which measurements are offered to the configured
`ExemplarReservoir`, which makes the final decision about whether or not the
offered measurement gets recorded as an `Exemplar`. Generally `ExemplarFilter`
is a mechanism to control the overhead associated with `Exemplar` offering.
OpenTelemetry SDK comes with the following Filters:
OpenTelemetry SDK comes with the following `ExemplarFilters` (defined on
`ExemplarFilterType`):
* `AlwaysOnExemplarFilter` - makes all measurements eligible for being an Exemplar.
* `AlwaysOffExemplarFilter` - makes no measurements eligible for being an
Exemplar. Using this is as good as turning off Exemplar feature, and is the current
* `AlwaysOff`: Makes no measurements eligible for becoming an `Exemplar`. Using
this is as good as turning off the `Exemplar` feature and is the current
default.
* `TraceBasedExemplarFilter` - makes those measurements eligible for being an
Exemplar, which are recorded in the context of a sampled parent `Activity`
(span).
* `AlwaysOn`: Makes all measurements eligible for becoming an `Exemplar`.
* `TraceBased`: Makes those measurements eligible for becoming an `Exemplar`
which are recorded in the context of a sampled `Activity` (span).
`SetExemplarFilter` method on `MeterProviderBuilder` can be used to set the
desired `ExemplarFilter`.
The snippet below shows how to set `ExemplarFilter`.
The `SetExemplarFilter` extension method on `MeterProviderBuilder` can be used
to set the desired `ExemplarFilterType` and enable `Exemplar` collection:
```csharp
using OpenTelemetry;
@ -439,31 +436,14 @@ using OpenTelemetry.Metrics;
using var meterProvider = Sdk.CreateMeterProviderBuilder()
// rest of config not shown
.SetExemplarFilter(new TraceBasedExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.TraceBased)
.Build();
```
> [!NOTE]
> As of today, there is no separate toggle for enable/disable Exemplar feature.
Exemplars can be disabled by setting filter as `AlwaysOffExemplarFilter`, which
is also the default (i.e Exemplar feature is disabled by default). Users can
enable the feature by setting filter to anything other than
`AlwaysOffExemplarFilter`. For example: `.SetExemplarFilter(new TraceBasedExemplarFilter())`.
If the built-in `ExemplarFilter`s are not meeting the needs, one may author
custom `ExemplarFilter` as shown
[here](../extending-the-sdk/README.md#exemplarfilter). A custom filter, which
eliminates all un-interesting measurements from becoming Exemplar is a
recommended way to control performance overhead associated with collecting
Exemplars. See
[benchmark](../../../test/Benchmarks/Metrics/ExemplarBenchmarks.cs) to see how
much impact can `ExemplarFilter` have on performance.
#### ExemplarReservoir
`ExemplarReservoir` receives the measurements sampled in by the `ExemplarFilter`
and is responsible for storing Exemplars. `ExemplarReservoir` ultimately decides
which measurements get stored as exemplars. The following are the default
`ExemplarReservoir` receives the measurements sampled by the `ExemplarFilter`
and is responsible for recording `Exemplar`s. The following are the default
reservoirs:
* `AlignedHistogramBucketExemplarReservoir` is the default reservoir used for
@ -479,7 +459,7 @@ size (currently defaulting to 1) determines the maximum number of exemplars
stored.
> [!NOTE]
> Currently there is no ability to change or configure Reservoir.
> Currently there is no ability to change or configure `ExemplarReservoir`.
### Instrumentation

View File

@ -74,44 +74,7 @@ Not supported.
## ExemplarFilter
OpenTelemetry .NET SDK has provided the following built-in `ExemplarFilter`s:
* [AlwaysOnExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs)
* [AlwaysOffExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs)
* [TraceBasedExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs)
Custom exemplar filters can be implemented to achieve filtering based on other criterion:
* `ExemplarFilter` should derive from `OpenTelemetry.ExemplarFilter` (which
belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package)
and implement the `ShouldSample` method.
One example is a filter, which filters all measurements of value lower
than given threshold is given below. Such a filter prevents any measurements
below the given threshold from ever becoming a `Exemplar`. Such filters could
also incorporate the `TraceBasedExemplarFilter` condition as well, as storing
exemplars for non-sampled traces may be undesired.
```csharp
public sealed class HighValueFilter : ExemplarFilter
{
private readonly double maxValue;
public HighValueFilter(double maxValue)
{
this.maxValue = maxValue;
}
public override bool ShouldSample(long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
return Activity.Current?.Recorded && value > this.maxValue;
}
public override bool ShouldSample(double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
return Activity.Current?.Recorded && value > this.maxValue;
}
}
```
Not supported.
## ExemplarReservoir

View File

@ -85,7 +85,7 @@ appBuilder.Services.AddOpenTelemetry()
builder
.AddMeter(Instrumentation.MeterName)
#if EXPOSE_EXPERIMENTAL_FEATURES
.SetExemplarFilter(new TraceBasedExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.TraceBased)
#endif
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()

View File

@ -5,10 +5,6 @@ OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverit
OpenTelemetry.Logs.LogRecord.Severity.set -> void
OpenTelemetry.Logs.LogRecord.SeverityText.get -> string?
OpenTelemetry.Logs.LogRecord.SeverityText.set -> void
OpenTelemetry.Metrics.AlwaysOffExemplarFilter
OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
OpenTelemetry.Metrics.AlwaysOnExemplarFilter
OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
OpenTelemetry.Metrics.Exemplar
OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
OpenTelemetry.Metrics.Exemplar.Exemplar() -> void
@ -17,8 +13,10 @@ 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.ExemplarFilterType
OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOff = 0 -> OpenTelemetry.Metrics.ExemplarFilterType
OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOn = 1 -> OpenTelemetry.Metrics.ExemplarFilterType
OpenTelemetry.Metrics.ExemplarFilterType.TraceBased = 2 -> OpenTelemetry.Metrics.ExemplarFilterType
OpenTelemetry.Metrics.ExemplarMeasurement<T>
OpenTelemetry.Metrics.ExemplarMeasurement<T>.ExemplarMeasurement() -> void
OpenTelemetry.Metrics.ExemplarMeasurement<T>.Tags.get -> System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>>
@ -33,8 +31,6 @@ 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<string!, object?>
@ -51,19 +47,11 @@ static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(thi
static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor<OpenTelemetry.Logs.LogRecord!>! processor) -> OpenTelemetry.Logs.LoggerProvider!
static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool
static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool
static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder!
static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilterType exemplarFilter = OpenTelemetry.Metrics.ExemplarFilterType.TraceBased) -> OpenTelemetry.Metrics.MeterProviderBuilder!
static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder!
static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>! configure) -> OpenTelemetry.IOpenTelemetryBuilder!
static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>? configureBuilder, System.Action<OpenTelemetry.Logs.OpenTelemetryLoggerOptions!>? configureOptions) -> OpenTelemetry.IOpenTelemetryBuilder!
static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder!
abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string!, object?>> tags) -> bool
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<OpenTelemetry.Logs.LoggerProviderBuilder!>! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder!
static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>? configureBuilder, System.Action<OpenTelemetry.Logs.OpenTelemetryLoggerOptions!>? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder!

View File

@ -55,6 +55,16 @@
API.
([#5396](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5396))
* **Experimental (pre-release builds only):** Removed the `ExemplarFilter`,
`AlwaysOffExemplarFilter`, `AlwaysOnExemplarFilter`, and
`TraceBasedExemplarFilter` APIs. The `MeterProviderBuilder.SetExemplarFilter`
extension method now accepts an `ExemplarFilterType` enumeration (which
contains definitions for the supported filter types `AlwaysOff`, `AlwaysOn`,
and `TraceBased`) instead of an `ExemplarFilter` instance. This was done in
response to changes made to the [OpenTelemetry Metrics SDK
Specification](https://github.com/open-telemetry/opentelemetry-specification/pull/3820).
([#5404](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5404))
## 1.7.0
Released 2023-Dec-08

View File

@ -319,36 +319,54 @@ public static class MeterProviderBuilderExtensions
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Sets the <see cref="ExemplarFilter"/> to be used for this provider.
/// This is applied to all the metrics from this provider.
/// Sets the <see cref="ExemplarFilterType"/> to be used for this provider
/// which controls how measurements will be offered to exemplar reservoirs.
/// Default provider configuration: <see
/// cref="ExemplarFilterType.AlwaysOff"/>.
/// </summary>
/// <remarks><inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/></remarks>
/// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
/// <param name="exemplarFilter"><see cref="ExemplarFilter"/> ExemplarFilter to use.</param>
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
/// <remarks>
/// <inheritdoc cref="Exemplar"
/// path="/remarks/para[@experimental-warning='true']"/>
/// <para>Note: Use <see cref="ExemplarFilterType.TraceBased"/> or <see
/// cref="ExemplarFilterType.AlwaysOn"/> to enable exemplars.</para>
/// <para>Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarfilter"/>.</para>
/// </remarks>
/// <param name="meterProviderBuilder"><see
/// cref="MeterProviderBuilder"/>.</param>
/// <param name="exemplarFilter"><see cref="ExemplarFilterType"/> to
/// use.</param>
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for
/// chaining.</returns>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
/// <summary>
/// Sets the <see cref="ExemplarFilter"/> to be used for this provider.
/// This is applied to all the metrics from this provider.
/// </summary>
/// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
/// <param name="exemplarFilter"><see cref="ExemplarFilter"/> ExemplarFilter to use.</param>
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
internal
#endif
static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter)
static MeterProviderBuilder SetExemplarFilter(
this MeterProviderBuilder meterProviderBuilder,
ExemplarFilterType exemplarFilter = ExemplarFilterType.TraceBased)
{
Guard.ThrowIfNull(exemplarFilter);
meterProviderBuilder.ConfigureBuilder((sp, builder) =>
{
if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
{
meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter);
switch (exemplarFilter)
{
case ExemplarFilterType.AlwaysOn:
meterProviderBuilderSdk.SetExemplarFilter(new AlwaysOnExemplarFilter());
break;
case ExemplarFilterType.AlwaysOff:
meterProviderBuilderSdk.SetExemplarFilter(new AlwaysOffExemplarFilter());
break;
case ExemplarFilterType.TraceBased:
meterProviderBuilderSdk.SetExemplarFilter(new TraceBasedExemplarFilter());
break;
default:
throw new NotSupportedException($"SdkExemplarFilter '{exemplarFilter}' is not supported.");
}
}
});

View File

@ -1,31 +1,17 @@
// 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
/// <summary>
/// An <see cref="ExemplarFilter"/> implementation which makes no measurements
/// eligible for becoming an <see cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwaysoff"/>.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
internal
#endif
sealed class AlwaysOffExemplarFilter : ExemplarFilter
internal sealed class AlwaysOffExemplarFilter : ExemplarFilter
{
/// <inheritdoc/>
public override bool ShouldSample(long value, ReadOnlySpan<KeyValuePair<string, object?>> tags)

View File

@ -1,34 +1,17 @@
// 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
/// <summary>
/// An <see cref="ExemplarFilter"/> implementation which makes all measurements
/// eligible for becoming an <see cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwayson"/>.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
/// <summary>
/// An ExemplarFilter which makes all measurements eligible for being an Exemplar.
/// </summary>
internal
#endif
sealed class AlwaysOnExemplarFilter : ExemplarFilter
internal sealed class AlwaysOnExemplarFilter : ExemplarFilter
{
/// <inheritdoc/>
public override bool ShouldSample(long value, ReadOnlySpan<KeyValuePair<string, object?>> tags)

View File

@ -1,30 +1,16 @@
// 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
/// <summary>
/// ExemplarFilter base implementation and contract.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarfilter"/>.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
internal
#endif
abstract class ExemplarFilter
internal abstract class ExemplarFilter
{
/// <summary>
/// Determines if a given measurement is eligible for being

View File

@ -0,0 +1,62 @@
// 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
/// <summary>
/// Defines the supported exemplar filters.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarfilter"/>.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
internal
#endif
enum ExemplarFilterType
{
/// <summary>
/// An exemplar filter which makes no measurements eligible for becoming an
/// <see cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// <para>Note: Setting <see cref="AlwaysOff"/> on a meter provider
/// effectively disables exemplars.</para>
/// <para>Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwaysoff"/>.</para>
/// </remarks>
AlwaysOff,
/// <summary>
/// An exemplar filter which makes all measurements eligible for becoming an
/// <see cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwayson"/>.
/// </remarks>
AlwaysOn,
/// <summary>
/// An exemplar filter which makes measurements recorded in the context of a
/// sampled <see cref="Activity"/> (span) eligible for becoming an <see
/// cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#tracebased"/>.
/// </remarks>
TraceBased,
}

View File

@ -1,34 +1,20 @@
// 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
using System.Diagnostics;
namespace OpenTelemetry.Metrics;
#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// An <see cref="ExemplarFilter"/> implementation which makes measurements
/// recorded in the context of a sampled <see cref="Activity"/> (span) eligible
/// for becoming an <see cref="Exemplar"/>.
/// </summary>
/// <remarks>
/// <inheritdoc cref="Exemplar" path="/remarks/para[@experimental-warning='true']"/>
/// Specification: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#tracebased"/>.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#endif
public
#else
internal
#endif
sealed class TraceBasedExemplarFilter : ExemplarFilter
internal sealed class TraceBasedExemplarFilter : ExemplarFilter
{
/// <inheritdoc/>
public override bool ShouldSample(long value, ReadOnlySpan<KeyValuePair<string, object?>> tags)

View File

@ -9,21 +9,31 @@ using OpenTelemetry.Metrics;
using OpenTelemetry.Tests;
/*
BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000)
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
12th Gen Intel Core i9-12900HK, 1 CPU, 20 logical and 14 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
| Method | ExemplarFilter | Mean | Error | StdDev | Allocated |
|-------------------------- |--------------- |---------:|--------:|--------:|----------:|
| HistogramNoTagReduction | AlwaysOff | 274.2 ns | 2.94 ns | 2.60 ns | - |
| HistogramWithTagReduction | AlwaysOff | 241.6 ns | 1.78 ns | 1.58 ns | - |
| HistogramNoTagReduction | AlwaysOn | 300.9 ns | 3.10 ns | 2.90 ns | - |
| HistogramWithTagReduction | AlwaysOn | 312.9 ns | 4.81 ns | 4.50 ns | - |
| HistogramNoTagReduction | HighValueOnly | 262.8 ns | 2.24 ns | 1.99 ns | - |
| HistogramWithTagReduction | HighValueOnly | 258.3 ns | 5.12 ns | 5.03 ns | - |
| Method | ExemplarConfiguration | Mean | Error | StdDev | Allocated |
|-------------------------- |---------------------- |---------:|--------:|--------:|----------:|
| HistogramNoTagReduction | AlwaysOff | 174.6 ns | 1.32 ns | 1.24 ns | - |
| HistogramWithTagReduction | AlwaysOff | 161.8 ns | 2.63 ns | 2.46 ns | - |
| CounterNoTagReduction | AlwaysOff | 141.6 ns | 2.12 ns | 1.77 ns | - |
| CounterWithTagReduction | AlwaysOff | 141.7 ns | 2.11 ns | 1.87 ns | - |
| HistogramNoTagReduction | AlwaysOn | 201.1 ns | 3.05 ns | 2.86 ns | - |
| HistogramWithTagReduction | AlwaysOn | 196.5 ns | 1.91 ns | 1.78 ns | - |
| CounterNoTagReduction | AlwaysOn | 149.7 ns | 1.42 ns | 1.33 ns | - |
| CounterWithTagReduction | AlwaysOn | 143.5 ns | 2.09 ns | 1.95 ns | - |
| HistogramNoTagReduction | TraceBased | 171.9 ns | 2.33 ns | 2.18 ns | - |
| HistogramWithTagReduction | TraceBased | 164.9 ns | 2.70 ns | 2.52 ns | - |
| CounterNoTagReduction | TraceBased | 148.1 ns | 2.76 ns | 2.58 ns | - |
| CounterWithTagReduction | TraceBased | 141.2 ns | 1.43 ns | 1.34 ns | - |
| HistogramNoTagReduction | Alway(...)pling [29] | 183.9 ns | 1.49 ns | 1.39 ns | - |
| HistogramWithTagReduction | Alway(...)pling [29] | 176.1 ns | 3.35 ns | 3.29 ns | - |
| CounterNoTagReduction | Alway(...)pling [29] | 159.3 ns | 3.12 ns | 4.38 ns | - |
| CounterWithTagReduction | Alway(...)pling [29] | 158.7 ns | 3.06 ns | 3.65 ns | - |
*/
namespace Benchmarks.Metrics;
@ -32,51 +42,74 @@ public class ExemplarBenchmarks
{
private static readonly ThreadLocal<Random> ThreadLocalRandom = new(() => new Random());
private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" };
private Histogram<long> histogramWithoutTagReduction;
private Histogram<long> histogramWithTagReduction;
private Histogram<double> histogramWithoutTagReduction;
private Histogram<double> histogramWithTagReduction;
private Counter<long> counterWithoutTagReduction;
private Counter<long> counterWithTagReduction;
private MeterProvider meterProvider;
private Meter meter;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Test only.")]
public enum ExemplarFilterToUse
public enum ExemplarConfigurationType
{
AlwaysOff,
AlwaysOn,
HighValueOnly,
TraceBased,
AlwaysOnWithHighValueSampling,
}
[Params(ExemplarFilterToUse.AlwaysOn, ExemplarFilterToUse.AlwaysOff, ExemplarFilterToUse.HighValueOnly)]
public ExemplarFilterToUse ExemplarFilter { get; set; }
[Params(ExemplarConfigurationType.AlwaysOn, ExemplarConfigurationType.AlwaysOff, ExemplarConfigurationType.TraceBased, ExemplarConfigurationType.AlwaysOnWithHighValueSampling)]
public ExemplarConfigurationType ExemplarConfiguration { get; set; }
[GlobalSetup]
public void Setup()
{
this.meter = new Meter(Utils.GetCurrentMethodName());
this.histogramWithoutTagReduction = this.meter.CreateHistogram<long>("HistogramWithoutTagReduction");
this.histogramWithTagReduction = this.meter.CreateHistogram<long>("HistogramWithTagReduction");
this.histogramWithoutTagReduction = this.meter.CreateHistogram<double>("HistogramWithoutTagReduction");
this.histogramWithTagReduction = this.meter.CreateHistogram<double>("HistogramWithTagReduction");
this.counterWithoutTagReduction = this.meter.CreateCounter<long>("CounterWithoutTagReduction");
this.counterWithTagReduction = this.meter.CreateCounter<long>("CounterWithTagReduction");
var exportedItems = new List<Metric>();
ExemplarFilter exemplarFilter = new AlwaysOffExemplarFilter();
if (this.ExemplarFilter == ExemplarFilterToUse.AlwaysOn)
{
exemplarFilter = new AlwaysOnExemplarFilter();
}
else if (this.ExemplarFilter == ExemplarFilterToUse.HighValueOnly)
{
exemplarFilter = new HighValueExemplarFilter();
}
var exemplarFilter = this.ExemplarConfiguration == ExemplarConfigurationType.TraceBased
? ExemplarFilterType.TraceBased
: this.ExemplarConfiguration != ExemplarConfigurationType.AlwaysOff
? ExemplarFilterType.AlwaysOn
: ExemplarFilterType.AlwaysOff;
this.meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(this.meter.Name)
.SetExemplarFilter(exemplarFilter)
.AddView("HistogramWithTagReduction", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } })
.AddView(i =>
{
if (i.Name.Contains("WithTagReduction"))
{
return new MetricStreamConfiguration()
{
TagKeys = new string[] { "DimName1", "DimName2", "DimName3" },
ExemplarReservoirFactory = CreateExemplarReservoir,
};
}
else
{
return new MetricStreamConfiguration()
{
ExemplarReservoirFactory = CreateExemplarReservoir,
};
}
})
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000;
})
.Build();
ExemplarReservoir CreateExemplarReservoir()
{
return this.ExemplarConfiguration == ExemplarConfigurationType.AlwaysOnWithHighValueSampling
? new HighValueExemplarReservoir(800D)
: null;
}
}
[GlobalCleanup]
@ -99,7 +132,7 @@ public class ExemplarBenchmarks
{ "DimName5", this.dimensionValues[random.Next(0, 10)] },
};
this.histogramWithoutTagReduction.Record(random.Next(1000), tags);
this.histogramWithoutTagReduction.Record(random.NextDouble() * 1000D, tags);
}
[Benchmark]
@ -115,19 +148,71 @@ public class ExemplarBenchmarks
{ "DimName5", this.dimensionValues[random.Next(0, 10)] },
};
this.histogramWithTagReduction.Record(random.Next(1000), tags);
this.histogramWithTagReduction.Record(random.NextDouble() * 1000D, tags);
}
internal class HighValueExemplarFilter : ExemplarFilter
[Benchmark]
public void CounterNoTagReduction()
{
public override bool ShouldSample(long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
var random = ThreadLocalRandom.Value;
var tags = new TagList
{
return value > 800;
{ "DimName1", this.dimensionValues[random.Next(0, 2)] },
{ "DimName2", this.dimensionValues[random.Next(0, 2)] },
{ "DimName3", this.dimensionValues[random.Next(0, 5)] },
{ "DimName4", this.dimensionValues[random.Next(0, 5)] },
{ "DimName5", this.dimensionValues[random.Next(0, 10)] },
};
this.counterWithoutTagReduction.Add(random.Next(1000), tags);
}
[Benchmark]
public void CounterWithTagReduction()
{
var random = ThreadLocalRandom.Value;
var tags = new TagList
{
{ "DimName1", this.dimensionValues[random.Next(0, 2)] },
{ "DimName2", this.dimensionValues[random.Next(0, 2)] },
{ "DimName3", this.dimensionValues[random.Next(0, 5)] },
{ "DimName4", this.dimensionValues[random.Next(0, 5)] },
{ "DimName5", this.dimensionValues[random.Next(0, 10)] },
};
this.counterWithTagReduction.Add(random.Next(1000), tags);
}
private sealed class HighValueExemplarReservoir : FixedSizeExemplarReservoir
{
private readonly double threshold;
private int measurementCount;
public HighValueExemplarReservoir(double threshold)
: base(10)
{
this.threshold = threshold;
}
public override bool ShouldSample(double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
public override void Offer(in ExemplarMeasurement<long> measurement)
{
return value > 800;
if (measurement.Value >= this.threshold)
{
this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement);
}
}
public override void Offer(in ExemplarMeasurement<double> measurement)
{
if (measurement.Value >= this.threshold)
{
this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement);
}
}
protected override void OnCollected()
{
this.measurementCount = 0;
}
}
}

View File

@ -234,7 +234,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter())
.SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff)
.AddInMemoryExporter(metrics)
.Build();
@ -309,7 +309,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter())
.SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff)
.AddInMemoryExporter(metrics, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = aggregationTemporality;
@ -406,7 +406,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter())
.SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff)
.AddInMemoryExporter(metrics, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = aggregationTemporality;
@ -503,7 +503,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter())
.SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff)
.AddInMemoryExporter(metrics, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = aggregationTemporality;
@ -643,7 +643,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter())
.SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff)
.AddInMemoryExporter(metrics, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = aggregationTemporality;
@ -793,7 +793,7 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(i =>
{
return !enableTagFiltering

View File

@ -56,7 +56,7 @@ public static class Program
if (options.EnableExemplars)
{
builder.SetExemplarFilter(new AlwaysOnExemplarFilter());
builder.SetExemplarFilter(ExemplarFilterType.AlwaysOn);
}
if (options.AddViewToFilterTags)

View File

@ -28,7 +28,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(i =>
{
if (i.Name.StartsWith("testCounter"))
@ -153,7 +153,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = temporality;
@ -237,7 +237,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(i =>
{
if (i.Name.StartsWith("histogramWithBucketsAndMinMax"))
@ -367,7 +367,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(i =>
{
if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax"))
@ -495,7 +495,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(i =>
{
if (i.Name.StartsWith("exponentialHistogramWithMinMax"))
@ -601,6 +601,53 @@ public class MetricExemplarTests : MetricTestsBase
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void TestTraceBasedExemplarFilter(bool enableTracing)
{
var exportedItems = new List<Metric>();
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var counter = meter.CreateCounter<long>("testCounter");
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(ExemplarFilterType.TraceBased)
.AddInMemoryExporter(exportedItems));
if (enableTracing)
{
using var act = new Activity("test").Start();
act.ActivityTraceFlags = ActivityTraceFlags.Recorded;
counter.Add(18);
}
else
{
counter.Add(18);
}
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metricPoint = GetFirstMetricPoint(exportedItems);
Assert.NotNull(metricPoint);
var exemplars = GetExemplars(metricPoint.Value);
if (enableTracing)
{
Assert.Single(exemplars);
}
else
{
Assert.Empty(exemplars);
}
}
[Fact]
public void TestExemplarsFilterTags()
{
@ -612,7 +659,7 @@ public class MetricExemplarTests : MetricTestsBase
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetExemplarFilter(new AlwaysOnExemplarFilter())
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
.AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } })
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{