Skip to content

Commit cf00e42

Browse files
CodeBlanchYun-Ting
andauthored
[sdk-metrics] Obsolete SetMaxMetricPointsPerMetricStream + standarize on "Cardinality Limit" name (#5328)
Co-authored-by: Yun-Ting Lin <[email protected]>
1 parent f214d27 commit cf00e42

File tree

16 files changed

+190
-232
lines changed

16 files changed

+190
-232
lines changed

docs/diagnostics/experimental-apis/OTEL1003.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,37 @@ Experimental APIs may be changed or removed in the future.
1111

1212
## Details
1313

14-
The OpenTelemetry Specification defines the
15-
[cardinality limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits)
16-
of a metric can be set by the matching view.
17-
1814
From the specification:
1915

2016
> The cardinality limit for an aggregation is defined in one of three ways:
21-
> A view with criteria matching the instrument an aggregation is created for has
22-
> an aggregation_cardinality_limit value defined for the stream, that value
23-
> SHOULD be used. If there is no matching view, but the MetricReader defines a
24-
> default cardinality limit value based on the instrument an aggregation is
25-
> created for, that value SHOULD be used. If none of the previous values are
26-
> defined, the default value of 2000 SHOULD be used.
17+
>
18+
> 1. A view with criteria matching the instrument an aggregation is created for
19+
> has an `aggregation_cardinality_limit` value defined for the stream, that
20+
> value SHOULD be used.
21+
> 2. If there is no matching view, but the `MetricReader` defines a default
22+
> cardinality limit value based on the instrument an aggregation is created
23+
> for, that value SHOULD be used.
24+
> 3. If none of the previous values are defined, the default value of 2000
25+
> SHOULD be used.
2726
2827
We are exposing these APIs experimentally until the specification declares them
2928
stable.
29+
30+
### Setting cardinality limit for a specific Metric via the View API
31+
32+
The OpenTelemetry Specification defines the [cardinality
33+
limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits)
34+
of a metric can be set by the matching view.
35+
36+
```csharp
37+
using var meterProvider = Sdk.CreateMeterProviderBuilder()
38+
.AddView(
39+
instrumentName: "MyFruitCounter",
40+
new MetricStreamConfiguration { CardinalityLimit = 10 })
41+
.Build();
42+
```
43+
44+
### Setting cardinality limit for a specific MetricReader
45+
46+
[This is not currently supported by OpenTelemetry
47+
.NET.](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5331)

docs/metrics/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -379,17 +379,19 @@ predictable and reliable behavior when excessive cardinality happens, whether it
379379
was due to a malicious attack or developer making mistakes while writing code.
380380

381381
OpenTelemetry has a default cardinality limit of `2000` per metric. This limit
382-
can be configured at `MeterProvider` level using the
383-
`SetMaxMetricPointsPerMetricStream` method, or at individual
384-
[view](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view)
385-
level using `MetricStreamConfiguration.CardinalityLimit`. Refer to this
386-
[doc](../../docs/metrics/customizing-the-sdk/README.md#changing-maximum-metricpoints-per-metricstream)
382+
can be configured at the individual metric level using the [View
383+
API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view)
384+
and the `MetricStreamConfiguration.CardinalityLimit` setting. Refer to this
385+
[doc](../../docs/metrics/customizing-the-sdk/README.md#changing-the-cardinality-limit-for-a-metric)
387386
for more information.
388387

389388
Given a metric, once the cardinality limit is reached, any new measurement which
390-
cannot be independently aggregated because of the limit will be aggregated using
391-
the [overflow
392-
attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute).
389+
cannot be independently aggregated because of the limit will be dropped or
390+
aggregated using the [overflow
391+
attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute)
392+
(if enabled). When NOT using the overflow attribute feature a warning is written
393+
to the [self-diagnostic log](../../src/OpenTelemetry/README.md#self-diagnostics)
394+
the first time an overflow is detected for a given metric.
393395

394396
> [!NOTE]
395397
> Overflow attribute was introduced in OpenTelemetry .NET

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

Lines changed: 11 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -367,90 +367,24 @@ MyFruitCounter.Add(1, new("name", "apple"), new("color", "red"));
367367
AnotherFruitCounter.Add(1, new("name", "apple"), new("color", "red"));
368368
```
369369

370-
### Changing maximum MetricPoints per MetricStream
370+
### Changing the cardinality limit for a Metric
371371

372-
A Metric stream can contain as many Metric points as the number of unique
373-
combination of keys and values. To protect the SDK from unbounded memory usage,
374-
SDK limits the maximum number of metric points per metric stream, to a default
375-
of 2000. Once the limit is hit, any new key/value combination for that metric is
376-
ignored. The SDK chooses the key/value combinations in the order in which they
377-
are emitted. `SetMaxMetricPointsPerMetricStream` can be used to override the
378-
default.
372+
To set the [cardinality limit](../README.md#cardinality-limits) for an
373+
individual metric, use `MetricStreamConfiguration.CardinalityLimit` setting on
374+
the View API:
379375

380376
> [!NOTE]
381-
> One `MetricPoint` is reserved for every `MetricStream` for the
382-
special case where there is no key/value pair associated with the metric. The
383-
maximum number of `MetricPoint`s has to accommodate for this special case.
384-
385-
Consider the below example. Here we set the maximum number of `MetricPoint`s
386-
allowed to be `3`. This means that for every `MetricStream`, the SDK will export
387-
measurements for up to `3` distinct key/value combinations of the metric. There
388-
are two instruments published here: `MyFruitCounter` and `AnotherFruitCounter`.
389-
There are two total `MetricStream`s created one for each of these instruments.
390-
SDK will limit the maximum number of distinct key/value combinations for each of
391-
these `MetricStream`s to `3`.
392-
393-
```csharp
394-
using System.Collections.Generic;
395-
using System.Diagnostics.Metrics;
396-
using OpenTelemetry;
397-
using OpenTelemetry.Metrics;
398-
399-
Counter<long> MyFruitCounter = MyMeter.CreateCounter<long>("MyFruitCounter");
400-
Counter<long> AnotherFruitCounter = MyMeter.CreateCounter<long>("AnotherFruitCounter");
401-
402-
using var meterProvider = Sdk.CreateMeterProviderBuilder()
403-
.AddMeter("*")
404-
.AddConsoleExporter()
405-
.SetMaxMetricPointsPerMetricStream(3) // The default value is 2000
406-
.Build();
407-
408-
// There are four distinct key/value combinations emitted for `MyFruitCounter`:
409-
// 1. No key/value pair
410-
// 2. (name:apple, color:red)
411-
// 3. (name:lemon, color:yellow)
412-
// 4. (name:apple, color:green)
413-
414-
// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations:
415-
// 1. No key/value pair
416-
// 2. (name:apple, color:red)
417-
// 3. (name:lemon, color:yellow)
418-
419-
MyFruitCounter.Add(1); // Exported (No key/value pair)
420-
MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); // Exported
421-
MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); // Exported
422-
MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); // Exported
423-
MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); // Not exported
424-
MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); // Exported
425-
MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); // Exported
426-
427-
// There are four distinct key/value combinations emitted for `AnotherFruitCounter`:
428-
// 1. (name:kiwi)
429-
// 2. (name:banana, color:yellow)
430-
// 3. (name:mango, color:yellow)
431-
// 4. (name:banana, color:green)
432-
433-
// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations:
434-
// 1. No key/value pair (This is a special case. The SDK reserves a `MetricPoint` for it even if it's not explicitly emitted.)
435-
// 2. (name:kiwi)
436-
// 3. (name:banana, color:yellow)
437-
438-
AnotherFruitCounter.Add(4, new KeyValuePair<string, object>("name", "kiwi")); // Exported
439-
AnotherFruitCounter.Add(1, new("name", "banana"), new("color", "yellow")); // Exported
440-
AnotherFruitCounter.Add(2, new("name", "mango"), new("color", "yellow")); // Not exported
441-
AnotherFruitCounter.Add(1, new("name", "mango"), new("color", "yellow")); // Not exported
442-
AnotherFruitCounter.Add(2, new("name", "banana"), new("color", "green")); // Not exported
443-
AnotherFruitCounter.Add(5, new("name", "banana"), new("color", "yellow")); // Exported
444-
AnotherFruitCounter.Add(4, new("name", "mango"), new("color", "yellow")); // Not exported
445-
```
446-
447-
To set the [cardinality limit](../README.md#cardinality-limits) at individual
448-
metric level, use `MetricStreamConfiguration.CardinalityLimit`:
377+
> `MetricStreamConfiguration.CardinalityLimit` is an experimental API only
378+
available in pre-release builds. For details see:
379+
[OTEL1003](../../diagnostics/experimental-apis/OTEL1003.md).
449380

450381
```csharp
451382
var meterProvider = Sdk.CreateMeterProviderBuilder()
452383
.AddMeter("MyCompany.MyProduct.MyLibrary")
453-
.AddView(instrumentName: "MyFruitCounter", new MetricStreamConfiguration { CardinalityLimit = 10 })
384+
// Set a custom CardinalityLimit (10) for "MyFruitCounter"
385+
.AddView(
386+
instrumentName: "MyFruitCounter",
387+
new MetricStreamConfiguration { CardinalityLimit = 10 })
454388
.AddConsoleExporter()
455389
.Build();
456390
```

src/OpenTelemetry/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121

2222
* **Experimental (pre-release builds only):** Added support for setting
2323
`CardinalityLimit` (the maximum number of data points allowed for a metric)
24-
when configuring a view.
25-
([#5312](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5312))
24+
when configuring a view (applies to individual metrics) and obsoleted
25+
`MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream` (previously
26+
applied to all metrics). The default cardinality limit for metrics remains at
27+
`2000`.
28+
([#5312](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5312),
29+
[#5328](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5328))
2630

2731
* Updated `LogRecord` to keep `CategoryName` and `Logger` in sync when using the
2832
experimental Log Bridge API.

src/OpenTelemetry/Metrics/AggregatorStore.cs

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ internal sealed class AggregatorStore
1313
{
1414
internal readonly bool OutputDelta;
1515
internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled;
16+
internal readonly int CardinalityLimit;
17+
internal readonly bool EmitOverflowAttribute;
1618
internal long DroppedMeasurements = 0;
1719

1820
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
4244
private readonly int exponentialHistogramMaxScale;
4345
private readonly UpdateLongDelegate updateLongCallback;
4446
private readonly UpdateDoubleDelegate updateDoubleCallback;
45-
private readonly int maxMetricPoints;
46-
private readonly bool emitOverflowAttribute;
4747
private readonly ExemplarFilter exemplarFilter;
4848
private readonly Func<KeyValuePair<string, object?>[], int, int> lookupAggregatorStore;
4949

@@ -57,17 +57,17 @@ internal AggregatorStore(
5757
MetricStreamIdentity metricStreamIdentity,
5858
AggregationType aggType,
5959
AggregationTemporality temporality,
60-
int maxMetricPoints,
60+
int cardinalityLimit,
6161
bool emitOverflowAttribute,
6262
bool shouldReclaimUnusedMetricPoints,
6363
ExemplarFilter? exemplarFilter = null)
6464
{
6565
this.name = metricStreamIdentity.InstrumentName;
66-
this.maxMetricPoints = maxMetricPoints;
66+
this.CardinalityLimit = cardinalityLimit;
6767

68-
this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.maxMetricPoints}";
69-
this.metricPoints = new MetricPoint[maxMetricPoints];
70-
this.currentMetricPointBatch = new int[maxMetricPoints];
68+
this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.CardinalityLimit}";
69+
this.metricPoints = new MetricPoint[cardinalityLimit];
70+
this.currentMetricPointBatch = new int[cardinalityLimit];
7171
this.aggType = aggType;
7272
this.OutputDelta = temporality == AggregationTemporality.Delta;
7373
this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity);
@@ -89,7 +89,7 @@ internal AggregatorStore(
8989
this.tagsKeysInterestingCount = hs.Count;
9090
}
9191

92-
this.emitOverflowAttribute = emitOverflowAttribute;
92+
this.EmitOverflowAttribute = emitOverflowAttribute;
9393

9494
var reservedMetricPointsCount = 1;
9595

@@ -105,17 +105,17 @@ internal AggregatorStore(
105105

106106
if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled)
107107
{
108-
this.availableMetricPoints = new Queue<int>(maxMetricPoints - reservedMetricPointsCount);
108+
this.availableMetricPoints = new Queue<int>(cardinalityLimit - reservedMetricPointsCount);
109109

110110
// There is no overload which only takes capacity as the parameter
111111
// 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
112112
// We expect at the most (maxMetricPoints - reservedMetricPointsCount) * 2 entries- one for sorted and one for unsorted input
113113
this.tagsToMetricPointIndexDictionaryDelta =
114-
new ConcurrentDictionary<Tags, LookupData>(concurrencyLevel: Environment.ProcessorCount, capacity: (maxMetricPoints - reservedMetricPointsCount) * 2);
114+
new ConcurrentDictionary<Tags, LookupData>(concurrencyLevel: Environment.ProcessorCount, capacity: (cardinalityLimit - reservedMetricPointsCount) * 2);
115115

116116
// Add all the indices except for the reserved ones to the queue so that threads have
117117
// readily available access to these MetricPoints for their use.
118-
for (int i = reservedMetricPointsCount; i < this.maxMetricPoints; i++)
118+
for (int i = reservedMetricPointsCount; i < this.CardinalityLimit; i++)
119119
{
120120
this.availableMetricPoints.Enqueue(i);
121121
}
@@ -164,12 +164,12 @@ internal int Snapshot()
164164
}
165165
else if (this.OutputDelta)
166166
{
167-
var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1);
167+
var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1);
168168
this.SnapshotDelta(indexSnapshot);
169169
}
170170
else
171171
{
172-
var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1);
172+
var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1);
173173
this.SnapshotCumulative(indexSnapshot);
174174
}
175175

@@ -227,7 +227,7 @@ internal void SnapshotDeltaWithMetricPointReclaim()
227227

228228
int startIndexForReclaimableMetricPoints = 1;
229229

230-
if (this.emitOverflowAttribute)
230+
if (this.EmitOverflowAttribute)
231231
{
232232
startIndexForReclaimableMetricPoints = 2; // Index 0 and 1 are reserved for no tags and overflow
233233

@@ -249,7 +249,7 @@ internal void SnapshotDeltaWithMetricPointReclaim()
249249
}
250250
}
251251

252-
for (int i = startIndexForReclaimableMetricPoints; i < this.maxMetricPoints; i++)
252+
for (int i = startIndexForReclaimableMetricPoints; i < this.CardinalityLimit; i++)
253253
{
254254
ref var metricPoint = ref this.metricPoints[i];
255255

@@ -440,7 +440,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
440440
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
441441
{
442442
aggregatorIndex = this.metricPointIndex;
443-
if (aggregatorIndex >= this.maxMetricPoints)
443+
if (aggregatorIndex >= this.CardinalityLimit)
444444
{
445445
// sorry! out of data points.
446446
// TODO: Once we support cleanup of
@@ -469,7 +469,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
469469
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex))
470470
{
471471
aggregatorIndex = ++this.metricPointIndex;
472-
if (aggregatorIndex >= this.maxMetricPoints)
472+
if (aggregatorIndex >= this.CardinalityLimit)
473473
{
474474
// sorry! out of data points.
475475
// TODO: Once we support cleanup of
@@ -496,7 +496,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
496496
{
497497
// This else block is for tag length = 1
498498
aggregatorIndex = this.metricPointIndex;
499-
if (aggregatorIndex >= this.maxMetricPoints)
499+
if (aggregatorIndex >= this.CardinalityLimit)
500500
{
501501
// sorry! out of data points.
502502
// TODO: Once we support cleanup of
@@ -518,7 +518,7 @@ private int LookupAggregatorStore(KeyValuePair<string, object?>[] tagKeysAndValu
518518
if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex))
519519
{
520520
aggregatorIndex = ++this.metricPointIndex;
521-
if (aggregatorIndex >= this.maxMetricPoints)
521+
if (aggregatorIndex >= this.CardinalityLimit)
522522
{
523523
// sorry! out of data points.
524524
// TODO: Once we support cleanup of
@@ -929,7 +929,7 @@ private void UpdateLong(long value, ReadOnlySpan<KeyValuePair<string, object?>>
929929
{
930930
Interlocked.Increment(ref this.DroppedMeasurements);
931931

932-
if (this.emitOverflowAttribute)
932+
if (this.EmitOverflowAttribute)
933933
{
934934
this.InitializeOverflowTagPointIfNotInitialized();
935935
this.metricPoints[1].Update(value);
@@ -973,7 +973,7 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan<KeyValuePair<string,
973973
{
974974
Interlocked.Increment(ref this.DroppedMeasurements);
975975

976-
if (this.emitOverflowAttribute)
976+
if (this.EmitOverflowAttribute)
977977
{
978978
this.InitializeOverflowTagPointIfNotInitialized();
979979
this.metricPoints[1].Update(value);
@@ -1017,7 +1017,7 @@ private void UpdateDouble(double value, ReadOnlySpan<KeyValuePair<string, object
10171017
{
10181018
Interlocked.Increment(ref this.DroppedMeasurements);
10191019

1020-
if (this.emitOverflowAttribute)
1020+
if (this.EmitOverflowAttribute)
10211021
{
10221022
this.InitializeOverflowTagPointIfNotInitialized();
10231023
this.metricPoints[1].Update(value);
@@ -1061,7 +1061,7 @@ private void UpdateDoubleCustomTags(double value, ReadOnlySpan<KeyValuePair<stri
10611061
{
10621062
Interlocked.Increment(ref this.DroppedMeasurements);
10631063

1064-
if (this.emitOverflowAttribute)
1064+
if (this.EmitOverflowAttribute)
10651065
{
10661066
this.InitializeOverflowTagPointIfNotInitialized();
10671067
this.metricPoints[1].Update(value);

0 commit comments

Comments
 (0)