874 lines
36 KiB
C#
874 lines
36 KiB
C#
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#nullable enable
|
|
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Metrics;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using OpenTelemetry.Tests;
|
|
using Xunit;
|
|
|
|
namespace OpenTelemetry.Metrics.Tests;
|
|
|
|
public class MetricExemplarTests : MetricTestsBase
|
|
{
|
|
private const int MaxTimeToAllowForFlush = 10000;
|
|
|
|
[Theory]
|
|
[InlineData(null, null, null)]
|
|
[InlineData(null, "always_off", (int)ExemplarFilterType.AlwaysOff)]
|
|
[InlineData(null, "ALWays_ON", (int)ExemplarFilterType.AlwaysOn)]
|
|
[InlineData(null, "trace_based", (int)ExemplarFilterType.TraceBased)]
|
|
[InlineData(null, "invalid", null)]
|
|
[InlineData((int)ExemplarFilterType.AlwaysOn, "trace_based", (int)ExemplarFilterType.AlwaysOn)]
|
|
public void TestExemplarFilterSetFromConfiguration(
|
|
int? programmaticValue,
|
|
string? configValue,
|
|
int? expectedValue)
|
|
{
|
|
var configBuilder = new ConfigurationBuilder();
|
|
if (!string.IsNullOrEmpty(configValue))
|
|
{
|
|
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
[MeterProviderSdk.ExemplarFilterConfigKey] = configValue,
|
|
[MeterProviderSdk.ExemplarFilterHistogramsConfigKey] = configValue,
|
|
});
|
|
}
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, b =>
|
|
{
|
|
b.ConfigureServices(
|
|
s => s.AddSingleton<IConfiguration>(configBuilder.Build()));
|
|
|
|
if (programmaticValue.HasValue)
|
|
{
|
|
b.SetExemplarFilter(((ExemplarFilterType?)programmaticValue).Value);
|
|
}
|
|
});
|
|
|
|
var meterProviderSdk = meterProvider as MeterProviderSdk;
|
|
|
|
Assert.NotNull(meterProviderSdk);
|
|
Assert.Equal((ExemplarFilterType?)expectedValue, meterProviderSdk.ExemplarFilter);
|
|
if (programmaticValue.HasValue)
|
|
{
|
|
Assert.False(meterProviderSdk.ExemplarFilterForHistograms.HasValue);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal((ExemplarFilterType?)expectedValue, meterProviderSdk.ExemplarFilterForHistograms);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(MetricReaderTemporalityPreference.Cumulative)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta)]
|
|
public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality)
|
|
{
|
|
DateTime testStartTime = DateTime.UtcNow;
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
var counterDouble = meter.CreateCounter<double>("testCounterDouble");
|
|
var counterLong = meter.CreateCounter<long>("testCounterLong");
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
|
|
.AddView(i =>
|
|
{
|
|
if (i.Name.StartsWith("testCounter"))
|
|
{
|
|
return new MetricStreamConfiguration
|
|
{
|
|
ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3),
|
|
};
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
|
|
{
|
|
metricReaderOptions.TemporalityPreference = temporality;
|
|
}));
|
|
|
|
var measurementValues = GenerateRandomValues(2, false, null);
|
|
foreach (var value in measurementValues)
|
|
{
|
|
counterDouble.Add(value.Value);
|
|
counterLong.Add((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateFirstPhase("testCounterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue);
|
|
ValidateFirstPhase("testCounterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue);
|
|
|
|
exportedItems.Clear();
|
|
|
|
#if NETFRAMEWORK
|
|
Thread.Sleep(10); // Compensates for low resolution timing in netfx.
|
|
#endif
|
|
|
|
var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues);
|
|
foreach (var value in secondMeasurementValues)
|
|
{
|
|
using var act = new Activity("test").Start();
|
|
counterDouble.Add(value.Value);
|
|
counterLong.Add((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateSecondPhase("testCounterDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.DoubleValue);
|
|
ValidateSecondPhase("testCounterLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.LongValue);
|
|
|
|
void ValidateFirstPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues,
|
|
Func<Exemplar, double> getExemplarValueFunc)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, getExemplarValueFunc);
|
|
}
|
|
|
|
void ValidateSecondPhase(
|
|
string instrumentName,
|
|
MetricReaderTemporalityPreference temporality,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] firstMeasurementValues,
|
|
(double Value, bool ExpectTraceId)[] secondMeasurementValues,
|
|
Func<Exemplar, double> getExemplarValueFunc)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
if (temporality == MetricReaderTemporalityPreference.Cumulative)
|
|
{
|
|
// Current design:
|
|
// First collect we saw Exemplar A & B
|
|
// Second collect we saw Exemplar C but B remained in the reservoir
|
|
Assert.Equal(2, exemplars.Count);
|
|
secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(1)).ToArray();
|
|
}
|
|
else
|
|
{
|
|
Assert.Single(exemplars);
|
|
}
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, getExemplarValueFunc);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(MetricReaderTemporalityPreference.Cumulative)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta)]
|
|
public void TestExemplarsObservable(MetricReaderTemporalityPreference temporality)
|
|
{
|
|
DateTime testStartTime = DateTime.UtcNow;
|
|
var exportedItems = new List<Metric>();
|
|
|
|
(double Value, bool ExpectTraceId)[] measurementValues = new (double Value, bool ExpectTraceId)[]
|
|
{
|
|
(18D, false),
|
|
(19D, false),
|
|
};
|
|
|
|
int measurementIndex = 0;
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
var gaugeDouble = meter.CreateObservableGauge("testGaugeDouble", () => measurementValues[measurementIndex].Value);
|
|
var gaugeLong = meter.CreateObservableGauge("testGaugeLong", () => (long)measurementValues[measurementIndex].Value);
|
|
var counterDouble = meter.CreateObservableCounter("counterDouble", () => measurementValues[measurementIndex].Value);
|
|
var counterLong = meter.CreateObservableCounter("counterLong", () => (long)measurementValues[measurementIndex].Value);
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
|
|
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
|
|
{
|
|
metricReaderOptions.TemporalityPreference = temporality;
|
|
}));
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateFirstPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue);
|
|
ValidateFirstPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue);
|
|
ValidateFirstPhase("counterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue);
|
|
ValidateFirstPhase("counterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue);
|
|
|
|
exportedItems.Clear();
|
|
|
|
measurementIndex++;
|
|
|
|
#if NETFRAMEWORK
|
|
Thread.Sleep(10); // Compensates for low resolution timing in netfx.
|
|
#endif
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateSecondPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue);
|
|
ValidateSecondPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue);
|
|
|
|
void ValidateFirstPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues,
|
|
Func<Exemplar, double> getExemplarValueFunc)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Take(1), getExemplarValueFunc);
|
|
}
|
|
|
|
static void ValidateSecondPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues,
|
|
Func<Exemplar, double> getExemplarValueFunc)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
// Note: Gauges are only observed when collection happens. For
|
|
// Cumulative & Delta the behavior will be the same. We will record the
|
|
// single measurement each time as the only exemplar.
|
|
|
|
Assert.Single(exemplars);
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Skip(1), getExemplarValueFunc);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(MetricReaderTemporalityPreference.Cumulative, null)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta, null)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta, "always_on")]
|
|
public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference temporality, string? configValue)
|
|
{
|
|
DateTime testStartTime = DateTime.UtcNow;
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
var histogramWithBucketsAndMinMaxDouble = meter.CreateHistogram<double>("histogramWithBucketsAndMinMaxDouble");
|
|
var histogramWithBucketsDouble = meter.CreateHistogram<double>("histogramWithBucketsDouble");
|
|
var histogramWithBucketsAndMinMaxLong = meter.CreateHistogram<long>("histogramWithBucketsAndMinMaxLong");
|
|
var histogramWithBucketsLong = meter.CreateHistogram<long>("histogramWithBucketsLong");
|
|
|
|
var buckets = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
|
|
|
var configBuilder = new ConfigurationBuilder();
|
|
if (!string.IsNullOrEmpty(configValue))
|
|
{
|
|
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
[MeterProviderSdk.ExemplarFilterConfigKey] = "always_off",
|
|
[MeterProviderSdk.ExemplarFilterHistogramsConfigKey] = configValue,
|
|
});
|
|
}
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder =>
|
|
{
|
|
if (string.IsNullOrEmpty(configValue))
|
|
{
|
|
builder.SetExemplarFilter(ExemplarFilterType.AlwaysOn);
|
|
}
|
|
|
|
builder
|
|
.ConfigureServices(s => s.AddSingleton<IConfiguration>(configBuilder.Build()))
|
|
.AddMeter(meter.Name)
|
|
.AddView(i =>
|
|
{
|
|
if (i.Name.StartsWith("histogramWithBucketsAndMinMax"))
|
|
{
|
|
return new ExplicitBucketHistogramConfiguration
|
|
{
|
|
Boundaries = buckets,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new ExplicitBucketHistogramConfiguration
|
|
{
|
|
Boundaries = buckets,
|
|
RecordMinMax = false,
|
|
};
|
|
}
|
|
})
|
|
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
|
|
{
|
|
metricReaderOptions.TemporalityPreference = temporality;
|
|
});
|
|
});
|
|
|
|
var measurementValues = buckets
|
|
/* 2000 is here to test overflow measurement */
|
|
.Concat(new double[] { 2000 })
|
|
.Select(b => (Value: b, ExpectTraceId: false))
|
|
.ToArray();
|
|
foreach (var value in measurementValues)
|
|
{
|
|
histogramWithBucketsAndMinMaxDouble.Record(value.Value);
|
|
histogramWithBucketsDouble.Record(value.Value);
|
|
histogramWithBucketsAndMinMaxLong.Record((long)value.Value);
|
|
histogramWithBucketsLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateFirstPhase("histogramWithBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithBucketsDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithBucketsLong", testStartTime, exportedItems, measurementValues);
|
|
|
|
exportedItems.Clear();
|
|
|
|
#if NETFRAMEWORK
|
|
Thread.Sleep(10); // Compensates for low resolution timing in netfx.
|
|
#endif
|
|
|
|
var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray();
|
|
foreach (var value in secondMeasurementValues)
|
|
{
|
|
using var act = new Activity("test").Start();
|
|
histogramWithBucketsAndMinMaxDouble.Record(value.Value);
|
|
histogramWithBucketsDouble.Record(value.Value);
|
|
histogramWithBucketsAndMinMaxLong.Record((long)value.Value);
|
|
histogramWithBucketsLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateScondPhase("histogramWithBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateScondPhase("histogramWithBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateScondPhase("histogramWithBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateScondPhase("histogramWithBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
|
|
static void ValidateFirstPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue);
|
|
}
|
|
|
|
static void ValidateScondPhase(
|
|
string instrumentName,
|
|
MetricReaderTemporalityPreference temporality,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] firstMeasurementValues,
|
|
(double Value, bool ExpectTraceId)[] secondMeasurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
if (temporality == MetricReaderTemporalityPreference.Cumulative)
|
|
{
|
|
Assert.Equal(11, exemplars.Count);
|
|
secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray();
|
|
}
|
|
else
|
|
{
|
|
Assert.Single(exemplars);
|
|
}
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(MetricReaderTemporalityPreference.Cumulative)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta)]
|
|
public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreference temporality)
|
|
{
|
|
DateTime testStartTime = DateTime.UtcNow;
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
var histogramWithoutBucketsAndMinMaxDouble = meter.CreateHistogram<double>("histogramWithoutBucketsAndMinMaxDouble");
|
|
var histogramWithoutBucketsDouble = meter.CreateHistogram<double>("histogramWithoutBucketsDouble");
|
|
var histogramWithoutBucketsAndMinMaxLong = meter.CreateHistogram<long>("histogramWithoutBucketsAndMinMaxLong");
|
|
var histogramWithoutBucketsLong = meter.CreateHistogram<long>("histogramWithoutBucketsLong");
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
|
|
.AddView(i =>
|
|
{
|
|
if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax"))
|
|
{
|
|
return new ExplicitBucketHistogramConfiguration
|
|
{
|
|
Boundaries = Array.Empty<double>(),
|
|
ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3),
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new ExplicitBucketHistogramConfiguration
|
|
{
|
|
Boundaries = Array.Empty<double>(),
|
|
RecordMinMax = false,
|
|
ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3),
|
|
};
|
|
}
|
|
})
|
|
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
|
|
{
|
|
metricReaderOptions.TemporalityPreference = temporality;
|
|
}));
|
|
|
|
var measurementValues = GenerateRandomValues(2, false, null);
|
|
foreach (var value in measurementValues)
|
|
{
|
|
histogramWithoutBucketsAndMinMaxDouble.Record(value.Value);
|
|
histogramWithoutBucketsDouble.Record(value.Value);
|
|
histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value);
|
|
histogramWithoutBucketsLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateFirstPhase("histogramWithoutBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithoutBucketsDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithoutBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("histogramWithoutBucketsLong", testStartTime, exportedItems, measurementValues);
|
|
|
|
exportedItems.Clear();
|
|
|
|
#if NETFRAMEWORK
|
|
Thread.Sleep(10); // Compensates for low resolution timing in netfx.
|
|
#endif
|
|
|
|
var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues);
|
|
foreach (var value in secondMeasurementValues)
|
|
{
|
|
using var act = new Activity("test").Start();
|
|
histogramWithoutBucketsAndMinMaxDouble.Record(value.Value);
|
|
histogramWithoutBucketsDouble.Record(value.Value);
|
|
histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value);
|
|
histogramWithoutBucketsLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateSecondPhase("histogramWithoutBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("histogramWithoutBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("histogramWithoutBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("histogramWithoutBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
|
|
static void ValidateFirstPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue);
|
|
}
|
|
|
|
static void ValidateSecondPhase(
|
|
string instrumentName,
|
|
MetricReaderTemporalityPreference temporality,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] firstMeasurementValues,
|
|
(double Value, bool ExpectTraceId)[] secondMeasurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
if (temporality == MetricReaderTemporalityPreference.Cumulative)
|
|
{
|
|
Assert.Equal(2, exemplars.Count);
|
|
secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray();
|
|
}
|
|
else
|
|
{
|
|
Assert.Single(exemplars);
|
|
}
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(MetricReaderTemporalityPreference.Cumulative)]
|
|
[InlineData(MetricReaderTemporalityPreference.Delta)]
|
|
public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference temporality)
|
|
{
|
|
DateTime testStartTime = DateTime.UtcNow;
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
var exponentialHistogramWithMinMaxDouble = meter.CreateHistogram<double>("exponentialHistogramWithMinMaxDouble");
|
|
var exponentialHistogramDouble = meter.CreateHistogram<double>("exponentialHistogramDouble");
|
|
var exponentialHistogramWithMinMaxLong = meter.CreateHistogram<long>("exponentialHistogramWithMinMaxLong");
|
|
var exponentialHistogramLong = meter.CreateHistogram<long>("exponentialHistogramLong");
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
|
|
.AddView(i =>
|
|
{
|
|
if (i.Name.StartsWith("exponentialHistogramWithMinMax"))
|
|
{
|
|
return new Base2ExponentialBucketHistogramConfiguration();
|
|
}
|
|
else
|
|
{
|
|
return new Base2ExponentialBucketHistogramConfiguration()
|
|
{
|
|
RecordMinMax = false,
|
|
};
|
|
}
|
|
})
|
|
.AddInMemoryExporter(exportedItems, metricReaderOptions =>
|
|
{
|
|
metricReaderOptions.TemporalityPreference = temporality;
|
|
}));
|
|
|
|
var measurementValues = GenerateRandomValues(20, false, null);
|
|
foreach (var value in measurementValues)
|
|
{
|
|
exponentialHistogramWithMinMaxDouble.Record(value.Value);
|
|
exponentialHistogramDouble.Record(value.Value);
|
|
exponentialHistogramWithMinMaxLong.Record((long)value.Value);
|
|
exponentialHistogramLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateFirstPhase("exponentialHistogramWithMinMaxDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("exponentialHistogramDouble", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("exponentialHistogramWithMinMaxLong", testStartTime, exportedItems, measurementValues);
|
|
ValidateFirstPhase("exponentialHistogramLong", testStartTime, exportedItems, measurementValues);
|
|
|
|
exportedItems.Clear();
|
|
|
|
#if NETFRAMEWORK
|
|
Thread.Sleep(10); // Compensates for low resolution timing in netfx.
|
|
#endif
|
|
|
|
var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues);
|
|
foreach (var value in secondMeasurementValues)
|
|
{
|
|
using var act = new Activity("test").Start();
|
|
exponentialHistogramWithMinMaxDouble.Record(value.Value);
|
|
exponentialHistogramDouble.Record(value.Value);
|
|
exponentialHistogramWithMinMaxLong.Record((long)value.Value);
|
|
exponentialHistogramLong.Record((long)value.Value);
|
|
}
|
|
|
|
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
|
|
|
|
ValidateSecondPhase("exponentialHistogramWithMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("exponentialHistogramDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("exponentialHistogramWithMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
ValidateSecondPhase("exponentialHistogramLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues);
|
|
|
|
static void ValidateFirstPhase(
|
|
string instrumentName,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] measurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue);
|
|
}
|
|
|
|
static void ValidateSecondPhase(
|
|
string instrumentName,
|
|
MetricReaderTemporalityPreference temporality,
|
|
DateTime testStartTime,
|
|
List<Metric> exportedItems,
|
|
(double Value, bool ExpectTraceId)[] firstMeasurementValues,
|
|
(double Value, bool ExpectTraceId)[] secondMeasurementValues)
|
|
{
|
|
var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName));
|
|
|
|
Assert.NotNull(metricPoint);
|
|
Assert.True(metricPoint.Value.StartTime >= testStartTime);
|
|
Assert.True(metricPoint.Value.EndTime != default);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
if (temporality == MetricReaderTemporalityPreference.Cumulative)
|
|
{
|
|
Assert.Equal(20, exemplars.Count);
|
|
secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(19)).ToArray();
|
|
}
|
|
else
|
|
{
|
|
Assert.Single(exemplars);
|
|
}
|
|
|
|
ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void TestTraceBasedExemplarFilter(bool enableTracing)
|
|
{
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
|
|
var counter = meter.CreateCounter<long>("testCounter");
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.TraceBased)
|
|
.AddInMemoryExporter(exportedItems));
|
|
|
|
if (enableTracing)
|
|
{
|
|
using var act = new Activity("test").Start();
|
|
act.ActivityTraceFlags = ActivityTraceFlags.Recorded;
|
|
counter.Add(18);
|
|
}
|
|
else
|
|
{
|
|
counter.Add(18);
|
|
}
|
|
|
|
meterProvider.ForceFlush();
|
|
|
|
Assert.Single(exportedItems);
|
|
|
|
var metricPoint = GetFirstMetricPoint(exportedItems);
|
|
|
|
Assert.NotNull(metricPoint);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
if (enableTracing)
|
|
{
|
|
Assert.Single(exemplars);
|
|
}
|
|
else
|
|
{
|
|
Assert.Empty(exemplars);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void TestExemplarsFilterTags(bool enableTagFiltering)
|
|
{
|
|
var exportedItems = new List<Metric>();
|
|
|
|
using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
|
|
|
|
var histogram = meter.CreateHistogram<double>("testHistogram");
|
|
|
|
TestExemplarReservoir? testExemplarReservoir = null;
|
|
|
|
using var container = this.BuildMeterProvider(out var meterProvider, builder => builder
|
|
.AddMeter(meter.Name)
|
|
.SetExemplarFilter(ExemplarFilterType.AlwaysOn)
|
|
.AddView(
|
|
histogram.Name,
|
|
new MetricStreamConfiguration()
|
|
{
|
|
TagKeys = enableTagFiltering ? new string[] { "key1" } : null,
|
|
ExemplarReservoirFactory = () =>
|
|
{
|
|
if (testExemplarReservoir != null)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
return testExemplarReservoir = new TestExemplarReservoir();
|
|
},
|
|
})
|
|
.AddInMemoryExporter(exportedItems));
|
|
|
|
histogram.Record(
|
|
0,
|
|
new("key1", "value1"),
|
|
new("key2", "value2"),
|
|
new("key3", "value3"));
|
|
|
|
meterProvider.ForceFlush();
|
|
|
|
Assert.NotNull(testExemplarReservoir);
|
|
Assert.NotNull(testExemplarReservoir.MeasurementTags);
|
|
Assert.Equal(3, testExemplarReservoir.MeasurementTags.Length);
|
|
Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key1" && (string?)t.Value == "value1");
|
|
Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key2" && (string?)t.Value == "value2");
|
|
Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key3" && (string?)t.Value == "value3");
|
|
|
|
var metricPoint = GetFirstMetricPoint(exportedItems);
|
|
|
|
Assert.NotNull(metricPoint);
|
|
|
|
var exemplars = GetExemplars(metricPoint.Value);
|
|
|
|
Assert.NotNull(exemplars);
|
|
|
|
foreach (var exemplar in exemplars)
|
|
{
|
|
if (!enableTagFiltering)
|
|
{
|
|
Assert.Equal(0, exemplar.FilteredTags.MaximumCount);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal(3, exemplar.FilteredTags.MaximumCount);
|
|
|
|
var filteredTags = exemplar.FilteredTags.ToReadOnlyList();
|
|
|
|
Assert.Equal(2, filteredTags.Count);
|
|
|
|
Assert.Contains(new("key2", "value2"), filteredTags);
|
|
Assert.Contains(new("key3", "value3"), filteredTags);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static (double Value, bool ExpectTraceId)[] GenerateRandomValues(
|
|
int count,
|
|
bool expectTraceId,
|
|
(double Value, bool ExpectTraceId)[]? previousValues)
|
|
{
|
|
var random = new Random();
|
|
var values = new (double, bool)[count];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var nextValue = random.NextDouble() * 100_000;
|
|
if (values.Any(m => m.Item1 == nextValue || m.Item1 == (long)nextValue)
|
|
|| previousValues?.Any(m => m.Value == nextValue || m.Value == (long)nextValue) == true)
|
|
{
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
values[i] = (nextValue, expectTraceId);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static void ValidateExemplars(
|
|
IReadOnlyList<Exemplar> exemplars,
|
|
DateTimeOffset startTime,
|
|
DateTimeOffset endTime,
|
|
IEnumerable<(double Value, bool ExpectTraceId)> measurementValues,
|
|
Func<Exemplar, double> getExemplarValueFunc)
|
|
{
|
|
int count = 0;
|
|
|
|
foreach (var exemplar in exemplars)
|
|
{
|
|
Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}");
|
|
Assert.Equal(0, exemplar.FilteredTags.MaximumCount);
|
|
|
|
var measurement = measurementValues.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar)
|
|
|| (long)v.Value == getExemplarValueFunc(exemplar));
|
|
Assert.NotEqual(default, measurement);
|
|
if (measurement.ExpectTraceId)
|
|
{
|
|
Assert.NotEqual(default, exemplar.TraceId);
|
|
Assert.NotEqual(default, exemplar.SpanId);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal(default, exemplar.TraceId);
|
|
Assert.Equal(default, exemplar.SpanId);
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
Assert.Equal(measurementValues.Count(), count);
|
|
}
|
|
|
|
private sealed class TestExemplarReservoir : FixedSizeExemplarReservoir
|
|
{
|
|
public TestExemplarReservoir()
|
|
: base(1)
|
|
{
|
|
}
|
|
|
|
public KeyValuePair<string, object?>[]? MeasurementTags { get; private set; }
|
|
|
|
public override void Offer(in ExemplarMeasurement<double> measurement)
|
|
{
|
|
this.MeasurementTags = measurement.Tags.ToArray();
|
|
|
|
this.UpdateExemplar(0, in measurement);
|
|
}
|
|
|
|
public override void Offer(in ExemplarMeasurement<long> measurement)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
}
|