diff --git a/docs/diagnostics/experimental-apis/OTEL1003.md b/docs/diagnostics/experimental-apis/OTEL1003.md index d6ed954b2bc..5f62f03575f 100644 --- a/docs/diagnostics/experimental-apis/OTEL1003.md +++ b/docs/diagnostics/experimental-apis/OTEL1003.md @@ -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) diff --git a/docs/metrics/README.md b/docs/metrics/README.md index b58d55023c4..2d50ebb7bc4 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -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 diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index d7cf59961a9..653dd4979f3 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -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 MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); -Counter AnotherFruitCounter = MyMeter.CreateCounter("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("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(); ``` diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index b2636f35de3..6260596c78c 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -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. diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index e181a81fb82..bccdbbf42c7 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -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[], int, int> lookupAggregatorStore; @@ -57,17 +57,17 @@ internal 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 AggregatorStore( this.tagsKeysInterestingCount = hs.Count; } - this.emitOverflowAttribute = emitOverflowAttribute; + this.EmitOverflowAttribute = emitOverflowAttribute; var reservedMetricPointsCount = 1; @@ -105,17 +105,17 @@ internal AggregatorStore( if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) { - this.availableMetricPoints = new Queue(maxMetricPoints - reservedMetricPointsCount); + this.availableMetricPoints = new Queue(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(concurrencyLevel: Environment.ProcessorCount, capacity: (maxMetricPoints - reservedMetricPointsCount) * 2); + new ConcurrentDictionary(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 int Snapshot() } 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 void SnapshotDeltaWithMetricPointReclaim() 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 void SnapshotDeltaWithMetricPointReclaim() } } - 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 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu 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 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu 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 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu { // 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 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu 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 @@ private void UpdateLong(long value, ReadOnlySpan> { Interlocked.Increment(ref this.DroppedMeasurements); - if (this.emitOverflowAttribute) + if (this.EmitOverflowAttribute) { this.InitializeOverflowTagPointIfNotInitialized(); this.metricPoints[1].Update(value); @@ -973,7 +973,7 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan. /// Maximum number of metric points allowed per metric stream. /// The supplied for chaining. +#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 MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterP { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - meterProviderBuilderSdk.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream); + meterProviderBuilderSdk.SetDefaultCardinalityLimit(maxMetricPointsPerMetricStream); } }); diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs index 8f09a896254..a3ca2a15e56 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs @@ -15,8 +15,8 @@ namespace OpenTelemetry.Metrics; /// 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 @@ public MeterProviderBuilderSdk(IServiceProvider serviceProvider) public List> 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; /// /// Returns whether the given instrument name is valid according to the specification. @@ -186,16 +186,16 @@ public MeterProviderBuilder AddView(Func 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; } diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 5dc529cfbd9..37be71cf77c 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -76,9 +76,12 @@ internal MeterProviderSdk( 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) { diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 60abe6051cc..cfd6b4e3463 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -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 @@ internal 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 @@ internal Metric( /// /// . public MetricPointsAccessor GetMetricPoints() - => this.aggStore.GetMetricPoints(); + => this.AggregatorStore.GetMetricPoints(); internal void UpdateLong(long value, ReadOnlySpan> tags) - => this.aggStore.Update(value, tags); + => this.AggregatorStore.Update(value, tags); internal void UpdateDouble(double value, ReadOnlySpan> tags) - => this.aggStore.Update(value, tags); + => this.AggregatorStore.Update(value, tags); internal int Snapshot() - => this.aggStore.Snapshot(); + => this.AggregatorStore.Snapshot(); } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index c8a5fa9696e..46c3b464557 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -17,8 +17,8 @@ public abstract partial class MetricReader private readonly HashSet metricStreamNames = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary 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 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) } 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 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) 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 @@ internal List AddMetricsListWithViews(Instrument instrument, List= 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 @@ internal List AddMetricsListWithViews(Instrument instrument, List metrics) } } - 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 @@ private Batch GetMetricsBatch() 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++) diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs index 077f332ea68..c38379bb718 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs @@ -109,10 +109,8 @@ public string[]? TagKeys /// Spec reference: Cardinality /// limits. - /// Note: If not set, the MeterProvider cardinality limit value will be - /// used, which defaults to 2000. Call - /// to configure the MeterProvider default. + /// Note: If not set the default MeterProvider cardinality limit of 2000 + /// will apply. /// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.CardinalityLimitExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs index bb9f1489b9f..61d739d6a18 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs @@ -255,7 +255,7 @@ public void HistogramBucketsDefaultUpdatesForSecondsTest(string meterName, strin metricStreamIdentity, AggregationType.Histogram, AggregationTemporality.Cumulative, - maxMetricPoints: 1024, + cardinalityLimit: 1024, this.emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints); @@ -332,7 +332,7 @@ internal void ExponentialHistogramTests(AggregationType aggregationType, Aggrega metricStreamIdentity, aggregationType, aggregationTemporality, - maxMetricPoints: 1024, + cardinalityLimit: 1024, this.emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints, exemplarsEnabled ? new AlwaysOnExemplarFilter() : null); @@ -442,7 +442,7 @@ internal void ExponentialMaxScaleConfigWorks(int? maxScale) metricStreamIdentity, AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, - maxMetricPoints: 1024, + cardinalityLimit: 1024, this.emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints); diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index 1e0c8a59dd1..ad18233a016 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -1423,26 +1423,26 @@ int MetricPointCount() // 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("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("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("key", "value" + i)); } @@ -1453,7 +1453,7 @@ int MetricPointCount() counterLong.Add(10, new KeyValuePair("key", "valueC")); exportedItems.Clear(); meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount()); + Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount()); } [Fact] diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs index 4726b38043f..baff3b86d76 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs @@ -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 void TestEmitOverflowAttributeConfigWithEnvVar(string value, bool isEmitO 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 void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, 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 void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(in 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 void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem 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 void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT 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++) { diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs index 6b8505a611a..de5eb88a0af 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs @@ -286,17 +286,12 @@ private sealed class CustomExporter : BaseExporter 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 override ExportResult Export(in Batch batch) { 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; var droppedMeasurements = aggStore.DroppedMeasurements; @@ -316,7 +311,7 @@ public override ExportResult Export(in Batch batch) } // 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()) { diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index ca8bbf3163b..70a26753ef0 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -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 void ViewConflict_OneInstrument_DifferentDescription() 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(); - 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("counter"); - counter.Add(100); + builder + .AddMeter(meter.Name) + .AddView((instrument) => + { + if (instrument.Name == "counter2") + { + return new MetricStreamConfiguration() { Name = "MetricStreamA", CardinalityLimit = 10000 }; + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + return null; + }) + .AddInMemoryExporter(exportedItems); + }); - var metric = exportedItems[0]; + var counter1 = meter.CreateCounter("counter1"); + counter1.Add(100); - 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); + var counter2 = meter.CreateCounter("counter2"); + counter2.Add(100); - Assert.Equal(10000, maxMetricPointsAttribute); + var counter3 = meter.CreateCounter("counter3"); + counter3.Add(100); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(3, exportedItems.Count); + + 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 void ViewConflict_TwoDistinctInstruments_ThreeStreams() 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));