Skip to content

Commit 1e065cb

Browse files
[sdk-metrics] Add experimental envvar for setting ExemplarFilter for histograms (#5611)
Co-authored-by: Cijo Thomas <[email protected]>
1 parent 808abc8 commit 1e065cb

File tree

8 files changed

+180
-54
lines changed

8 files changed

+180
-54
lines changed

docs/metrics/customizing-the-sdk/README.md

+39-6
Original file line numberDiff line numberDiff line change
@@ -352,26 +352,40 @@ tutorial](../exemplars/README.md) demonstrates how to use exemplars to achieve
352352
correlation from metrics to traces, which is one of the primary use cases for
353353
exemplars.
354354

355+
#### Default behavior
356+
357+
Exemplars in OpenTelemetry .NET are **off by default**
358+
(`ExemplarFilterType.AlwaysOff`). The [OpenTelemetry
359+
Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarfilter)
360+
recommends Exemplars collection should be on by default
361+
(`ExemplarFilterType.TraceBased`) however there is a performance cost associated
362+
with Exemplars so OpenTelemetry .NET has taken a more conservative stance for
363+
its default behavior.
364+
355365
#### ExemplarFilter
356366

357367
`ExemplarFilter` determines which measurements are offered to the configured
358368
`ExemplarReservoir`, which makes the final decision about whether or not the
359369
offered measurement gets recorded as an `Exemplar`. Generally `ExemplarFilter`
360-
is a mechanism to control the overhead associated with `Exemplar` offering.
370+
is a mechanism to control the overhead associated with the offering and
371+
recording of `Exemplar`s.
361372

362-
OpenTelemetry SDK comes with the following `ExemplarFilters` (defined on
373+
OpenTelemetry SDK comes with the following `ExemplarFilter`s (defined on
363374
`ExemplarFilterType`):
364375

365-
* `AlwaysOff`: Makes no measurements eligible for becoming an `Exemplar`. Using
366-
this is as good as turning off the `Exemplar` feature and is the current
367-
default.
376+
* (Default behavior) `AlwaysOff`: Makes no measurements eligible for becoming an
377+
`Exemplar`. Using this disables `Exemplar` collection and avoids all
378+
performance costs associated with `Exemplar`s.
368379
* `AlwaysOn`: Makes all measurements eligible for becoming an `Exemplar`.
369380
* `TraceBased`: Makes those measurements eligible for becoming an `Exemplar`
370381
which are recorded in the context of a sampled `Activity` (span).
371382

372383
The `SetExemplarFilter` extension method on `MeterProviderBuilder` can be used
373384
to set the desired `ExemplarFilterType` and enable `Exemplar` collection:
374385

386+
> [!NOTE]
387+
> The `SetExemplarFilter` API was added in the `1.9.0` release.
388+
375389
```csharp
376390
using OpenTelemetry;
377391
using OpenTelemetry.Metrics;
@@ -382,6 +396,24 @@ using var meterProvider = Sdk.CreateMeterProviderBuilder()
382396
.Build();
383397
```
384398

399+
It is also possible to configure the `ExemplarFilter` by using following
400+
environmental variables:
401+
402+
> [!NOTE]
403+
> Programmatically calling `SetExemplarFilter` will override any defaults set
404+
using environment variables or configuration.
405+
406+
| Environment variable | Description | Notes |
407+
| -------------------------- | -------------------------------------------------- |-------|
408+
| `OTEL_METRICS_EXEMPLAR_FILTER` | Sets the default `ExemplarFilter` to use for all metrics. | Added in `1.9.0` |
409+
| `OTEL_DOTNET_EXPERIMENTAL_METRICS_EXEMPLAR_FILTER_HISTOGRAMS` | Sets the default `ExemplarFilter` to use for histogram metrics. If set `OTEL_DOTNET_EXPERIMENTAL_METRICS_EXEMPLAR_FILTER_HISTOGRAMS` takes precedence over `OTEL_METRICS_EXEMPLAR_FILTER` for histogram metrics. | Experimental key (may be removed or changed in the future). Added in `1.9.0` |
410+
411+
Allowed values:
412+
413+
* `always_off`: Equivalent to `ExemplarFilterType.AlwaysOff`
414+
* `always_on`: Equivalent to `ExemplarFilterType.AlwaysOn`
415+
* `trace_based`: Equivalent to `ExemplarFilterType.TraceBased`
416+
385417
#### ExemplarReservoir
386418

387419
`ExemplarReservoir` receives the measurements sampled by the `ExemplarFilter`
@@ -398,7 +430,8 @@ metrics except Histograms with buckets. It has a fixed reservoir pool, and
398430
implements the equivalent of [naive
399431
reservoir](https://en.wikipedia.org/wiki/Reservoir_sampling). The reservoir pool
400432
size (currently defaulting to 1) determines the maximum number of exemplars
401-
stored.
433+
stored. Exponential histograms use a `SimpleFixedSizeExemplarReservoir` with a
434+
pool size equal to the number of buckets up to a max of `20`.
402435

403436
> [!NOTE]
404437
> Currently there is no ability to change or configure `ExemplarReservoir`.

examples/AspNetCore/Program.cs

-2
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@
8484
// Ensure the MeterProvider subscribes to any custom Meters.
8585
builder
8686
.AddMeter(Instrumentation.MeterName)
87-
#if EXPOSE_EXPERIMENTAL_FEATURES
8887
.SetExemplarFilter(ExemplarFilterType.TraceBased)
89-
#endif
9088
.AddRuntimeInstrumentation()
9189
.AddHttpClientInstrumentation()
9290
.AddAspNetCoreInstrumentation();

src/OpenTelemetry/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
which has always been supported.
3131
([#5614](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5614))
3232

33+
* The `ExemplarFilter` used by SDK `MeterProvider`s for histogram metrics can
34+
now be controlled via the experimental
35+
`OTEL_DOTNET_EXPERIMENTAL_METRICS_EXEMPLAR_FILTER_HISTOGRAMS` environment
36+
variable. The supported values are: `always_off`, `always_on`, and
37+
`trace_based`.
38+
([#5611](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5611))
39+
3340
## 1.8.1
3441

3542
Released 2024-Apr-17

src/OpenTelemetry/Metrics/MeterProviderSdk.cs

+52-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal sealed class MeterProviderSdk : MeterProvider
1616
internal const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE";
1717
internal const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS";
1818
internal const string ExemplarFilterConfigKey = "OTEL_METRICS_EXEMPLAR_FILTER";
19+
internal const string ExemplarFilterHistogramsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EXEMPLAR_FILTER_HISTOGRAMS";
1920

2021
internal readonly IServiceProvider ServiceProvider;
2122
internal readonly IDisposable? OwnedServiceProvider;
@@ -24,6 +25,7 @@ internal sealed class MeterProviderSdk : MeterProvider
2425
internal bool EmitOverflowAttribute;
2526
internal bool ReclaimUnusedMetricPoints;
2627
internal ExemplarFilterType? ExemplarFilter;
28+
internal ExemplarFilterType? ExemplarFilterForHistograms;
2729
internal Action? OnCollectObservableInstruments;
2830

2931
private readonly List<object> instrumentations = new();
@@ -72,6 +74,9 @@ internal MeterProviderSdk(
7274

7375
this.viewConfigs = state.ViewConfigs;
7476

77+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent(
78+
$"MeterProvider configuration: {{MetricLimit={state.MetricLimit}, CardinalityLimit={state.CardinalityLimit}, EmitOverflowAttribute={this.EmitOverflowAttribute}, ReclaimUnusedMetricPoints={this.ReclaimUnusedMetricPoints}, ExemplarFilter={this.ExemplarFilter}, ExemplarFilterForHistograms={this.ExemplarFilterForHistograms}}}.");
79+
7580
foreach (var reader in state.Readers)
7681
{
7782
Guard.ThrowIfNull(reader);
@@ -83,7 +88,8 @@ internal MeterProviderSdk(
8388
state.CardinalityLimit,
8489
this.EmitOverflowAttribute,
8590
this.ReclaimUnusedMetricPoints,
86-
this.ExemplarFilter);
91+
this.ExemplarFilter,
92+
this.ExemplarFilterForHistograms);
8793

8894
if (this.reader == null)
8995
{
@@ -490,37 +496,70 @@ private void ApplySpecificationConfigurationKeys(IConfiguration configuration)
490496
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Reclaim unused metric point feature enabled via configuration.");
491497
}
492498

499+
var hasProgrammaticExemplarFilterValue = this.ExemplarFilter.HasValue;
500+
493501
if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue))
494502
{
495-
if (this.ExemplarFilter.HasValue)
503+
if (hasProgrammaticExemplarFilterValue)
496504
{
497505
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent(
498506
$"Exemplar filter configuration value '{configValue}' has been ignored because a value '{this.ExemplarFilter}' was set programmatically.");
499507
return;
500508
}
501509

502-
ExemplarFilterType? exemplarFilter;
510+
if (!TryParseExemplarFilterFromConfigurationValue(configValue, out var exemplarFilter))
511+
{
512+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter configuration was found but the value '{configValue}' is invalid and will be ignored.");
513+
return;
514+
}
515+
516+
this.ExemplarFilter = exemplarFilter;
517+
518+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter set to '{exemplarFilter}' from configuration.");
519+
}
520+
521+
if (configuration.TryGetStringValue(ExemplarFilterHistogramsConfigKey, out configValue))
522+
{
523+
if (hasProgrammaticExemplarFilterValue)
524+
{
525+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent(
526+
$"Exemplar filter histogram configuration value '{configValue}' has been ignored because a value '{this.ExemplarFilter}' was set programmatically.");
527+
return;
528+
}
529+
530+
if (!TryParseExemplarFilterFromConfigurationValue(configValue, out var exemplarFilter))
531+
{
532+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter histogram configuration was found but the value '{configValue}' is invalid and will be ignored.");
533+
return;
534+
}
535+
536+
this.ExemplarFilterForHistograms = exemplarFilter;
537+
538+
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter for histograms set to '{exemplarFilter}' from configuration.");
539+
}
540+
541+
static bool TryParseExemplarFilterFromConfigurationValue(string? configValue, out ExemplarFilterType? exemplarFilter)
542+
{
503543
if (string.Equals("always_off", configValue, StringComparison.OrdinalIgnoreCase))
504544
{
505545
exemplarFilter = ExemplarFilterType.AlwaysOff;
546+
return true;
506547
}
507-
else if (string.Equals("always_on", configValue, StringComparison.OrdinalIgnoreCase))
548+
549+
if (string.Equals("always_on", configValue, StringComparison.OrdinalIgnoreCase))
508550
{
509551
exemplarFilter = ExemplarFilterType.AlwaysOn;
552+
return true;
510553
}
511-
else if (string.Equals("trace_based", configValue, StringComparison.OrdinalIgnoreCase))
554+
555+
if (string.Equals("trace_based", configValue, StringComparison.OrdinalIgnoreCase))
512556
{
513557
exemplarFilter = ExemplarFilterType.TraceBased;
558+
return true;
514559
}
515-
else
516-
{
517-
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter configuration was found but the value '{configValue}' is invalid and will be ignored.");
518-
return;
519-
}
520-
521-
this.ExemplarFilter = exemplarFilter;
522560

523-
OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter set to '{exemplarFilter}' from configuration.");
561+
exemplarFilter = null;
562+
return false;
524563
}
525564
}
526565
}

src/OpenTelemetry/Metrics/Metric.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,7 @@ internal Metric(
125125
aggType = AggregationType.LongGauge;
126126
this.MetricType = MetricType.LongGauge;
127127
}
128-
else if (instrumentIdentity.InstrumentType == typeof(Histogram<long>)
129-
|| instrumentIdentity.InstrumentType == typeof(Histogram<int>)
130-
|| instrumentIdentity.InstrumentType == typeof(Histogram<short>)
131-
|| instrumentIdentity.InstrumentType == typeof(Histogram<byte>)
132-
|| instrumentIdentity.InstrumentType == typeof(Histogram<float>)
133-
|| instrumentIdentity.InstrumentType == typeof(Histogram<double>))
128+
else if (instrumentIdentity.IsHistogram)
134129
{
135130
var explicitBucketBounds = instrumentIdentity.HistogramBucketBounds;
136131
var exponentialMaxSize = instrumentIdentity.ExponentialHistogramMaxSize;

src/OpenTelemetry/Metrics/MetricReaderExt.cs

+21-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public abstract partial class MetricReader
2525
private bool emitOverflowAttribute;
2626
private bool reclaimUnusedMetricPoints;
2727
private ExemplarFilterType? exemplarFilter;
28+
private ExemplarFilterType? exemplarFilterForHistograms;
2829

2930
internal static void DeactivateMetric(Metric metric)
3031
{
@@ -54,6 +55,11 @@ internal virtual List<Metric> AddMetricWithNoViews(Instrument instrument)
5455
Debug.Assert(this.metrics != null, "this.metrics was null");
5556

5657
var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfiguration: null);
58+
59+
var exemplarFilter = metricStreamIdentity.IsHistogram
60+
? this.exemplarFilterForHistograms ?? this.exemplarFilter
61+
: this.exemplarFilter;
62+
5763
lock (this.instrumentCreationLock)
5864
{
5965
if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric))
@@ -72,7 +78,13 @@ internal virtual List<Metric> AddMetricWithNoViews(Instrument instrument)
7278
Metric? metric = null;
7379
try
7480
{
75-
metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, this.emitOverflowAttribute, this.reclaimUnusedMetricPoints, this.exemplarFilter);
81+
metric = new Metric(
82+
metricStreamIdentity,
83+
this.GetAggregationTemporality(metricStreamIdentity.InstrumentType),
84+
this.cardinalityLimit,
85+
this.emitOverflowAttribute,
86+
this.reclaimUnusedMetricPoints,
87+
exemplarFilter);
7688
}
7789
catch (NotSupportedException nse)
7890
{
@@ -114,6 +126,10 @@ internal virtual List<Metric> AddMetricWithViews(Instrument instrument, List<Met
114126
var metricStreamConfig = metricStreamConfigs[i];
115127
var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfig);
116128

129+
var exemplarFilter = metricStreamIdentity.IsHistogram
130+
? this.exemplarFilterForHistograms ?? this.exemplarFilter
131+
: this.exemplarFilter;
132+
117133
if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricStreamIdentity.InstrumentName))
118134
{
119135
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
@@ -150,7 +166,7 @@ internal virtual List<Metric> AddMetricWithViews(Instrument instrument, List<Met
150166
metricStreamConfig?.CardinalityLimit ?? this.cardinalityLimit,
151167
this.emitOverflowAttribute,
152168
this.reclaimUnusedMetricPoints,
153-
this.exemplarFilter,
169+
exemplarFilter,
154170
metricStreamConfig?.ExemplarReservoirFactory);
155171

156172
this.instrumentIdentityToMetric[metricStreamIdentity] = metric;
@@ -170,7 +186,8 @@ internal void ApplyParentProviderSettings(
170186
int cardinalityLimit,
171187
bool emitOverflowAttribute,
172188
bool reclaimUnusedMetricPoints,
173-
ExemplarFilterType? exemplarFilter)
189+
ExemplarFilterType? exemplarFilter,
190+
ExemplarFilterType? exemplarFilterForHistograms)
174191
{
175192
this.metricLimit = metricLimit;
176193
this.metrics = new Metric[metricLimit];
@@ -179,6 +196,7 @@ internal void ApplyParentProviderSettings(
179196
this.emitOverflowAttribute = emitOverflowAttribute;
180197
this.reclaimUnusedMetricPoints = reclaimUnusedMetricPoints;
181198
this.exemplarFilter = exemplarFilter;
199+
this.exemplarFilterForHistograms = exemplarFilterForHistograms;
182200
}
183201

184202
private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric)

src/OpenTelemetry/Metrics/MetricStreamIdentity.cs

+8
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me
115115

116116
public bool HistogramRecordMinMax { get; }
117117

118+
public bool IsHistogram =>
119+
this.InstrumentType == typeof(Histogram<long>)
120+
|| this.InstrumentType == typeof(Histogram<int>)
121+
|| this.InstrumentType == typeof(Histogram<short>)
122+
|| this.InstrumentType == typeof(Histogram<byte>)
123+
|| this.InstrumentType == typeof(Histogram<float>)
124+
|| this.InstrumentType == typeof(Histogram<double>);
125+
118126
public static bool operator ==(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => metricIdentity1.Equals(metricIdentity2);
119127

120128
public static bool operator !=(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2);

0 commit comments

Comments
 (0)