Make MetricPoint reclaim an opt-in experimental feature (#5052)
This commit is contained in:
parent
bdd931e08c
commit
f2c225519d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue