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));