[sdk-metrics] Obsolete SetMaxMetricPointsPerMetricStream + standarize on "Cardinality Limit" name (#5328)

Co-authored-by: Yun-Ting Lin <yunl@microsoft.com>
This commit is contained in:
Mikel Blanchard 2024-02-09 11:06:14 -08:00 committed by GitHub
parent f214d27e93
commit cf00e4254e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 190 additions and 232 deletions

View File

@ -11,19 +11,37 @@ Experimental APIs may be changed or removed in the future.
## Details
The OpenTelemetry Specification defines the
[cardinality limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits)
of a metric can be set by the matching view.
From the specification:
> The cardinality limit for an aggregation is defined in one of three ways:
> A view with criteria matching the instrument an aggregation is created for has
> an aggregation_cardinality_limit value defined for the stream, that value
> SHOULD be used. If there is no matching view, but the MetricReader defines a
> default cardinality limit value based on the instrument an aggregation is
> created for, that value SHOULD be used. If none of the previous values are
> defined, the default value of 2000 SHOULD be used.
>
> 1. A view with criteria matching the instrument an aggregation is created for
> has an `aggregation_cardinality_limit` value defined for the stream, that
> value SHOULD be used.
> 2. If there is no matching view, but the `MetricReader` defines a default
> cardinality limit value based on the instrument an aggregation is created
> for, that value SHOULD be used.
> 3. If none of the previous values are defined, the default value of 2000
> SHOULD be used.
We are exposing these APIs experimentally until the specification declares them
stable.
### Setting cardinality limit for a specific Metric via the View API
The OpenTelemetry Specification defines the [cardinality
limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits)
of a metric can be set by the matching view.
```csharp
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddView(
instrumentName: "MyFruitCounter",
new MetricStreamConfiguration { CardinalityLimit = 10 })
.Build();
```
### Setting cardinality limit for a specific MetricReader
[This is not currently supported by OpenTelemetry
.NET.](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5331)

View File

@ -379,17 +379,19 @@ predictable and reliable behavior when excessive cardinality happens, whether it
was due to a malicious attack or developer making mistakes while writing code.
OpenTelemetry has a default cardinality limit of `2000` per metric. This limit
can be configured at `MeterProvider` level using the
`SetMaxMetricPointsPerMetricStream` method, or at individual
[view](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view)
level using `MetricStreamConfiguration.CardinalityLimit`. Refer to this
[doc](../../docs/metrics/customizing-the-sdk/README.md#changing-maximum-metricpoints-per-metricstream)
can be configured at the individual metric level using the [View
API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view)
and the `MetricStreamConfiguration.CardinalityLimit` setting. Refer to this
[doc](../../docs/metrics/customizing-the-sdk/README.md#changing-the-cardinality-limit-for-a-metric)
for more information.
Given a metric, once the cardinality limit is reached, any new measurement which
cannot be independently aggregated because of the limit will be aggregated using
the [overflow
attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute).
cannot be independently aggregated because of the limit will be dropped or
aggregated using the [overflow
attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute)
(if enabled). When NOT using the overflow attribute feature a warning is written
to the [self-diagnostic log](../../src/OpenTelemetry/README.md#self-diagnostics)
the first time an overflow is detected for a given metric.
> [!NOTE]
> Overflow attribute was introduced in OpenTelemetry .NET

View File

@ -367,90 +367,24 @@ MyFruitCounter.Add(1, new("name", "apple"), new("color", "red"));
AnotherFruitCounter.Add(1, new("name", "apple"), new("color", "red"));
```
### Changing maximum MetricPoints per MetricStream
### Changing the cardinality limit for a Metric
A Metric stream can contain as many Metric points as the number of unique
combination of keys and values. To protect the SDK from unbounded memory usage,
SDK limits the maximum number of metric points per metric stream, to a default
of 2000. Once the limit is hit, any new key/value combination for that metric is
ignored. The SDK chooses the key/value combinations in the order in which they
are emitted. `SetMaxMetricPointsPerMetricStream` can be used to override the
default.
To set the [cardinality limit](../README.md#cardinality-limits) for an
individual metric, use `MetricStreamConfiguration.CardinalityLimit` setting on
the View API:
> [!NOTE]
> One `MetricPoint` is reserved for every `MetricStream` for the
special case where there is no key/value pair associated with the metric. The
maximum number of `MetricPoint`s has to accommodate for this special case.
Consider the below example. Here we set the maximum number of `MetricPoint`s
allowed to be `3`. This means that for every `MetricStream`, the SDK will export
measurements for up to `3` distinct key/value combinations of the metric. There
are two instruments published here: `MyFruitCounter` and `AnotherFruitCounter`.
There are two total `MetricStream`s created one for each of these instruments.
SDK will limit the maximum number of distinct key/value combinations for each of
these `MetricStream`s to `3`.
```csharp
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using OpenTelemetry;
using OpenTelemetry.Metrics;
Counter<long> MyFruitCounter = MyMeter.CreateCounter<long>("MyFruitCounter");
Counter<long> AnotherFruitCounter = MyMeter.CreateCounter<long>("AnotherFruitCounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("*")
.AddConsoleExporter()
.SetMaxMetricPointsPerMetricStream(3) // The default value is 2000
.Build();
// There are four distinct key/value combinations emitted for `MyFruitCounter`:
// 1. No key/value pair
// 2. (name:apple, color:red)
// 3. (name:lemon, color:yellow)
// 4. (name:apple, color:green)
// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations:
// 1. No key/value pair
// 2. (name:apple, color:red)
// 3. (name:lemon, color:yellow)
MyFruitCounter.Add(1); // Exported (No key/value pair)
MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); // Exported
MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); // Exported
MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); // Exported
MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); // Not exported
MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); // Exported
MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); // Exported
// There are four distinct key/value combinations emitted for `AnotherFruitCounter`:
// 1. (name:kiwi)
// 2. (name:banana, color:yellow)
// 3. (name:mango, color:yellow)
// 4. (name:banana, color:green)
// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations:
// 1. No key/value pair (This is a special case. The SDK reserves a `MetricPoint` for it even if it's not explicitly emitted.)
// 2. (name:kiwi)
// 3. (name:banana, color:yellow)
AnotherFruitCounter.Add(4, new KeyValuePair<string, object>("name", "kiwi")); // Exported
AnotherFruitCounter.Add(1, new("name", "banana"), new("color", "yellow")); // Exported
AnotherFruitCounter.Add(2, new("name", "mango"), new("color", "yellow")); // Not exported
AnotherFruitCounter.Add(1, new("name", "mango"), new("color", "yellow")); // Not exported
AnotherFruitCounter.Add(2, new("name", "banana"), new("color", "green")); // Not exported
AnotherFruitCounter.Add(5, new("name", "banana"), new("color", "yellow")); // Exported
AnotherFruitCounter.Add(4, new("name", "mango"), new("color", "yellow")); // Not exported
```
To set the [cardinality limit](../README.md#cardinality-limits) at individual
metric level, use `MetricStreamConfiguration.CardinalityLimit`:
> `MetricStreamConfiguration.CardinalityLimit` is an experimental API only
available in pre-release builds. For details see:
[OTEL1003](../../diagnostics/experimental-apis/OTEL1003.md).
```csharp
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("MyCompany.MyProduct.MyLibrary")
.AddView(instrumentName: "MyFruitCounter", new MetricStreamConfiguration { CardinalityLimit = 10 })
// Set a custom CardinalityLimit (10) for "MyFruitCounter"
.AddView(
instrumentName: "MyFruitCounter",
new MetricStreamConfiguration { CardinalityLimit = 10 })
.AddConsoleExporter()
.Build();
```

View File

@ -21,8 +21,12 @@
* **Experimental (pre-release builds only):** Added support for setting
`CardinalityLimit` (the maximum number of data points allowed for a metric)
when configuring a view.
([#5312](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5312))
when configuring a view (applies to individual metrics) and obsoleted
`MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream` (previously
applied to all metrics). The default cardinality limit for metrics remains at
`2000`.
([#5312](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5312),
[#5328](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5328))
* Updated `LogRecord` to keep `CategoryName` and `Logger` in sync when using the
experimental Log Bridge API.

View File

@ -13,6 +13,8 @@ internal sealed class AggregatorStore
{
internal readonly bool OutputDelta;
internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled;
internal readonly int CardinalityLimit;
internal readonly bool EmitOverflowAttribute;
internal long DroppedMeasurements = 0;
private static readonly string MetricPointCapHitFixMessage = "Consider opting in for the experimental SDK feature to emit all the throttled metrics under the overflow attribute by setting env variable OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE = true. You could also modify instrumentation to reduce the number of unique key/value pair combinations. Or use Views to drop unwanted tags. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit.";
@ -42,8 +44,6 @@ internal sealed class AggregatorStore
private readonly int exponentialHistogramMaxScale;
private readonly UpdateLongDelegate updateLongCallback;
private readonly UpdateDoubleDelegate updateDoubleCallback;
private readonly int maxMetricPoints;
private readonly bool emitOverflowAttribute;
private readonly ExemplarFilter exemplarFilter;
private readonly Func<KeyValuePair<string, object?>[], int, int> lookupAggregatorStore;
@ -57,17 +57,17 @@ internal sealed class AggregatorStore
MetricStreamIdentity metricStreamIdentity,
AggregationType aggType,
AggregationTemporality temporality,
int maxMetricPoints,
int cardinalityLimit,
bool emitOverflowAttribute,
bool shouldReclaimUnusedMetricPoints,
ExemplarFilter? exemplarFilter = null)
{
this.name = metricStreamIdentity.InstrumentName;
this.maxMetricPoints = maxMetricPoints;
this.CardinalityLimit = cardinalityLimit;
this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.maxMetricPoints}";
this.metricPoints = new MetricPoint[maxMetricPoints];
this.currentMetricPointBatch = new int[maxMetricPoints];
this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.CardinalityLimit}";
this.metricPoints = new MetricPoint[cardinalityLimit];
this.currentMetricPointBatch = new int[cardinalityLimit];
this.aggType = aggType;
this.OutputDelta = temporality == AggregationTemporality.Delta;
this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity);
@ -89,7 +89,7 @@ internal sealed class AggregatorStore
this.tagsKeysInterestingCount = hs.Count;
}
this.emitOverflowAttribute = emitOverflowAttribute;
this.EmitOverflowAttribute = emitOverflowAttribute;
var reservedMetricPointsCount = 1;
@ -105,17 +105,17 @@ internal sealed class AggregatorStore
if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled)
{
this.availableMetricPoints = new Queue<int>(maxMetricPoints - reservedMetricPointsCount);
this.availableMetricPoints = new Queue<int>(cardinalityLimit - reservedMetricPointsCount);
// There is no overload which only takes capacity as the parameter
// Using the DefaultConcurrencyLevel defined in the ConcurrentDictionary class: https://github.com/dotnet/runtime/blob/v7.0.5/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L2020
// We expect at the most (maxMetricPoints - reservedMetricPointsCount) * 2 entries- one for sorted and one for unsorted input
this.tagsToMetricPointIndexDictionaryDelta =
new ConcurrentDictionary<Tags, LookupData>(concurrencyLevel: Environment.ProcessorCount, capacity: (maxMetricPoints - reservedMetricPointsCount) * 2);
new ConcurrentDictionary<Tags, LookupData>(concurrencyLevel: Environment.ProcessorCount, capacity: (cardinalityLimit - reservedMetricPointsCount) * 2);
// Add all the indices except for the reserved ones to the queue so that threads have
// readily available access to these MetricPoints for their use.
for (int i = reservedMetricPointsCount; i < this.maxMetricPoints; i++)
for (int i = reservedMetricPointsCount; i < this.CardinalityLimit; i++)
{
this.availableMetricPoints.Enqueue(i);
}
@ -164,12 +164,12 @@ internal sealed class AggregatorStore
}
else if (this.OutputDelta)
{
var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1);
var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1);
this.SnapshotDelta(indexSnapshot);
}
else
{
var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1);
var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1);
this.SnapshotCumulative(indexSnapshot);
}
@ -227,7 +227,7 @@ internal sealed class AggregatorStore
int startIndexForReclaimableMetricPoints = 1;
if (this.emitOverflowAttribute)
if (this.EmitOverflowAttribute)
{
startIndexForReclaimableMetricPoints = 2; // Index 0 and 1 are reserved for no tags and overflow
@ -249,7 +249,7 @@ internal sealed class AggregatorStore
}
}
for (int i = startIndexForReclaimableMetricPoints; i < this.maxMetricPoints; i++)
for (int i = startIndexForReclaimableMetricPoints; i < this.CardinalityLimit; i++)
{
ref var metricPoint = ref this.metricPoints[i];
@ -440,7 +440,7 @@ internal sealed class AggregatorStore
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
{
aggregatorIndex = this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
if (aggregatorIndex >= this.CardinalityLimit)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
@ -469,7 +469,7 @@ internal sealed class AggregatorStore
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
{
aggregatorIndex = ++this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
if (aggregatorIndex >= this.CardinalityLimit)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
@ -496,7 +496,7 @@ internal sealed class AggregatorStore
{
// This else block is for tag length = 1
aggregatorIndex = this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
if (aggregatorIndex >= this.CardinalityLimit)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
@ -518,7 +518,7 @@ internal sealed class AggregatorStore
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex))
{
aggregatorIndex = ++this.metricPointIndex;
if (aggregatorIndex >= this.maxMetricPoints)
if (aggregatorIndex >= this.CardinalityLimit)
{
// sorry! out of data points.
// TODO: Once we support cleanup of
@ -929,7 +929,7 @@ internal sealed class AggregatorStore
{
Interlocked.Increment(ref this.DroppedMeasurements);
if (this.emitOverflowAttribute)
if (this.EmitOverflowAttribute)
{
this.InitializeOverflowTagPointIfNotInitialized();
this.metricPoints[1].Update(value);
@ -973,7 +973,7 @@ internal sealed class AggregatorStore
{
Interlocked.Increment(ref this.DroppedMeasurements);
if (this.emitOverflowAttribute)
if (this.EmitOverflowAttribute)
{
this.InitializeOverflowTagPointIfNotInitialized();
this.metricPoints[1].Update(value);
@ -1017,7 +1017,7 @@ internal sealed class AggregatorStore
{
Interlocked.Increment(ref this.DroppedMeasurements);
if (this.emitOverflowAttribute)
if (this.EmitOverflowAttribute)
{
this.InitializeOverflowTagPointIfNotInitialized();
this.metricPoints[1].Update(value);
@ -1061,7 +1061,7 @@ internal sealed class AggregatorStore
{
Interlocked.Increment(ref this.DroppedMeasurements);
if (this.emitOverflowAttribute)
if (this.EmitOverflowAttribute)
{
this.InitializeOverflowTagPointIfNotInitialized();
this.metricPoints[1].Update(value);

View File

@ -218,7 +218,7 @@ public static class MeterProviderBuilderExtensions
{
if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
{
meterProviderBuilderSdk.SetMaxMetricStreams(maxMetricStreams);
meterProviderBuilderSdk.SetMetricLimit(maxMetricStreams);
}
});
@ -238,6 +238,9 @@ public static class MeterProviderBuilderExtensions
/// <param name="meterProviderBuilder"><see cref="MeterProviderBuilder"/>.</param>
/// <param name="maxMetricPointsPerMetricStream">Maximum number of metric points allowed per metric stream.</param>
/// <returns>The supplied <see cref="MeterProviderBuilder"/> for chaining.</returns>
#if EXPOSE_EXPERIMENTAL_FEATURES
[Obsolete("Use MetricStreamConfiguration.CardinalityLimit via the AddView API instead. This method will be removed in a future version.")]
#endif
public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream)
{
Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1);
@ -246,7 +249,7 @@ public static class MeterProviderBuilderExtensions
{
if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
{
meterProviderBuilderSdk.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream);
meterProviderBuilderSdk.SetDefaultCardinalityLimit(maxMetricPointsPerMetricStream);
}
});

View File

@ -15,8 +15,8 @@ namespace OpenTelemetry.Metrics;
/// </summary>
internal sealed class MeterProviderBuilderSdk : MeterProviderBuilder, IMeterProviderBuilder
{
public const int MaxMetricsDefault = 1000;
public const int MaxMetricPointsPerMetricDefault = 2000;
public const int DefaultMetricLimit = 1000;
public const int DefaultCardinalityLimit = 2000;
private const string DefaultInstrumentationVersion = "1.0.0.0";
private readonly IServiceProvider serviceProvider;
@ -49,9 +49,9 @@ internal sealed class MeterProviderBuilderSdk : MeterProviderBuilder, IMeterProv
public List<Func<Instrument, MetricStreamConfiguration?>> ViewConfigs { get; } = new();
public int MaxMetricStreams { get; private set; } = MaxMetricsDefault;
public int MetricLimit { get; private set; } = DefaultMetricLimit;
public int MaxMetricPointsPerMetricStream { get; private set; } = MaxMetricPointsPerMetricDefault;
public int CardinalityLimit { get; private set; } = DefaultCardinalityLimit;
/// <summary>
/// Returns whether the given instrument name is valid according to the specification.
@ -186,16 +186,16 @@ internal sealed class MeterProviderBuilderSdk : MeterProviderBuilder, IMeterProv
return this;
}
public MeterProviderBuilder SetMaxMetricStreams(int maxMetricStreams)
public MeterProviderBuilder SetMetricLimit(int metricLimit)
{
this.MaxMetricStreams = maxMetricStreams;
this.MetricLimit = metricLimit;
return this;
}
public MeterProviderBuilder SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream)
public MeterProviderBuilder SetDefaultCardinalityLimit(int cardinalityLimit)
{
this.MaxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream;
this.CardinalityLimit = cardinalityLimit;
return this;
}

View File

@ -76,9 +76,12 @@ internal sealed class MeterProviderSdk : MeterProvider
Guard.ThrowIfNull(reader);
reader.SetParentProvider(this);
reader.SetMaxMetricStreams(state.MaxMetricStreams);
reader.SetMaxMetricPointsPerMetricStream(state.MaxMetricPointsPerMetricStream, isEmitOverflowAttributeKeySet);
reader.SetExemplarFilter(state.ExemplarFilter);
reader.ApplyParentProviderSettings(
state.MetricLimit,
state.CardinalityLimit,
state.ExemplarFilter,
isEmitOverflowAttributeKeySet);
if (this.reader == null)
{

View File

@ -41,12 +41,12 @@ public sealed class Metric
("System.Net.Http", "http.client.connection.duration"),
};
private readonly AggregatorStore aggStore;
internal readonly AggregatorStore AggregatorStore;
internal Metric(
MetricStreamIdentity instrumentIdentity,
AggregationTemporality temporality,
int maxMetricPointsPerMetricStream,
int cardinalityLimit,
bool emitOverflowAttribute,
bool shouldReclaimUnusedMetricPoints,
ExemplarFilter? exemplarFilter = null)
@ -155,7 +155,7 @@ public sealed class Metric
throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}");
}
this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter);
this.AggregatorStore = new AggregatorStore(instrumentIdentity, aggType, temporality, cardinalityLimit, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter);
this.Temporality = temporality;
}
@ -211,14 +211,14 @@ public sealed class Metric
/// </summary>
/// <returns><see cref="MetricPointsAccessor"/>.</returns>
public MetricPointsAccessor GetMetricPoints()
=> this.aggStore.GetMetricPoints();
=> this.AggregatorStore.GetMetricPoints();
internal void UpdateLong(long value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
=> this.aggStore.Update(value, tags);
=> this.AggregatorStore.Update(value, tags);
internal void UpdateDouble(double value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
=> this.aggStore.Update(value, tags);
=> this.AggregatorStore.Update(value, tags);
internal int Snapshot()
=> this.aggStore.Snapshot();
=> this.AggregatorStore.Snapshot();
}

View File

@ -17,8 +17,8 @@ public abstract partial class MetricReader
private readonly HashSet<string> metricStreamNames = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<MetricStreamIdentity, Metric> instrumentIdentityToMetric = new();
private readonly object instrumentCreationLock = new();
private int maxMetricStreams;
private int maxMetricPointsPerMetricStream;
private int metricLimit;
private int cardinalityLimit;
private Metric?[]? metrics;
private Metric[]? metricsCurrentBatch;
private int metricIndex = -1;
@ -44,7 +44,7 @@ public abstract partial class MetricReader
}
var index = ++this.metricIndex;
if (index >= this.maxMetricStreams)
if (index >= this.metricLimit)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit.");
return null;
@ -55,7 +55,7 @@ public abstract partial class MetricReader
try
{
bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints;
metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter);
metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter);
}
catch (NotSupportedException nse)
{
@ -129,7 +129,7 @@ public abstract partial class MetricReader
}
var index = ++this.metricIndex;
if (index >= this.maxMetricStreams)
if (index >= this.metricLimit)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit.");
}
@ -137,12 +137,14 @@ public abstract partial class MetricReader
{
bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints;
var cardinalityLimit = this.cardinalityLimit;
if (metricStreamConfig != null && metricStreamConfig.CardinalityLimit != null)
{
this.maxMetricPointsPerMetricStream = metricStreamConfig.CardinalityLimit.Value;
cardinalityLimit = metricStreamConfig.CardinalityLimit.Value;
}
Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter);
Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), cardinalityLimit, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter);
this.instrumentIdentityToMetric[metricStreamIdentity] = metric;
this.metrics![index] = metric;
@ -205,26 +207,22 @@ public abstract partial class MetricReader
}
}
internal void SetMaxMetricStreams(int maxMetricStreams)
{
this.maxMetricStreams = maxMetricStreams;
this.metrics = new Metric[maxMetricStreams];
this.metricsCurrentBatch = new Metric[maxMetricStreams];
}
internal void SetExemplarFilter(ExemplarFilter? exemplarFilter)
internal void ApplyParentProviderSettings(
int metricLimit,
int cardinalityLimit,
ExemplarFilter? exemplarFilter,
bool isEmitOverflowAttributeKeySet)
{
this.metricLimit = metricLimit;
this.metrics = new Metric[metricLimit];
this.metricsCurrentBatch = new Metric[metricLimit];
this.cardinalityLimit = cardinalityLimit;
this.exemplarFilter = exemplarFilter;
}
internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream, bool isEmitOverflowAttributeKeySet)
{
this.maxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream;
if (isEmitOverflowAttributeKeySet)
{
// We need at least two metric points. One is reserved for zero tags and the other one for overflow attribute
if (maxMetricPointsPerMetricStream > 1)
if (cardinalityLimit > 1)
{
this.emitOverflowAttribute = true;
}
@ -273,7 +271,7 @@ public abstract partial class MetricReader
try
{
var indexSnapshot = Math.Min(this.metricIndex, this.maxMetricStreams - 1);
var indexSnapshot = Math.Min(this.metricIndex, this.metricLimit - 1);
var target = indexSnapshot + 1;
int metricCountCurrentBatch = 0;
for (int i = 0; i < target; i++)

View File

@ -109,10 +109,8 @@ public class MetricStreamConfiguration
/// <para>Spec reference: <see
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits">Cardinality
/// limits</see>.</para>
/// Note: If not set, the MeterProvider cardinality limit value will be
/// used, which defaults to 2000. Call <see
/// cref="MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream"/>
/// to configure the MeterProvider default.
/// Note: If not set the default MeterProvider cardinality limit of 2000
/// will apply.
/// </remarks>
#if NET8_0_OR_GREATER
[Experimental(DiagnosticDefinitions.CardinalityLimitExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]

View File

@ -255,7 +255,7 @@ public abstract class AggregatorTestsBase
metricStreamIdentity,
AggregationType.Histogram,
AggregationTemporality.Cumulative,
maxMetricPoints: 1024,
cardinalityLimit: 1024,
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints);
@ -332,7 +332,7 @@ public abstract class AggregatorTestsBase
metricStreamIdentity,
aggregationType,
aggregationTemporality,
maxMetricPoints: 1024,
cardinalityLimit: 1024,
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints,
exemplarsEnabled ? new AlwaysOnExemplarFilter() : null);
@ -442,7 +442,7 @@ public abstract class AggregatorTestsBase
metricStreamIdentity,
AggregationType.Base2ExponentialHistogram,
AggregationTemporality.Cumulative,
maxMetricPoints: 1024,
cardinalityLimit: 1024,
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints);

View File

@ -1423,26 +1423,26 @@ public abstract class MetricApiTestsBase : MetricTestsBase
// for no tag point!
// This may be changed later.
counterLong.Add(10);
for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++)
for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++)
{
counterLong.Add(10, new KeyValuePair<string, object>("key", "value" + i));
}
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount());
Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount());
exportedItems.Clear();
counterLong.Add(10);
for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++)
for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++)
{
counterLong.Add(10, new KeyValuePair<string, object>("key", "value" + i));
}
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount());
Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount());
counterLong.Add(10);
for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++)
for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++)
{
counterLong.Add(10, new KeyValuePair<string, object>("key", "value" + i));
}
@ -1453,7 +1453,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase
counterLong.Add(10, new KeyValuePair<string, object>("key", "valueC"));
exportedItems.Clear();
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount());
Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount());
}
[Fact]

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Metrics;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Tests;
@ -66,12 +65,7 @@ public abstract class MetricOverflowAttributeTestsBase
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore;
var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore);
Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute);
Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.EmitOverflowAttribute);
}
[Theory]
@ -106,12 +100,7 @@ public abstract class MetricOverflowAttributeTestsBase
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore;
var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore);
Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute);
Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.EmitOverflowAttribute);
}
[Theory]
@ -140,12 +129,7 @@ public abstract class MetricOverflowAttributeTestsBase
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore;
var emitOverflowAttribute = (bool)typeof(AggregatorStore).GetField("emitOverflowAttribute", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore);
Assert.Equal(isEmitOverflowAttributeKeySet, emitOverflowAttribute);
Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.EmitOverflowAttribute);
}
[Theory]
@ -174,7 +158,7 @@ public abstract class MetricOverflowAttributeTestsBase
counter.Add(10); // Record measurement for zero tags
// Max number for MetricPoints available for use when emitted with tags
int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2;
int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit - 2;
for (int i = 0; i < maxMetricPointsForUse; i++)
{
@ -325,7 +309,7 @@ public abstract class MetricOverflowAttributeTestsBase
histogram.Record(10); // Record measurement for zero tags
// Max number for MetricPoints available for use when emitted with tags
int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2;
int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit - 2;
for (int i = 0; i < maxMetricPointsForUse; i++)
{

View File

@ -286,17 +286,12 @@ public abstract class MetricPointReclaimTestsBase
private readonly bool assertNoDroppedMeasurements;
private readonly FieldInfo aggStoreFieldInfo;
private readonly FieldInfo metricPointLookupDictionaryFieldInfo;
public CustomExporter(bool assertNoDroppedMeasurements)
{
this.assertNoDroppedMeasurements = assertNoDroppedMeasurements;
var metricFields = typeof(Metric).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
this.aggStoreFieldInfo = metricFields!.FirstOrDefault(field => field.Name == "aggStore");
var aggregatorStoreFields = typeof(AggregatorStore).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
this.metricPointLookupDictionaryFieldInfo = aggregatorStoreFields!.FirstOrDefault(field => field.Name == "tagsToMetricPointIndexDictionaryDelta");
}
@ -305,7 +300,7 @@ public abstract class MetricPointReclaimTestsBase
{
foreach (var metric in batch)
{
var aggStore = this.aggStoreFieldInfo.GetValue(metric) as AggregatorStore;
var aggStore = metric.AggregatorStore;
var metricPointLookupDictionary = this.metricPointLookupDictionaryFieldInfo.GetValue(aggStore) as ConcurrentDictionary<Tags, LookupData>;
var droppedMeasurements = aggStore.DroppedMeasurements;
@ -316,7 +311,7 @@ public abstract class MetricPointReclaimTestsBase
}
// This is to ensure that the lookup dictionary does not have unbounded growth
Assert.True(metricPointLookupDictionary.Count <= (MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault * 2));
Assert.True(metricPointLookupDictionary.Count <= (MeterProviderBuilderSdk.DefaultCardinalityLimit * 2));
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Metrics;
using System.Reflection;
using OpenTelemetry.Internal;
using OpenTelemetry.Tests;
using Xunit;
@ -920,32 +919,61 @@ public class MetricViewTests : MetricTestsBase
Assert.Equal(10, metricPoint2.GetSumLong());
}
[Fact]
public void CardinalityLimitofMatchingViewTakesPrecedenceOverMetricProviderWhenBothWereSet()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CardinalityLimitofMatchingViewTakesPrecedenceOverMeterProvider(bool setDefault)
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
.AddMeter(meter.Name)
.SetMaxMetricPointsPerMetricStream(3)
.AddView((instrument) =>
using var container = this.BuildMeterProvider(out var meterProvider, builder =>
{
if (setDefault)
{
return new MetricStreamConfiguration() { Name = "MetricStreamA", CardinalityLimit = 10000 };
})
.AddInMemoryExporter(exportedItems));
#pragma warning disable CS0618 // Type or member is obsolete
builder.SetMaxMetricPointsPerMetricStream(3);
#pragma warning restore CS0618 // Type or member is obsolete
}
var counter = meter.CreateCounter<long>("counter");
counter.Add(100);
builder
.AddMeter(meter.Name)
.AddView((instrument) =>
{
if (instrument.Name == "counter2")
{
return new MetricStreamConfiguration() { Name = "MetricStreamA", CardinalityLimit = 10000 };
}
return null;
})
.AddInMemoryExporter(exportedItems);
});
var counter1 = meter.CreateCounter<long>("counter1");
counter1.Add(100);
var counter2 = meter.CreateCounter<long>("counter2");
counter2.Add(100);
var counter3 = meter.CreateCounter<long>("counter3");
counter3.Add(100);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
var metric = exportedItems[0];
Assert.Equal(3, exportedItems.Count);
var aggregatorStore = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metric) as AggregatorStore;
var maxMetricPointsAttribute = (int)typeof(AggregatorStore).GetField("maxMetricPoints", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStore);
Assert.Equal(10000, maxMetricPointsAttribute);
Assert.Equal(10000, exportedItems[1].AggregatorStore.CardinalityLimit);
if (setDefault)
{
Assert.Equal(3, exportedItems[0].AggregatorStore.CardinalityLimit);
Assert.Equal(3, exportedItems[2].AggregatorStore.CardinalityLimit);
}
else
{
Assert.Equal(2000, exportedItems[0].AggregatorStore.CardinalityLimit);
Assert.Equal(2000, exportedItems[2].AggregatorStore.CardinalityLimit);
}
}
[Fact]
@ -987,24 +1015,15 @@ public class MetricViewTests : MetricTestsBase
var metricB = exportedItems[1];
var metricC = exportedItems[2];
var aggregatorStoreA = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metricA) as AggregatorStore;
var maxMetricPointsAttributeA = (int)typeof(AggregatorStore).GetField("maxMetricPoints", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStoreA);
Assert.Equal(256, maxMetricPointsAttributeA);
Assert.Equal(256, metricA.AggregatorStore.CardinalityLimit);
Assert.Equal("MetricStreamA", metricA.Name);
Assert.Equal(20, GetAggregatedValue(metricA));
var aggregatorStoreB = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metricB) as AggregatorStore;
var maxMetricPointsAttributeB = (int)typeof(AggregatorStore).GetField("maxMetricPoints", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStoreB);
Assert.Equal(3, maxMetricPointsAttributeB);
Assert.Equal(3, metricB.AggregatorStore.CardinalityLimit);
Assert.Equal("MetricStreamB", metricB.Name);
Assert.Equal(10, GetAggregatedValue(metricB));
var aggregatorStoreC = typeof(Metric).GetField("aggStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(metricC) as AggregatorStore;
var maxMetricPointsAttributeC = (int)typeof(AggregatorStore).GetField("maxMetricPoints", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(aggregatorStoreC);
Assert.Equal(200000, maxMetricPointsAttributeC);
Assert.Equal(200000, metricC.AggregatorStore.CardinalityLimit);
Assert.Equal("MetricStreamC", metricC.Name);
Assert.Equal(10, GetAggregatedValue(metricC));