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

9 changes: 9 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ Notes](../../RELEASENOTES.md).
`Microsoft.Extensions.Diagnostics.Abstractions` packages version to
`9.0.0-rc.1.24431.7`.
([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853))

reyang marked this conversation as resolved.
Show resolved Hide resolved
* 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. Explicit bucket histogram boundary resolution applies in the following
order:
1. View API
2. Advice API
3. SDK defaults.
([#5854](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5854))

## 1.9.0

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