From 2abf2a9e71c03a667d8ead00136c472ec36eb388 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 20 May 2024 09:31:55 -0700 Subject: [PATCH] [docs-metrics] Customer ExemplarReservoir and View API configuration (#5624) --- .../diagnostics/experimental-apis/OTEL1004.md | 8 +- docs/metrics/customizing-the-sdk/README.md | 40 +++++-- docs/metrics/extending-the-sdk/README.md | 100 +++++++++++++++++- 3 files changed, 132 insertions(+), 16 deletions(-) diff --git a/docs/diagnostics/experimental-apis/OTEL1004.md b/docs/diagnostics/experimental-apis/OTEL1004.md index 543d9437ba..a3876df74b 100644 --- a/docs/diagnostics/experimental-apis/OTEL1004.md +++ b/docs/diagnostics/experimental-apis/OTEL1004.md @@ -54,9 +54,9 @@ We are exposing these APIs experimentally for the following reasons: **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. - +Please provide feedback on [this +issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5629) if +you need stable support for custom `ExemplarReservoir`s. The feedback will help +inform decisions about what to expose stable and when. diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index 7ed1a73855..830ad66957 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -239,7 +239,7 @@ within the maximum number of buckets defined by `MaxSize`. The default `MaxSize` is 160 buckets and the default `MaxScale` is 20. ```csharp - // Change the maximum number of buckets + // Change the maximum number of buckets for "MyHistogram" .AddView( instrumentName: "MyHistogram", new Base2ExponentialBucketHistogramConfiguration { MaxSize = 40 }) @@ -251,6 +251,28 @@ by using Views. See [Program.cs](./Program.cs) for a complete example. +#### Change the ExemplarReservoir + +> [!NOTE] +> `MetricStreamConfiguration.ExemplarReservoirFactory` is an experimental API only + available in pre-release builds. For details see: + [OTEL1004](../../diagnostics/experimental-apis/OTEL1004.md). + +To set the [ExemplarReservoir](#exemplarreservoir) for an instrument, use the +`MetricStreamConfiguration.ExemplarReservoirFactory` property on the View API: + +> [!IMPORTANT] +> Setting `MetricStreamConfiguration.ExemplarReservoirFactory` alone will NOT + enable `Exemplar`s for an instrument. An [ExemplarFilter](#exemplarfilter) + MUST also be used. + +```csharp + // Use MyCustomExemplarReservoir for "MyFruitCounter" + .AddView( + instrumentName: "MyFruitCounter", + new MetricStreamConfiguration { ExemplarReservoirFactory = () => new MyCustomExemplarReservoir() }) +``` + ### Changing maximum Metric Streams Every instrument results in the creation of a single Metric stream. With Views, @@ -421,20 +443,24 @@ and is responsible for recording `Exemplar`s. The following are the default reservoirs: * `AlignedHistogramBucketExemplarReservoir` is the default reservoir used for -Histograms with buckets, and it stores at most one exemplar per histogram -bucket. The exemplar stored is the last measurement recorded - i.e. any new +Histograms with buckets, and it stores at most one `Exemplar` per histogram +bucket. The `Exemplar` stored is the last measurement recorded - i.e. any new measurement overwrites the previous one in that bucket. * `SimpleFixedSizeExemplarReservoir` is the default reservoir used for all -metrics except Histograms with buckets. It has a fixed reservoir pool, and +metrics except histograms with buckets. It has a fixed reservoir pool, and implements the equivalent of [naive reservoir](https://en.wikipedia.org/wiki/Reservoir_sampling). The reservoir pool -size (currently defaulting to 1) determines the maximum number of exemplars +size (currently defaulting to 1) determines the maximum number of `Exemplar`s stored. Exponential histograms use a `SimpleFixedSizeExemplarReservoir` with a pool size equal to the number of buckets up to a max of `20`. -> [!NOTE] -> Currently there is no ability to change or configure `ExemplarReservoir`. +See [Change the ExemplarReservoir](#change-the-exemplarreservoir) for details on +how to use the View API to change `ExemplarReservoir`s for an instrument. + +See [Building your own +ExemplarReservoir](../extending-the-sdk/README.md#exemplarreservoir) for details +on how to implement custom `ExemplarReservoir`s. ### Instrumentation diff --git a/docs/metrics/extending-the-sdk/README.md b/docs/metrics/extending-the-sdk/README.md index c7293ac418..bf32c3368f 100644 --- a/docs/metrics/extending-the-sdk/README.md +++ b/docs/metrics/extending-the-sdk/README.md @@ -2,7 +2,6 @@ * [Building your own exporter](#exporter) * [Building your own reader](#reader) -* [Building your own exemplar filter](#exemplarfilter) * [Building your own exemplar reservoir](#exemplarreservoir) * [Building your own resource detector](../../resources/README.md#resource-detector) * [References](#references) @@ -72,12 +71,103 @@ to the `MeterProvider` as shown in the example [here](./Program.cs). Not supported. -## ExemplarFilter +## ExemplarReservoir -Not supported. +> [!NOTE] +> `ExemplarReservoir` is an experimental API only available in pre-release + builds. For details see: + [OTEL1004](../../diagnostics/experimental-apis/OTEL1004.md). Please [provide + feedback](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5629) + to help inform decisions about what should be exposed stable and when. -## ExemplarReservoir +Custom [ExemplarReservoir](../customizing-the-sdk/README.md#exemplarreservoir)s +can be implemented to control how `Exemplar`s are recorded for a metric: -Not supported. +* `ExemplarReservoir`s should derive from `FixedSizeExemplarReservoir` (which + belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package) + and implement the `Offer` methods. +* The `FixedSizeExemplarReservoir` constructor accepts a `capacity` parameter to + control the number of `Exemplar`s which may be recorded by the + `ExemplarReservoir`. +* The `virtual` `OnCollected` method is called after the `ExemplarReservoir` + collection operation has completed and may be used to implement cleanup or + reset logic. +* The `bool` `ResetOnCollect` property on `ExemplarReservoir` is set to `true` + when delta aggregation temporality is used for the metric using the + `ExemplarReservoir`. +* The `Offer` and `Collect` `ExemplarReservoir` methods are called concurrently + by the OpenTelemetry SDK. As such any state required by custom + `ExemplarReservoir` implementations needs to be managed using appropriate + thread-safety/concurrency mechanisms (`lock`, `Interlocked`, etc.). +* Custom `ExemplarReservoir` implementations MUST NOT throw exceptions. + Exceptions thrown in custom implementations MAY lead to unreleased locks and + undefined behaviors. + +The following example demonstrates a custom `ExemplarReservoir` implementation +which records `Exemplar`s for measurements which have the highest value. When +delta aggregation temporality is used the recorded `Exemplar` will be the +highest value for a given collection cycle. When cumulative aggregation +temporality is used the recorded `Exemplar` will be the highest value for the +lifetime of the process. + +```csharp +class HighestValueExemplarReservoir : FixedSizeExemplarReservoir +{ + private readonly object lockObject = new(); + private long? previousValueLong; + private double? previousValueDouble; + + public HighestValueExemplarReservoir() + : base(capacity: 1) + { + } + + public override void Offer(in ExemplarMeasurement measurement) + { + if (!this.previousValueLong.HasValue || measurement.Value > this.previousValueLong.Value) + { + lock (this.lockObject) + { + if (!this.previousValueLong.HasValue || measurement.Value > this.previousValueLong.Value) + { + this.UpdateExemplar(0, in measurement); + this.previousValueLong = measurement.Value; + } + } + } + } + + public override void Offer(in ExemplarMeasurement measurement) + { + if (!this.previousValueDouble.HasValue || measurement.Value > this.previousValueDouble.Value) + { + lock (this.lockObject) + { + if (!this.previousValueDouble.HasValue || measurement.Value > this.previousValueDouble.Value) + { + this.UpdateExemplar(0, in measurement); + this.previousValueDouble = measurement.Value; + } + } + } + } + + protected override void OnCollected() + { + if (this.ResetOnCollect) + { + lock (this.lockObject) + { + this.previousValueLong = null; + this.previousValueDouble = null; + } + } + } +} +``` + +Custom [ExemplarReservoir](../customizing-the-sdk/README.md#exemplarreservoir)s +can be configured using the View API. For details see: [Change the +ExemplarReservoir](../customizing-the-sdk/README.md#change-the-exemplarreservoir). ## References