Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sdk-metrics] Add support for .NET 9 Advice API #5854

14 changes: 14 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ Notes](../../RELEASENOTES.md).
`9.0.0-rc.1.24431.7`.
([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853))

* Added support in metrics for histogram bucket boundaries set via the .NET 9
[InstrumentAdvice<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.instrumentadvice-1)
API.

Note: With this change explicit bucket histogram boundary resolution will
apply in the following order:

1. View API
2. Advice API
3. SDK defaults

See [#5854](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5854)
for details.

## 1.9.0

Released 2024-Jun-14
Expand Down
48 changes: 47 additions & 1 deletion src/OpenTelemetry/Metrics/MetricStreamIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me
this.ViewId = metricStreamConfiguration?.ViewId;
this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}";
this.TagKeys = metricStreamConfiguration?.CopiedTagKeys;
this.HistogramBucketBounds = (metricStreamConfiguration as ExplicitBucketHistogramConfiguration)?.CopiedBoundaries;
this.HistogramBucketBounds = GetExplicitBucketHistogramBounds(instrument, metricStreamConfiguration);
this.ExponentialHistogramMaxSize = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxSize ?? 0;
this.ExponentialHistogramMaxScale = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxScale ?? 0;
this.HistogramRecordMinMax = (metricStreamConfiguration as HistogramConfiguration)?.RecordMinMax ?? true;
Expand Down Expand Up @@ -150,6 +150,52 @@ public bool Equals(MetricStreamIdentity other)

public override readonly int GetHashCode() => this.hashCode;

private static double[]? GetExplicitBucketHistogramBounds(Instrument instrument, MetricStreamConfiguration? metricStreamConfiguration)
{
if (metricStreamConfiguration is ExplicitBucketHistogramConfiguration explicitBucketHistogramConfiguration
&& explicitBucketHistogramConfiguration.CopiedBoundaries != null)
{
return explicitBucketHistogramConfiguration.CopiedBoundaries;
}

return instrument switch
{
Histogram<long> longHistogram => GetExplicitBucketHistogramBoundsFromAdvice(longHistogram),
Histogram<int> intHistogram => GetExplicitBucketHistogramBoundsFromAdvice(intHistogram),
Histogram<short> shortHistogram => GetExplicitBucketHistogramBoundsFromAdvice(shortHistogram),
Histogram<byte> byteHistogram => GetExplicitBucketHistogramBoundsFromAdvice(byteHistogram),
Histogram<float> floatHistogram => GetExplicitBucketHistogramBoundsFromAdvice(floatHistogram),
Histogram<double> doubleHistogram => GetExplicitBucketHistogramBoundsFromAdvice(doubleHistogram),
_ => null,
};
}

private static double[]? GetExplicitBucketHistogramBoundsFromAdvice<T>(Histogram<T> histogram)
where T : struct
{
var adviceExplicitBucketBoundaries = histogram.Advice?.HistogramBucketBoundaries;
if (adviceExplicitBucketBoundaries == null)
{
return null;
}

if (typeof(T) == typeof(double))
{
return ((IReadOnlyList<double>)adviceExplicitBucketBoundaries).ToArray();
}
else
{
double[] explicitBucketBoundaries = new double[adviceExplicitBucketBoundaries.Count];

for (int i = 0; i < adviceExplicitBucketBoundaries.Count; i++)
{
explicitBucketBoundaries[i] = Convert.ToDouble(adviceExplicitBucketBoundaries[i]);
}

return explicitBucketBoundaries;
}
}

private static bool HistogramBoundsEqual(double[]? bounds1, double[]? bounds2)
{
if (ReferenceEquals(bounds1, bounds2))
Expand Down
171 changes: 171 additions & 0 deletions test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,177 @@ public void ViewToProduceCustomHistogramBound()
Assert.Equal(boundaries.Length + 1, actualCount);
}

[Fact]
public void HistogramWithAdviceBoundaries_HandlesAllTypes()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
int counter = 0;

using var container = this.BuildMeterProvider(out var meterProvider, builder =>
{
builder.AddMeter(meter.Name);
builder.AddInMemoryExporter(exportedItems);
});

