OTLP support for exporting exponential histograms (#4337)

This commit is contained in:
Alan West 2023-03-27 21:23:24 -07:00 committed by GitHub
parent 87a33138ed
commit 410532ae49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 269 additions and 26 deletions

View File

@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>
using System.Diagnostics.Metrics;
using Examples.AspNetCore;
using OpenTelemetry.Exporter;
using OpenTelemetry.Instrumentation.AspNetCore;
@ -33,6 +34,9 @@ var metricsExporter = appBuilder.Configuration.GetValue<string>("UseMetricsExpor
// Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json.
var logExporter = appBuilder.Configuration.GetValue<string>("UseLogExporter").ToLowerInvariant();
// Note: Switch between Explicit/Exponential by setting HistogramAggregation in appsettings.json
var histogramAggregation = appBuilder.Configuration.GetValue<string>("HistogramAggregation").ToLowerInvariant();
// Build a resource configuration action to set service information.
Action<ResourceBuilder> configureResource = r => r.AddService(
serviceName: appBuilder.Configuration.GetValue<string>("ServiceName"),
@ -111,6 +115,22 @@ appBuilder.Services.AddOpenTelemetry()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
switch (histogramAggregation)
{
case "exponential":
builder.AddView(instrument =>
{
return instrument.GetType().GetGenericTypeDefinition() == typeof(Histogram<>)
? new Base2ExponentialBucketHistogramConfiguration()
: null;
});
break;
default:
// Explicit bounds histogram is the default.
// No additional configuration necessary.
break;
}
switch (metricsExporter)
{
case "prometheus":

View File

@ -14,6 +14,7 @@
"UseTracingExporter": "console",
"UseMetricsExporter": "console",
"UseLogExporter": "console",
"HistogramAggregation": "explicit",
"Jaeger": {
"AgentHost": "localhost",
"AgentPort": 6831,

View File

@ -92,7 +92,7 @@ namespace OpenTelemetry.Exporter
var metricType = metric.MetricType;
if (metricType.IsHistogram())
if (metricType == MetricType.Histogram || metricType == MetricType.ExponentialHistogram)
{
var bucketsBuilder = new StringBuilder();
var sum = metricPoint.GetHistogramSum();
@ -105,41 +105,49 @@ namespace OpenTelemetry.Exporter
bucketsBuilder.AppendLine();
bool isFirstIteration = true;
double previousExplicitBound = default;
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
if (metricType == MetricType.Histogram)
{
if (isFirstIteration)
bool isFirstIteration = true;
double previousExplicitBound = default;
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
{
bucketsBuilder.Append("(-Infinity,");
bucketsBuilder.Append(histogramMeasurement.ExplicitBound);
bucketsBuilder.Append(']');
bucketsBuilder.Append(':');
bucketsBuilder.Append(histogramMeasurement.BucketCount);
previousExplicitBound = histogramMeasurement.ExplicitBound;
isFirstIteration = false;
}
else
{
bucketsBuilder.Append('(');
bucketsBuilder.Append(previousExplicitBound);
bucketsBuilder.Append(',');
if (histogramMeasurement.ExplicitBound != double.PositiveInfinity)
if (isFirstIteration)
{
bucketsBuilder.Append("(-Infinity,");
bucketsBuilder.Append(histogramMeasurement.ExplicitBound);
bucketsBuilder.Append(']');
bucketsBuilder.Append(':');
bucketsBuilder.Append(histogramMeasurement.BucketCount);
previousExplicitBound = histogramMeasurement.ExplicitBound;
isFirstIteration = false;
}
else
{
bucketsBuilder.Append("+Infinity");
bucketsBuilder.Append('(');
bucketsBuilder.Append(previousExplicitBound);
bucketsBuilder.Append(',');
if (histogramMeasurement.ExplicitBound != double.PositiveInfinity)
{
bucketsBuilder.Append(histogramMeasurement.ExplicitBound);
previousExplicitBound = histogramMeasurement.ExplicitBound;
}
else
{
bucketsBuilder.Append("+Infinity");
}
bucketsBuilder.Append(']');
bucketsBuilder.Append(':');
bucketsBuilder.Append(histogramMeasurement.BucketCount);
}
bucketsBuilder.Append(']');
bucketsBuilder.Append(':');
bucketsBuilder.Append(histogramMeasurement.BucketCount);
bucketsBuilder.AppendLine();
}
bucketsBuilder.AppendLine();
}
else
{
// TODO: Consider how/if to display buckets for exponential histograms.
bucketsBuilder.AppendLine("Buckets are not displayed for exponential histograms.");
}
valueDisplay = bucketsBuilder.ToString();

View File

@ -2,6 +2,10 @@
## Unreleased
* Add support for exporting histograms aggregated using the
[Base2 Exponential Bucket Histogram Aggregation](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#base2-exponential-bucket-histogram-aggregation).
([#4337](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4337))
* Added support to set `TraceState` when converting the
System.Diagnostics.Activity object to its corresponding
OpenTelemetry.Proto.Trace.V1.Span object.

View File

@ -307,6 +307,57 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
otlpMetric.Histogram = histogram;
break;
}
case MetricType.ExponentialHistogram:
{
var histogram = new OtlpMetrics.ExponentialHistogram
{
AggregationTemporality = temporality,
};
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var dataPoint = new OtlpMetrics.ExponentialHistogramDataPoint
{
StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(),
TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(),
};
AddAttributes(metricPoint.Tags, dataPoint.Attributes);
dataPoint.Count = (ulong)metricPoint.GetHistogramCount();
dataPoint.Sum = metricPoint.GetHistogramSum();
if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max))
{
dataPoint.Min = min;
dataPoint.Max = max;
}
var exponentialHistogramData = metricPoint.GetExponentialHistogramData();
dataPoint.Scale = exponentialHistogramData.Scale;
dataPoint.Positive = new OtlpMetrics.ExponentialHistogramDataPoint.Types.Buckets();
dataPoint.Positive.Offset = exponentialHistogramData.PositiveBuckets.Offset;
foreach (var bucketCount in exponentialHistogramData.PositiveBuckets)
{
dataPoint.Positive.BucketCounts.Add((ulong)bucketCount);
}
dataPoint.Negative = new OtlpMetrics.ExponentialHistogramDataPoint.Types.Buckets();
dataPoint.Negative.Offset = exponentialHistogramData.NegativeBuckets.Offset;
foreach (var bucketCount in exponentialHistogramData.NegativeBuckets)
{
dataPoint.Negative.BucketCounts.Add((ulong)bucketCount);
}
// TODO: exemplars.
histogram.DataPoints.Add(dataPoint);
}
otlpMetric.ExponentialHistogram = histogram;
break;
}
}
return otlpMetric;

View File

@ -36,6 +36,13 @@ namespace OpenTelemetry.Exporter.Prometheus
public static int WriteMetric(byte[] buffer, int cursor, Metric metric)
{
if (metric.MetricType == MetricType.ExponentialHistogram)
{
// Exponential histograms are not yet support by Prometheus.
// They are ignored for now.
return cursor;
}
int metricType = (int)metric.MetricType >> 4;
cursor = WriteTypeMetadata(buffer, cursor, metric.Name, metric.Unit, MetricTypes[metricType]);
cursor = WriteUnitMetadata(buffer, cursor, metric.Name, metric.Unit);

View File

@ -2,6 +2,12 @@ OpenTelemetry.Metrics.AlwaysOffExemplarFilter
OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
OpenTelemetry.Metrics.AlwaysOnExemplarFilter
OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void
OpenTelemetry.Metrics.Exemplar
OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
OpenTelemetry.Metrics.Exemplar.Exemplar() -> void

View File

@ -2,6 +2,12 @@ OpenTelemetry.Metrics.AlwaysOffExemplarFilter
OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
OpenTelemetry.Metrics.AlwaysOnExemplarFilter
OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void
OpenTelemetry.Metrics.Exemplar
OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
OpenTelemetry.Metrics.Exemplar.Exemplar() -> void

View File

@ -2,6 +2,12 @@ OpenTelemetry.Metrics.AlwaysOffExemplarFilter
OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
OpenTelemetry.Metrics.AlwaysOnExemplarFilter
OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void
OpenTelemetry.Metrics.Exemplar
OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
OpenTelemetry.Metrics.Exemplar.Exemplar() -> void

View File

@ -2,6 +2,12 @@ OpenTelemetry.Metrics.AlwaysOffExemplarFilter
OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
OpenTelemetry.Metrics.AlwaysOnExemplarFilter
OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int
OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void
OpenTelemetry.Metrics.Exemplar
OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
OpenTelemetry.Metrics.Exemplar.Exemplar() -> void

View File

@ -2,6 +2,12 @@
## Unreleased
* Add support for configuring the
[Base2 Exponential Bucket Histogram Aggregation](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#base2-exponential-bucket-histogram-aggregation)
using the `AddView` API. This aggregation is supported by OTLP but not yet by
Prometheus.
([#4337](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4337))
* Implementation of `SuppressInstrumentationScope` changed to improve
performance.
([#4304](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4304))

View File

@ -19,7 +19,7 @@ namespace OpenTelemetry.Metrics;
/// <summary>
/// Stores configuration for a histogram metric stream with base-2 exponential bucket boundaries.
/// </summary>
internal sealed class Base2ExponentialBucketHistogramConfiguration : HistogramConfiguration
public sealed class Base2ExponentialBucketHistogramConfiguration : HistogramConfiguration
{
private int maxSize = Metric.DefaultExponentialHistogramMaxBuckets;
private int maxScale = Metric.DefaultExponentialHistogramMaxScale;

View File

@ -469,6 +469,103 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests
Assert.Empty(dataPoint.Exemplars);
}
[Theory]
[InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)]
[InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)]
[InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)]
[InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)]
[InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)]
public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues)
{
var metrics = new List<Metric>();
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(metrics, metricReaderOptions =>
{
metricReaderOptions.TemporalityPreference = aggregationTemporality;
})
.AddView(instrument =>
{
return new Base2ExponentialBucketHistogramConfiguration();
})
.Build();
var attributes = ToAttributes(keysValues).ToArray();
if (longValue.HasValue)
{
var histogram = meter.CreateHistogram<long>(name, unit, description);
histogram.Record(longValue.Value, attributes);
}
else
{
var histogram = meter.CreateHistogram<double>(name, unit, description);
histogram.Record(doubleValue.Value, attributes);
}
provider.ForceFlush();
var batch = new Batch<Metric>(metrics.ToArray(), metrics.Count);
var request = new OtlpCollector.ExportMetricsServiceRequest();
request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch);
var resourceMetric = request.ResourceMetrics.Single();
var scopeMetrics = resourceMetric.ScopeMetrics.Single();
var actual = scopeMetrics.Metrics.Single();
Assert.Equal(name, actual.Name);
Assert.Equal(description ?? string.Empty, actual.Description);
Assert.Equal(unit ?? string.Empty, actual.Unit);
Assert.Equal(OtlpMetrics.Metric.DataOneofCase.ExponentialHistogram, actual.DataCase);
Assert.Null(actual.Gauge);
Assert.Null(actual.Sum);
Assert.Null(actual.Histogram);
Assert.NotNull(actual.ExponentialHistogram);
Assert.Null(actual.Summary);
var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative
? OtlpMetrics.AggregationTemporality.Cumulative
: OtlpMetrics.AggregationTemporality.Delta;
Assert.Equal(otlpAggregationTemporality, actual.ExponentialHistogram.AggregationTemporality);
Assert.Single(actual.ExponentialHistogram.DataPoints);
var dataPoint = actual.ExponentialHistogram.DataPoints.First();
Assert.True(dataPoint.StartTimeUnixNano > 0);
Assert.True(dataPoint.TimeUnixNano > 0);
Assert.Equal(1UL, dataPoint.Count);
if (longValue.HasValue)
{
Assert.Equal((double)longValue, dataPoint.Sum);
}
else
{
Assert.Equal(doubleValue, dataPoint.Sum);
}
Assert.Equal(0UL, dataPoint.ZeroCount);
Assert.Equal(20, dataPoint.Scale);
Assert.True(dataPoint.Positive.Offset > 0);
Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]);
Assert.True(dataPoint.Negative.Offset <= 0);
if (attributes.Length > 0)
{
OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes);
}
else
{
Assert.Empty(dataPoint.Attributes);
}
Assert.Empty(dataPoint.Exemplars);
}
[Theory]
[InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)]
[InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)]

View File

@ -462,5 +462,30 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests
+ "$").Replace('\'', '"'),
Encoding.UTF8.GetString(buffer, 0, cursor));
}
[Fact]
public void ExponentialHistogramIsIgnoredForNow()
{
var buffer = new byte[85000];
var metrics = new List<Metric>();
using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddView(instrument => new Base2ExponentialBucketHistogramConfiguration())
.AddInMemoryExporter(metrics)
.Build();
var histogram = meter.CreateHistogram<double>("test_histogram");
histogram.Record(18);
histogram.Record(100);
provider.ForceFlush();
var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]);
Assert.Matches(
"^$",
Encoding.UTF8.GetString(buffer, 0, cursor));
}
}
}