diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 28c8c85b510..0b9de051d0d 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -329,6 +329,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental-apis", "experi docs\diagnostics\experimental-apis\OTEL1000.md = docs\diagnostics\experimental-apis\OTEL1000.md docs\diagnostics\experimental-apis\OTEL1001.md = docs\diagnostics\experimental-apis\OTEL1001.md docs\diagnostics\experimental-apis\OTEL1002.md = docs\diagnostics\experimental-apis\OTEL1002.md + docs\diagnostics\experimental-apis\OTEL1003.md = docs\diagnostics\experimental-apis\OTEL1003.md docs\diagnostics\experimental-apis\README.md = docs\diagnostics\experimental-apis\README.md EndProjectSection EndProject diff --git a/build/Common.props b/build/Common.props index 763c91d057a..d54badad2c6 100644 --- a/build/Common.props +++ b/build/Common.props @@ -10,7 +10,7 @@ enable enable - $(NoWarn);OTEL1000;OTEL1001;OTEL1002 + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1003 diff --git a/docs/diagnostics/experimental-apis/OTEL1003.md b/docs/diagnostics/experimental-apis/OTEL1003.md new file mode 100644 index 00000000000..d6ed954b2bc --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1003.md @@ -0,0 +1,29 @@ +# OpenTelemetry .NET Diagnostic: OTEL1003 + +## Overview + +This is an Experimental API diagnostic covering the following API: + +* `MetricStreamConfiguration.CardinalityLimit.get` +* `MetricStreamConfiguration.CardinalityLimit.set` + +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. + +We are exposing these APIs experimentally until the specification declares them +stable. diff --git a/docs/diagnostics/experimental-apis/README.md b/docs/diagnostics/experimental-apis/README.md index 3036596176a..a5d527de3ba 100644 --- a/docs/diagnostics/experimental-apis/README.md +++ b/docs/diagnostics/experimental-apis/README.md @@ -33,6 +33,12 @@ Description: Metrics Exemplar Support Details: [OTEL1002](./OTEL1002.md) +### OTEL1003 + +Description: MetricStreamConfiguration CardinalityLimit Support + +Details: [OTEL1003](./OTEL1003.md) + ## Inactive Experimental APIs which have been released stable or removed: diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index bb5bd1824f6..1f80fea01eb 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -18,6 +18,8 @@ OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! +OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? +OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void OpenTelemetry.Metrics.TraceBasedExemplarFilter OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 4a284473573..d6888194f10 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -11,13 +11,18 @@ * Fixed an issue where `SimpleExemplarReservoir` was not resetting internal state for cumulative temporality. - [#5230](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5230) + ([#5230](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5230)) * Fixed an issue causing `LogRecord`s to be incorrectly reused when wrapping an instance of `BatchLogRecordExportProcessor` inside another `BaseProcessor` which leads to missing or incorrect data during export. - [#5255](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5255) + ([#5255](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5255)) + +* **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)) ## 1.7.0 diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 11844d3e7b4..4e33de18a66 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -236,7 +236,7 @@ public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder /// This may change in the future. See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/2360. /// /// . - /// Maximum maximum number of metric points allowed per metric stream. + /// Maximum number of metric points allowed per metric stream. /// The supplied for chaining. public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) { diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index a0575491378..c8a5fa9696e 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -136,6 +136,12 @@ internal List AddMetricsListWithViews(Instrument instrument, List @@ -10,6 +15,8 @@ public class MetricStreamConfiguration { private string? name; + private int? cardinalityLimit = null; + /// /// Gets the drop configuration. /// @@ -91,11 +98,44 @@ public string[]? TagKeys } } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Gets or sets a positive integer value defining the maximum number of + /// data points allowed for the metric managed by the view. + /// + /// + /// WARNING: This is an experimental API which might change or + /// be removed in the future. Use at your own risk. + /// 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. + /// +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.CardinalityLimitExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + internal +#endif + int? CardinalityLimit + { + get => this.cardinalityLimit; + set + { + if (value != null) + { + Guard.ThrowIfOutOfRange(value.Value, min: 1, max: int.MaxValue); + } + + this.cardinalityLimit = value; + } + } + internal string[]? CopiedTagKeys { get; private set; } internal int? ViewId { get; set; } - - // TODO: MetricPoints caps can be configured here on - // a per stream basis, when we add such a capability - // in the future. } diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs index 8d2bf49723e..b772ea53652 100644 --- a/src/Shared/DiagnosticDefinitions.cs +++ b/src/Shared/DiagnosticDefinitions.cs @@ -12,4 +12,5 @@ internal static class DiagnosticDefinitions public const string LoggerProviderExperimentalApi = "OTEL1000"; public const string LogsBridgeExperimentalApi = "OTEL1001"; public const string ExemplarExperimentalApi = "OTEL1002"; + public const string CardinalityLimitExperimentalApi = "OTEL1003"; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index 588c15bb277..ca8bbf3163b 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; +using System.Reflection; using OpenTelemetry.Internal; using OpenTelemetry.Tests; using Xunit; @@ -919,6 +920,34 @@ public void ViewConflict_OneInstrument_DifferentDescription() Assert.Equal(10, metricPoint2.GetSumLong()); } + [Fact] + public void CardinalityLimitofMatchingViewTakesPrecedenceOverMetricProviderWhenBothWereSet() + { + 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) => + { + return new MetricStreamConfiguration() { Name = "MetricStreamA", CardinalityLimit = 10000 }; + }) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("counter"); + counter.Add(100); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + var metric = exportedItems[0]; + + 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); + } + [Fact] public void ViewConflict_TwoDistinctInstruments_ThreeStreams() { @@ -930,13 +959,18 @@ public void ViewConflict_TwoDistinctInstruments_ThreeStreams() .AddMeter(meter.Name) .AddView((instrument) => { - return new MetricStreamConfiguration() { Name = "MetricStreamA", Description = "description" }; + return new MetricStreamConfiguration() { Name = "MetricStreamA", Description = "description", CardinalityLimit = 256 }; }) .AddView((instrument) => { return instrument.Description == "description1" - ? new MetricStreamConfiguration() { Name = "MetricStreamB" } - : new MetricStreamConfiguration() { Name = "MetricStreamC" }; + ? new MetricStreamConfiguration() { Name = "MetricStreamB", CardinalityLimit = 3 } + : new MetricStreamConfiguration() { Name = "MetricStreamC", CardinalityLimit = 200000 }; + }) + .AddView((instrument) => + { + // This view is ignored as the passed in CardinalityLimit is out of range. + return new MetricStreamConfiguration() { Name = "MetricStreamD", CardinalityLimit = -1 }; }) .AddInMemoryExporter(exportedItems)); @@ -953,12 +987,24 @@ 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("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("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("MetricStreamC", metricC.Name); Assert.Equal(10, GetAggregatedValue(metricC));