diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 71f5108b0c2..d8c66536741 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -37,6 +37,13 @@ `8.0.0`. ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) +* Revert the default behavior of Metrics SDK for Delta aggregation. It would not + reclaim unused Metric Points by default. You can enable the SDK to reclaim + unused Metric Points by setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS` to `true` + before setting up the `MeterProvider`. + ([#5052](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5052)) + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 9bbceacf6d6..42d1aa0443c 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -25,6 +25,7 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { internal readonly bool OutputDelta; + internal readonly bool ShouldReclaimUnusedMetricPoints; 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."; @@ -81,6 +82,7 @@ internal AggregatorStore( AggregationTemporality temporality, int maxMetricPoints, bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, ExemplarFilter? exemplarFilter = null) { this.name = metricStreamIdentity.InstrumentName; @@ -122,7 +124,9 @@ internal AggregatorStore( reservedMetricPointsCount++; } - if (this.OutputDelta) + this.ShouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; + + if (this.OutputDelta && shouldReclaimUnusedMetricPoints) { this.availableMetricPoints = new Queue(maxMetricPoints - reservedMetricPointsCount); @@ -181,7 +185,7 @@ internal int Snapshot() this.batchSize = 0; if (this.OutputDelta) { - if (this.reclaimMetricPoints) + if (this.ShouldReclaimUnusedMetricPoints && this.reclaimMetricPoints) { this.SnapshotDeltaWithMetricPointReclaim(); } diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 917bb0b35f8..73880837dce 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -30,8 +30,10 @@ internal sealed class MeterProviderSdk : MeterProvider internal readonly IDisposable? OwnedServiceProvider; internal int ShutdownCount; internal bool Disposed; + internal bool ShouldReclaimUnusedMetricPoints; private const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + private const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; private readonly List instrumentations = new(); private readonly List> viewConfigs; @@ -51,6 +53,7 @@ internal MeterProviderSdk( var config = serviceProvider!.GetRequiredService(); _ = config.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out bool isEmitOverflowAttributeKeySet); + _ = config.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ShouldReclaimUnusedMetricPoints); this.ServiceProvider = serviceProvider!; diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 36631e5ce25..6fe6036498e 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -60,6 +60,7 @@ internal Metric( AggregationTemporality temporality, int maxMetricPointsPerMetricStream, bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, ExemplarFilter? exemplarFilter = null) { this.InstrumentIdentity = instrumentIdentity; @@ -166,7 +167,7 @@ internal Metric( throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, exemplarFilter); + this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); this.Temporality = temporality; this.InstrumentDisposed = false; } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index ca146fe6031..577f5afec63 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -64,7 +64,7 @@ internal MetricPoint( Debug.Assert(aggregatorStore != null, "AggregatorStore was null."); Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null."); - if (aggregatorStore!.OutputDelta) + if (aggregatorStore!.OutputDelta && aggregatorStore.ShouldReclaimUnusedMetricPoints) { Debug.Assert(lookupData != null, "LookupData was null."); } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 8a310352f2b..16762f97da6 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -75,7 +75,8 @@ internal AggregationTemporality GetAggregationTemporality(Type instrumentType) Metric? metric = null; try { - metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, this.exemplarFilter); + bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints; + metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter); } catch (NotSupportedException nse) { @@ -162,7 +163,8 @@ internal List AddMetricsListWithViews(Instrument instrument, List>()); @@ -529,7 +531,7 @@ private class ThreadArguments public class AggregatorTests : AggregatorTestsBase { public AggregatorTests() - : base(false) + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -537,7 +539,23 @@ public AggregatorTests() public class AggregatorTestsWithOverflowAttribute : AggregatorTestsBase { public AggregatorTestsWithOverflowAttribute() - : base(true) + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class AggregatorTestsWithReclaimAttribute : AggregatorTestsBase +{ + public AggregatorTestsWithReclaimAttribute() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class AggregatorTestsWithBothReclaimAndOverflowAttributes : AggregatorTestsBase +{ + public AggregatorTestsWithBothReclaimAndOverflowAttributes() + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index bfe2229dcf5..ff226b8e360 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -16,6 +16,8 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; using OpenTelemetry.Tests; @@ -26,7 +28,7 @@ namespace OpenTelemetry.Metrics.Tests; #pragma warning disable SA1402 -public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable +public abstract class MetricApiTestsBase : MetricTestsBase { private const int MaxTimeToAllowForFlush = 10000; private static readonly int NumberOfThreads = Environment.ProcessorCount; @@ -34,15 +36,27 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable private static readonly double DeltaDoubleValueUpdatedByEachCall = 11.987; private static readonly int NumberOfMetricUpdateByEachThread = 100000; private readonly ITestOutputHelper output; + private readonly IConfiguration configuration; - protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute) + protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { this.output = output; + var configurationData = new Dictionary(); + if (emitOverflowAttribute) { - Environment.SetEnvironmentVariable(EmitOverFlowAttributeConfigKey, "true"); + configurationData[EmitOverFlowAttributeConfigKey] = "true"; } + + if (shouldReclaimUnusedMetricPoints) + { + configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + } + + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); } [Fact] @@ -51,6 +65,10 @@ public void MeasurementWithNullValuedTag() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems) .Build(); @@ -84,6 +102,10 @@ public void ObserverCallbackTest() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems) .Build(); @@ -113,6 +135,10 @@ public void ObserverCallbackExceptionTest() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems) .Build(); @@ -147,6 +173,10 @@ public void MetricUnitIsExportedCorrectly(string unit) using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -170,6 +200,10 @@ public void MetricDescriptionIsExportedCorrectly(string description) using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -190,6 +224,10 @@ public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -224,6 +262,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -271,6 +313,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -318,6 +364,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -363,6 +413,10 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems); @@ -410,6 +464,10 @@ public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVers using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems); @@ -443,6 +501,10 @@ public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}"); using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => @@ -487,6 +549,10 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) var exportedItems = new List(); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter("AbcCompany.XyzProduct.Component?") .AddMeter("DefCompany.*.ComponentC") .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. @@ -536,6 +602,10 @@ public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) var exportedItems = new List(); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddInMemoryExporter(exportedItems); if (hasView) @@ -565,6 +635,10 @@ public void CounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("mycounter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -667,6 +741,10 @@ public void ObservableCounterAggregationTest(bool exportDelta) }); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -741,6 +819,10 @@ public void ObservableCounterWithTagsAggregationTest(bool exportDelta) }); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -838,6 +920,10 @@ public void ObservableCounterSpatialAggregationTest(bool exportDelta) }); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -879,6 +965,10 @@ public void UpDownCounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateUpDownCounter("mycounter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -961,6 +1051,10 @@ public void ObservableUpDownCounterAggregationTest(bool exportDelta) }); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1025,6 +1119,10 @@ public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) }); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1095,6 +1193,10 @@ public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1186,6 +1288,10 @@ public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1279,6 +1385,10 @@ public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality var counter1 = meter1.CreateCounter("counterFromMeter1"); var counter2 = meter2.CreateCounter("counterFromMeter2"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => @@ -1347,6 +1457,10 @@ int MetricPointCount() using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); var counterLong = meter.CreateCounter("mycounterCapTest"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1443,6 +1557,10 @@ public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems) .Build(); @@ -1465,6 +1583,10 @@ public void InstrumentWithValidNameIsExportedTest(string name) using var meter = new Meter("InstrumentValidNameIsExportedTest"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems) .Build(); @@ -1487,6 +1609,10 @@ public void SetupSdkProviderWithNoReader(bool hasViews) // This test ensures that MeterProviderSdk can be set up without any reader using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name); if (hasViews) @@ -1507,9 +1633,13 @@ public void UnsupportedMetricInstrument() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var exportedItems = new List(); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { @@ -1525,11 +1655,6 @@ public void UnsupportedMetricInstrument() Assert.Empty(exportedItems); } - public void Dispose() - { - Environment.SetEnvironmentVariable(EmitOverFlowAttributeConfigKey, null); - } - private static void CounterUpdateThread(object obj) where T : struct, IComparable { @@ -1705,7 +1830,7 @@ private class UpdateThreadArguments public class MetricApiTest : MetricApiTestsBase { public MetricApiTest(ITestOutputHelper output) - : base(output, false) + : base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -1713,7 +1838,23 @@ public MetricApiTest(ITestOutputHelper output) public class MetricApiTestWithOverflowAttribute : MetricApiTestsBase { public MetricApiTestWithOverflowAttribute(ITestOutputHelper output) - : base(output, true) + : base(output, emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricApiTestWithReclaimAttribute : MetricApiTestsBase +{ + public MetricApiTestWithReclaimAttribute(ITestOutputHelper output) + : base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class MetricApiTestWithBothOverflowAndReclaimAttributes : MetricApiTestsBase +{ + public MetricApiTestWithBothOverflowAndReclaimAttributes(ITestOutputHelper output) + : base(output, emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs deleted file mode 100644 index e6b1dde373f..00000000000 --- a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs +++ /dev/null @@ -1,447 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics.Metrics; -using System.Reflection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Tests; -using Xunit; - -namespace OpenTelemetry.Metrics.Tests; - -public class MetricOverflowAttributeTests -{ - [Theory] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("FALSE", false)] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("TRUE", true)] - public void TestEmitOverflowAttributeConfigWithEnvVar(string value, bool isEmitOverflowAttributeKeySet) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, value); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - 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); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("FALSE", false)] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("TRUE", true)] - public void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, bool isEmitOverflowAttributeKeySet) - { - try - { - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value }) - .Build(); - - services.AddSingleton(configuration); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - 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); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(1, false)] - [InlineData(2, true)] - [InlineData(10, true)] - public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(int maxMetricPoints, bool isEmitOverflowAttributeKeySet) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .SetMaxMetricPointsPerMetricStream(maxMetricPoints) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - counter.Add(10); - - 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); - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Delta)] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTemporalityPreference temporalityPreference) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("TestCounter"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) - .Build(); - - // There are two reserved MetricPoints - // 1. For zero tags - // 2. For metric overflow attribute when user opts-in for this feature - - counter.Add(10); // Record measurement for zero tags - - // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; - - for (int i = 0; i < maxMetricPointsForUse; i++) - { - // Emit unique key-value pairs to use up the available MetricPoints - // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags - counter.Add(10, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint overflowMetricPoint; - - // We still have not exceeded the max MetricPoint limit - Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - exportedItems.Clear(); - metricPoints.Clear(); - - counter.Add(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint zeroTagsMetricPoint; - if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) - { - // Check metric point for zero tags - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - Assert.Equal(10, zeroTagsMetricPoint.GetSumLong()); - } - - // Check metric point for overflow - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); - Assert.Equal(1, overflowMetricPoint.Tags.Count); - Assert.Equal(5, overflowMetricPoint.GetSumLong()); - - exportedItems.Clear(); - metricPoints.Clear(); - - counter.Add(15); // Record another measurement for zero tags - - // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) - { - counter.Add(5, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(15, zeroTagsMetricPoint.GetSumLong()); - - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 - // Number of metric points dropped = 2500 - 1998 = 502 - Assert.Equal(2510, overflowMetricPoint.GetSumLong()); // 502 * 5 - } - else - { - Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); - Assert.Equal(12505, overflowMetricPoint.GetSumLong()); // 5 + (2500 * 5) - } - - exportedItems.Clear(); - metricPoints.Clear(); - - // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred - counter.Add(25); - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); - } - else - { - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - Assert.Equal(50, zeroTagsMetricPoint.GetSumLong()); - Assert.Equal(12505, overflowMetricPoint.GetSumLong()); - } - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Delta)] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderTemporalityPreference temporalityPreference) - { - try - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); - - var exportedItems = new List(); - - var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("TestHistogram"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) - .Build(); - - // There are two reserved MetricPoints - // 1. For zero tags - // 2. For metric overflow attribute when user opts-in for this feature - - histogram.Record(10); // Record measurement for zero tags - - // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; - - for (int i = 0; i < maxMetricPointsForUse; i++) - { - // Emit unique key-value pairs to use up the available MetricPoints - // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags - histogram.Record(10, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - - Assert.Single(exportedItems); - var metric = exportedItems[0]; - - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint overflowMetricPoint; - - // We still have not exceeded the max MetricPoint limit - Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - exportedItems.Clear(); - metricPoints.Clear(); - - histogram.Record(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - MetricPoint zeroTagsMetricPoint; - if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) - { - // Check metric point for zero tags - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - Assert.Equal(10, zeroTagsMetricPoint.GetHistogramSum()); - } - - // Check metric point for overflow - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); - Assert.Equal(1, overflowMetricPoint.Tags.Count); - Assert.Equal(5, overflowMetricPoint.GetHistogramSum()); - - exportedItems.Clear(); - metricPoints.Clear(); - - histogram.Record(15); // Record another measurement for zero tags - - // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) - { - histogram.Record(5, new KeyValuePair("Key", i)); - } - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(15, zeroTagsMetricPoint.GetHistogramSum()); - - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 - // Number of metric points dropped = 2500 - 1998 = 502 - Assert.Equal(502, overflowMetricPoint.GetHistogramCount()); - Assert.Equal(2510, overflowMetricPoint.GetHistogramSum()); // 502 * 5 - } - else - { - Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); - - Assert.Equal(2501, overflowMetricPoint.GetHistogramCount()); - Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); // 5 + (2500 * 5) - } - - exportedItems.Clear(); - metricPoints.Clear(); - - // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred - histogram.Record(25); - - meterProvider.ForceFlush(); - metric = exportedItems[0]; - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); - - if (temporalityPreference == MetricReaderTemporalityPreference.Delta) - { - Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); - } - else - { - overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); - - Assert.Equal(50, zeroTagsMetricPoint.GetHistogramSum()); - Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); - } - } - finally - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } - } -} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs new file mode 100644 index 00000000000..d275e8c69c2 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs @@ -0,0 +1,489 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics.Metrics; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class MetricOverflowAttributeTestsBase +{ + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + + private readonly bool shouldReclaimUnusedMetricPoints; + private readonly Dictionary configurationData = new() + { + [MetricTestsBase.EmitOverFlowAttributeConfigKey] = "true", + }; + + private readonly IConfiguration configuration; + + public MetricOverflowAttributeTestsBase(bool shouldReclaimUnusedMetricPoints) + { + this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; + + if (shouldReclaimUnusedMetricPoints) + { + this.configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + } + + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(this.configurationData) + .Build(); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestEmitOverflowAttributeConfigWithEnvVar(string value, bool isEmitOverflowAttributeKeySet) + { + // Clear the environment variable value first + Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); + + // Set the environment variable to the value provided in the test input + Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, value); + + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + 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); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, bool isEmitOverflowAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value }) + .Build(); + + services.AddSingleton(configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + 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); + } + + [Theory] + [InlineData(1, false)] + [InlineData(2, true)] + [InlineData(10, true)] + public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(int maxMetricPoints, bool isEmitOverflowAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .SetMaxMetricPointsPerMetricStream(maxMetricPoints) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + counter.Add(10); + + 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); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Delta)] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("TestCounter"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) + .Build(); + + // There are two reserved MetricPoints + // 1. For zero tags + // 2. For metric overflow attribute when user opts-in for this feature + + counter.Add(10); // Record measurement for zero tags + + // Max number for MetricPoints available for use when emitted with tags + int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; + + for (int i = 0; i < maxMetricPointsForUse; i++) + { + // Emit unique key-value pairs to use up the available MetricPoints + // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags + counter.Add(10, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint overflowMetricPoint; + + // We still have not exceeded the max MetricPoint limit + Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + exportedItems.Clear(); + metricPoints.Clear(); + + counter.Add(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint zeroTagsMetricPoint; + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Check metric point for zero tags + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + Assert.Equal(10, zeroTagsMetricPoint.GetSumLong()); + } + + // Check metric point for overflow + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); + Assert.Equal(1, overflowMetricPoint.Tags.Count); + Assert.Equal(5, overflowMetricPoint.GetSumLong()); + + exportedItems.Clear(); + metricPoints.Clear(); + + counter.Add(15); // Record another measurement for zero tags + + // Emit 2500 more newer MetricPoints with distinct dimension combinations + for (int i = 2000; i < 4500; i++) + { + counter.Add(5, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(15, zeroTagsMetricPoint.GetSumLong()); + + int expectedSum; + + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + if (this.shouldReclaimUnusedMetricPoints) + { + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 + expectedSum = 2510; // 502 * 5 + } + else + { + expectedSum = 12500; // 2500 * 5 + } + + Assert.Equal(expectedSum, overflowMetricPoint.GetSumLong()); + } + else + { + Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); + Assert.Equal(12505, overflowMetricPoint.GetSumLong()); // 5 + (2500 * 5) + } + + exportedItems.Clear(); + metricPoints.Clear(); + + // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred + counter.Add(25); + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(25, zeroTagsMetricPoint.GetSumLong()); + } + else + { + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + Assert.Equal(50, zeroTagsMetricPoint.GetSumLong()); + Assert.Equal(12505, overflowMetricPoint.GetSumLong()); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Delta)] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("TestHistogram"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => metricReaderOptions.TemporalityPreference = temporalityPreference) + .Build(); + + // There are two reserved MetricPoints + // 1. For zero tags + // 2. For metric overflow attribute when user opts-in for this feature + + histogram.Record(10); // Record measurement for zero tags + + // Max number for MetricPoints available for use when emitted with tags + int maxMetricPointsForUse = MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault - 2; + + for (int i = 0; i < maxMetricPointsForUse; i++) + { + // Emit unique key-value pairs to use up the available MetricPoints + // Once this loop is run, we have used up all available MetricPoints for metrics emitted with tags + histogram.Record(10, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + var metric = exportedItems[0]; + + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint overflowMetricPoint; + + // We still have not exceeded the max MetricPoint limit + Assert.DoesNotContain(metricPoints, mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + exportedItems.Clear(); + metricPoints.Clear(); + + histogram.Record(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + MetricPoint zeroTagsMetricPoint; + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // Check metric point for zero tags + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + Assert.Equal(10, zeroTagsMetricPoint.GetHistogramSum()); + } + + // Check metric point for overflow + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + Assert.Equal(true, overflowMetricPoint.Tags.KeyAndValues[0].Value); + Assert.Equal(1, overflowMetricPoint.Tags.Count); + Assert.Equal(5, overflowMetricPoint.GetHistogramSum()); + + exportedItems.Clear(); + metricPoints.Clear(); + + histogram.Record(15); // Record another measurement for zero tags + + // Emit 2500 more newer MetricPoints with distinct dimension combinations + for (int i = 2000; i < 4500; i++) + { + histogram.Record(5, new KeyValuePair("Key", i)); + } + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(15, zeroTagsMetricPoint.GetHistogramSum()); + + int expectedCount; + int expectedSum; + + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + if (this.shouldReclaimUnusedMetricPoints) + { + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 + expectedCount = 502; + expectedSum = 2510; // 502 * 5 + } + else + { + expectedCount = 2500; + expectedSum = 12500; // 2500 * 5 + } + + Assert.Equal(expectedCount, overflowMetricPoint.GetHistogramCount()); + Assert.Equal(expectedSum, overflowMetricPoint.GetHistogramSum()); + } + else + { + Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); + + Assert.Equal(2501, overflowMetricPoint.GetHistogramCount()); + Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); // 5 + (2500 * 5) + } + + exportedItems.Clear(); + metricPoints.Clear(); + + // Test that the SDK continues to correctly aggregate the previously registered measurements even after overflow has occurred + histogram.Record(25); + + meterProvider.ForceFlush(); + metric = exportedItems[0]; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + zeroTagsMetricPoint = metricPoints.Single(mp => mp.Tags.Count == 0); + + if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); + } + else + { + overflowMetricPoint = metricPoints.Single(mp => mp.Tags.Count != 0 && mp.Tags.KeyAndValues[0].Key == "otel.metric.overflow"); + + Assert.Equal(50, zeroTagsMetricPoint.GetHistogramSum()); + Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); + } + } +} + +public class MetricOverflowAttributeTests : MetricOverflowAttributeTestsBase +{ + public MetricOverflowAttributeTests() + : base(false) + { + } +} + +public class MetricOverflowAttributeTestsWithReclaimAttribute : MetricOverflowAttributeTestsBase +{ + public MetricOverflowAttributeTestsWithReclaimAttribute() + : base(true) + { + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs index b5ef0b65d02..129b1bfbb09 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs @@ -17,6 +17,8 @@ using System.Collections.Concurrent; using System.Diagnostics.Metrics; using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; @@ -24,6 +26,80 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricPointReclaimTests { + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + + private readonly Dictionary configurationData = new() + { + [ReclaimUnusedMetricPointsConfigKey] = "true", + }; + + private readonly IConfiguration configuration; + + public MetricPointReclaimTests() + { + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(this.configurationData) + .Build(); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestReclaimAttributeConfigWithEnvVar(string value, bool isReclaimAttributeKeySet) + { + // Clear the environment variable value first + Environment.SetEnvironmentVariable(ReclaimUnusedMetricPointsConfigKey, null); + + // Set the environment variable to the value provided in the test input + Environment.SetEnvironmentVariable(ReclaimUnusedMetricPointsConfigKey, value); + + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + } + + [Theory] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("FALSE", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + public void TestReclaimAttributeConfigWithOtherConfigProvider(string value, bool isReclaimAttributeKeySet) + { + var exportedItems = new List(); + + var meter = new Meter(Utils.GetCurrentMethodName()); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [ReclaimUnusedMetricPointsConfigKey] = value }) + .Build(); + + services.AddSingleton(configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -42,6 +118,10 @@ public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) }; using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(Utils.GetCurrentMethodName()) .AddReader(metricReader) .Build(); @@ -131,6 +211,10 @@ public void MeasurementsAreAggregatedAfterMetricPointReclaim(bool emitMetricWith }; using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(Utils.GetCurrentMethodName()) .SetMaxMetricPointsPerMetricStream(10) // Set max MetricPoints limit to 5 .AddReader(metricReader) @@ -152,12 +236,12 @@ void EmitMetric() { int numberOfMeasurements = 0; var random = new Random(); - while (emitMetricWithNoDimension) + while (true) { if (numberOfMeasurements < numberOfMeasurementsPerThread) { // Check for cases where a metric with no dimension is also emitted - if (true) + if (emitMetricWithNoDimension) { counter.Add(25); Interlocked.Add(ref sum, 25); @@ -196,12 +280,12 @@ void EmitMetric() Assert.Equal(sum, exporter.Sum); } - private class ThreadArguments + private sealed class ThreadArguments { public int Counter; } - private class CustomExporter : BaseExporter + private sealed class CustomExporter : BaseExporter { public long Sum = 0; diff --git a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs index dc0810fd338..fad3663f773 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs @@ -15,7 +15,8 @@ // using System.Diagnostics.Metrics; - +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; @@ -24,14 +25,27 @@ namespace OpenTelemetry.Metrics.Tests; #pragma warning disable SA1402 -public abstract class MetricSnapshotTestsBase : IDisposable +public abstract class MetricSnapshotTestsBase { - protected MetricSnapshotTestsBase(bool emitOverflowAttribute) + private readonly IConfiguration configuration; + + protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { + var configurationData = new Dictionary(); + if (emitOverflowAttribute) { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, "true"); + configurationData[MetricTestsBase.EmitOverFlowAttributeConfigKey] = "true"; + } + + if (shouldReclaimUnusedMetricPoints) + { + configurationData[MetricTestsBase.ReclaimUnusedMetricPointsConfigKey] = "true"; } + + this.configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); } [Fact] @@ -43,6 +57,10 @@ public void VerifySnapshot_Counter() using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("meter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -112,6 +130,10 @@ public void VerifySnapshot_Histogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -204,6 +226,10 @@ public void VerifySnapshot_ExponentialHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) .AddMeter(meter.Name) .AddView("histogram", new Base2ExponentialBucketHistogramConfiguration()) .AddInMemoryExporter(exportedMetrics) @@ -292,17 +318,12 @@ public void VerifySnapshot_ExponentialHistogram() Assert.Equal(10, max); AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData()); } - - public void Dispose() - { - Environment.SetEnvironmentVariable(MetricTestsBase.EmitOverFlowAttributeConfigKey, null); - } } public class MetricSnapshotTests : MetricSnapshotTestsBase { public MetricSnapshotTests() - : base(false) + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) { } } @@ -310,7 +331,23 @@ public MetricSnapshotTests() public class MetricSnapshotTestsWithOverflowAttribute : MetricSnapshotTestsBase { public MetricSnapshotTestsWithOverflowAttribute() - : base(true) + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricSnapshotTestsWithReclaimAttribute : MetricSnapshotTestsBase +{ + public MetricSnapshotTestsWithReclaimAttribute() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: true) + { + } +} + +public class MetricSnapshotTestsWithBothAttributes : MetricSnapshotTestsBase +{ + public MetricSnapshotTestsWithBothAttributes() + : base(emitOverflowAttribute: true, shouldReclaimUnusedMetricPoints: true) { } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 6cb5395ed29..19328025704 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -21,6 +21,7 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricTestsBase { public const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + public const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; // This method relies on the assumption that MetricPoints are exported in the order in which they are emitted. // For Delta AggregationTemporality, this holds true only until the AggregatorStore has not begun recaliming the MetricPoints.