Make MetricPoint reclaim an opt-in experimental feature (#5052)

This commit is contained in:
Utkarsh Umesan Pillai 2023-11-16 14:51:15 -08:00 committed by GitHub
parent bdd931e08c
commit f2c225519d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 831 additions and 491 deletions

View File

@ -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

View File

@ -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 sealed class AggregatorStore
AggregationTemporality temporality,
int maxMetricPoints,
bool emitOverflowAttribute,
bool shouldReclaimUnusedMetricPoints,
ExemplarFilter? exemplarFilter = null)
{
this.name = metricStreamIdentity.InstrumentName;
@ -122,7 +124,9 @@ internal sealed class AggregatorStore
reservedMetricPointsCount++;
}
if (this.OutputDelta)
this.ShouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints;
if (this.OutputDelta && shouldReclaimUnusedMetricPoints)
{
this.availableMetricPoints = new Queue<int>(maxMetricPoints - reservedMetricPointsCount);
@ -181,7 +185,7 @@ internal sealed class AggregatorStore
this.batchSize = 0;
if (this.OutputDelta)
{
if (this.reclaimMetricPoints)
if (this.ShouldReclaimUnusedMetricPoints && this.reclaimMetricPoints)
{
this.SnapshotDeltaWithMetricPointReclaim();
}

View File

@ -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<object> instrumentations = new();
private readonly List<Func<Instrument, MetricStreamConfiguration?>> viewConfigs;
@ -51,6 +53,7 @@ internal sealed class MeterProviderSdk : MeterProvider
var config = serviceProvider!.GetRequiredService<IConfiguration>();
_ = config.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out bool isEmitOverflowAttributeKeySet);
_ = config.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ShouldReclaimUnusedMetricPoints);
this.ServiceProvider = serviceProvider!;

View File

@ -60,6 +60,7 @@ public sealed class Metric
AggregationTemporality temporality,
int maxMetricPointsPerMetricStream,
bool emitOverflowAttribute,
bool shouldReclaimUnusedMetricPoints,
ExemplarFilter? exemplarFilter = null)
{
this.InstrumentIdentity = instrumentIdentity;
@ -166,7 +167,7 @@ public sealed class 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;
}

View File

@ -64,7 +64,7 @@ public struct 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.");
}

View File

@ -75,7 +75,8 @@ public abstract partial class MetricReader
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 @@ public abstract partial class MetricReader
}
else
{
Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, this.exemplarFilter);
bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints;
Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter);
this.instrumentIdentityToMetric[metricStreamIdentity] = metric;
this.metrics![index] = metric;

View File

@ -29,16 +29,15 @@ public abstract class AggregatorTestsBase
private static readonly MetricStreamIdentity MetricStreamIdentity = new(Instrument, HistogramConfiguration);
private readonly bool emitOverflowAttribute;
private readonly bool shouldReclaimUnusedMetricPoints;
private readonly AggregatorStore aggregatorStore;
protected AggregatorTestsBase(bool emitOverflowAttribute)
protected AggregatorTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints)
{
if (emitOverflowAttribute)
{
this.emitOverflowAttribute = emitOverflowAttribute;
}
this.emitOverflowAttribute = emitOverflowAttribute;
this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints;
this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute);
this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints);
}
[Fact]
@ -268,7 +267,8 @@ public abstract class AggregatorTestsBase
AggregationType.Histogram,
AggregationTemporality.Cumulative,
maxMetricPoints: 1024,
this.emitOverflowAttribute);
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints);
KnownHistogramBuckets actualHistogramBounds = KnownHistogramBuckets.Default;
if (aggregatorStore.HistogramBounds == Metric.DefaultHistogramBoundsShortSeconds)
@ -345,6 +345,7 @@ public abstract class AggregatorTestsBase
aggregationTemporality,
maxMetricPoints: 1024,
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints,
exemplarsEnabled ? new AlwaysOnExemplarFilter() : null);
var expectedHistogram = new Base2ExponentialBucketHistogram();
@ -453,7 +454,8 @@ public abstract class AggregatorTestsBase
AggregationType.Base2ExponentialHistogram,
AggregationTemporality.Cumulative,
maxMetricPoints: 1024,
this.emitOverflowAttribute);
this.emitOverflowAttribute,
this.shouldReclaimUnusedMetricPoints);
aggregatorStore.Update(10, Array.Empty<KeyValuePair<string, object>>());
@ -529,7 +531,7 @@ public abstract class AggregatorTestsBase
public class AggregatorTests : AggregatorTestsBase
{
public AggregatorTests()
: base(false)
: base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false)
{
}
}
@ -537,7 +539,23 @@ public class AggregatorTests : AggregatorTestsBase
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)
{
}
}

