Skip to content

Commit

Permalink
[sdk-metrics] Add support for .NET 9 Advice API (#5854)
Browse files Browse the repository at this point in the history
Co-authored-by: Mikel Blanchard <mblanchard@macrosssoftware.com>
Co-authored-by: Reiley Yang <reyang@microsoft.com>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 1c02da7 commit d650352
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 1 deletion.
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&lt;T&gt;](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

0 comments on commit d650352

Please sign in to comment.