// Test cases for different histogram types
var histograms = new Instrument[]
{
meter.CreateHistogram<long>("longHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List<long>() { 10, 20 } }),
meter.CreateHistogram<int>("intHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List<int>() { 10, 20 } }),
meter.CreateHistogram<short>("shortHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List<short>() { 10, 20 } }),
meter.CreateHistogram<float>("floatHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List<float>() { 10.0F, 20.0F } }),
meter.CreateHistogram<double>("doubleHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = new List<double>() { 10.0, 20.0 } }),
};

foreach (var histogram in histograms)
{
exportedItems.Clear();

if (histogram is Histogram<long> longHistogram)
{
longHistogram.Record(-10);
longHistogram.Record(9);
longHistogram.Record(19);
}
else if (histogram is Histogram<int> intHistogram)
{
intHistogram.Record(-10);
intHistogram.Record(9);
intHistogram.Record(19);
counter++;
}
else if (histogram is Histogram<short> shortHistogram)
{
shortHistogram.Record(-10);
shortHistogram.Record(9);
shortHistogram.Record(19);
counter++;
}
else if (histogram is Histogram<float> floatHistogram)
{
floatHistogram.Record(-10.0F);
floatHistogram.Record(9.0F);
floatHistogram.Record(19.0F);
counter++;
}
else if (histogram is Histogram<double> doubleHistogram)
{
doubleHistogram.Record(-10.0);
doubleHistogram.Record(9.0);
doubleHistogram.Record(19.0);
counter++;
}

meterProvider.ForceFlush(MaxTimeToAllowForFlush);
var metricCustom = exportedItems[counter];

List<MetricPoint> metricPointsCustom = new List<MetricPoint>();
foreach (ref readonly var mp in metricCustom.GetMetricPoints())
{
metricPointsCustom.Add(mp);
}

Assert.Single(metricPointsCustom);
var histogramPoint = metricPointsCustom[0];

var count = histogramPoint.GetHistogramCount();
var sum = histogramPoint.GetHistogramSum();

Assert.Equal(18, sum);
Assert.Equal(3, count);

var index = 0;
var actualCount = 0;
long[] expectedBucketCounts = new long[] { 2, 1, 0 };

foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets())
{
Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount);
index++;
actualCount++;
}

Assert.Equal(3, actualCount);
}
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void HistogramWithAdviceBoundariesSpecifiedTests(bool useViewToOverride)
{
using var meter = new Meter(Utils.GetCurrentMethodName());
var exportedItems = new List<Metric>();
IReadOnlyList<long> adviceBoundaries = new List<long>() { 5, 10, 20 };
double[] viewBoundaries = new double[] { 10, 20 };

using var container = this.BuildMeterProvider(out var meterProvider, builder =>
{
builder.AddMeter(meter.Name);

if (useViewToOverride)
{
builder.AddView("MyHistogram", new ExplicitBucketHistogramConfiguration { Boundaries = viewBoundaries });
}

builder.AddInMemoryExporter(exportedItems);
});

var histogram = meter.CreateHistogram<long>(
"MyHistogram",
unit: null,
description: null,
tags: null,
new()
{
HistogramBucketBoundaries = adviceBoundaries,
});

histogram.Record(-10);
histogram.Record(0);
histogram.Record(1);
histogram.Record(9);
histogram.Record(10);
histogram.Record(11);
histogram.Record(19);
histogram.Record(22);

meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Single(exportedItems);
var metricCustom = exportedItems[0];

Assert.Equal("MyHistogram", metricCustom.Name);

List<MetricPoint> metricPointsCustom = new List<MetricPoint>();
foreach (ref readonly var mp in metricCustom.GetMetricPoints())
{
metricPointsCustom.Add(mp);
}

Assert.Single(metricPointsCustom);
var histogramPoint = metricPointsCustom[0];

var count = histogramPoint.GetHistogramCount();
var sum = histogramPoint.GetHistogramSum();

Assert.Equal(62, sum);
Assert.Equal(8, count);

var index = 0;
var actualCount = 0;
long[] expectedBucketCounts = useViewToOverride ? new long[] { 5, 2, 1 } : new long[] { 3, 2, 2, 1 };

foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets())
{
Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount);
index++;
actualCount++;
}

Assert.Equal(useViewToOverride ? viewBoundaries.Length + 1 : adviceBoundaries.Count + 1, actualCount);
}

[Fact]
public void ViewToProduceExponentialHistogram()
{
Expand Down
Loading