View File

@ -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<string, string>();
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
@ -84,6 +102,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
@ -113,6 +135,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems)
.Build();
@ -147,6 +173,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
var exportedItems = new List<Metric>();
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
var exportedItems = new List<Metric>();
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddInMemoryExporter(exportedItems);
if (hasView)
@ -565,6 +635,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("mycounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -667,6 +741,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -741,6 +819,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -838,6 +920,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -879,6 +965,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateUpDownCounter<long>("mycounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -961,6 +1051,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -1025,6 +1119,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
});
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -1095,6 +1193,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("Counter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -1186,6 +1288,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}");
var counterLong = meter.CreateCounter<long>("Counter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -1279,6 +1385,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
var counter1 = meter1.CreateCounter<long>("counterFromMeter1");
var counter2 = meter2.CreateCounter<long>("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 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}");
var counterLong = meter.CreateCounter<long>("mycounterCapTest");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
{
@ -1443,6 +1557,10 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
// 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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
var exportedItems = new List<Metric>();
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 abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
Assert.Empty(exportedItems);
}
public void Dispose()
{
Environment.SetEnvironmentVariable(EmitOverFlowAttributeConfigKey, null);
}
private static void CounterUpdateThread<T>(object obj)
where T : struct, IComparable
{
@ -1705,7 +1830,7 @@ public abstract class MetricApiTestsBase : MetricTestsBase, IDisposable
public class MetricApiTest : MetricApiTestsBase
{
public MetricApiTest(ITestOutputHelper output)
: base(output, false)
: base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false)
{
}
}
@ -1713,7 +1838,23 @@ public class MetricApiTest : MetricApiTestsBase
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)
{
}
}

View File

@ -1,447 +0,0 @@
// <copyright file="MetricOverflowAttributeTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("TestCounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value })
.Build();
services.AddSingleton<IConfiguration>(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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<string, object>("Key", i));
}
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var metricPoints = new List<MetricPoint>();
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<string, object>("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<string, object>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<long>("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<string, object>("Key", i));
}
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var metricPoints = new List<MetricPoint>();
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<string, object>("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<string, object>("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);
}
}
}

View File

@ -0,0 +1,489 @@
// <copyright file="MetricOverflowAttributeTestsBase.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, string> 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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("TestCounter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [MetricTestsBase.EmitOverFlowAttributeConfigKey] = value })
.Build();
services.AddSingleton<IConfiguration>(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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("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<string, object>("Key", i));
}
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var metricPoints = new List<MetricPoint>();
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<string, object>("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<string, object>("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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<long>("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<string, object>("Key", i));
}
meterProvider.ForceFlush();
Assert.Single(exportedItems);
var metric = exportedItems[0];
var metricPoints = new List<MetricPoint>();
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<string, object>("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<string, object>("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)
{
}
}

View File

@ -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<string, string> 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<Metric>();
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<Metric>();
var meter = new Meter(Utils.GetCurrentMethodName());
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [ReclaimUnusedMetricPointsConfigKey] = value })
.Build();
services.AddSingleton<IConfiguration>(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 class MetricPointReclaimTests
};
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(Utils.GetCurrentMethodName())
.AddReader(metricReader)
.Build();
@ -131,6 +211,10 @@ public class MetricPointReclaimTests
};
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 @@ public class MetricPointReclaimTests
{
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 @@ public class MetricPointReclaimTests
Assert.Equal(sum, exporter.Sum);
}
private class ThreadArguments
private sealed class ThreadArguments
{
public int Counter;
}
private class CustomExporter : BaseExporter<Metric>
private sealed class CustomExporter : BaseExporter<Metric>
{
public long Sum = 0;

View File

@ -15,7 +15,8 @@
// </copyright>
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<string, string>();
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 abstract class MetricSnapshotTestsBase : IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var counter = meter.CreateCounter<long>("meter");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedMetrics)
.AddInMemoryExporter(exportedSnapshots)
@ -112,6 +130,10 @@ public abstract class MetricSnapshotTestsBase : IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<int>("histogram");
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services =>
{
services.AddSingleton(this.configuration);
})
.AddMeter(meter.Name)
.AddInMemoryExporter(exportedMetrics)
.AddInMemoryExporter(exportedSnapshots)
@ -204,6 +226,10 @@ public abstract class MetricSnapshotTestsBase : IDisposable
using var meter = new Meter(Utils.GetCurrentMethodName());
var histogram = meter.CreateHistogram<int>("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 abstract class MetricSnapshotTestsBase : IDisposable
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 class MetricSnapshotTests : MetricSnapshotTestsBase
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)
{
}
}

View File

@ -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.