diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs index 8835f260fba..bc4e0602cda 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System.Diagnostics.Metrics; using Examples.AspNetCore; using OpenTelemetry.Exporter; using OpenTelemetry.Instrumentation.AspNetCore; @@ -33,6 +34,9 @@ // Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json. var logExporter = appBuilder.Configuration.GetValue("UseLogExporter").ToLowerInvariant(); +// Note: Switch between Explicit/Exponential by setting HistogramAggregation in appsettings.json +var histogramAggregation = appBuilder.Configuration.GetValue("HistogramAggregation").ToLowerInvariant(); + // Build a resource configuration action to set service information. Action configureResource = r => r.AddService( serviceName: appBuilder.Configuration.GetValue("ServiceName"), @@ -111,6 +115,22 @@ .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": diff --git a/examples/AspNetCore/appsettings.json b/examples/AspNetCore/appsettings.json index 19f0513276b..1f2d3368dc9 100644 --- a/examples/AspNetCore/appsettings.json +++ b/examples/AspNetCore/appsettings.json @@ -14,6 +14,7 @@ "UseTracingExporter": "console", "UseMetricsExporter": "console", "UseLogExporter": "console", + "HistogramAggregation": "explicit", "Jaeger": { "AgentHost": "localhost", "AgentPort": 6831, diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index f66a34ba756..fc9d13de06b 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -92,7 +92,7 @@ public override ExportResult Export(in Batch batch) 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 @@ public override ExportResult Export(in Batch batch) 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(); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 0afaba4da4c..f043de9bc64 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -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. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 9208eaa3e99..f63d95f3647 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -307,6 +307,57 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) 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; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 445e0410c81..3094129cc7e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -36,6 +36,13 @@ UpDownCounter becomes gauge 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); diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index ac2084882d9..d0ed4d4dec8 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt index c79ebddeb5e..a06e307d7c4 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index c79ebddeb5e..a06e307d7c4 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index c79ebddeb5e..a06e307d7c4 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index a7b9a3e133a..33ac5c0f5f0 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -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)) diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs index 4697f6f981e..1394d3a8715 100644 --- a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs @@ -19,7 +19,7 @@ namespace OpenTelemetry.Metrics; /// /// Stores configuration for a histogram metric stream with base-2 exponential bucket boundaries. /// -internal sealed class Base2ExponentialBucketHistogramConfiguration : HistogramConfiguration +public sealed class Base2ExponentialBucketHistogramConfiguration : HistogramConfiguration { private int maxSize = Metric.DefaultExponentialHistogramMaxBuckets; private int maxScale = Metric.DefaultExponentialHistogramMaxScale; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 48e41e3fb86..9b6e8f5c87f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -469,6 +469,103 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin 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(); + + 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(name, unit, description); + histogram.Record(longValue.Value, attributes); + } + else + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(doubleValue.Value, attributes); + } + + provider.ForceFlush(); + + var batch = new Batch(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)] diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 9970be5c83d..4819867c527 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -462,5 +462,30 @@ public void HistogramNaN() + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } + + [Fact] + public void ExponentialHistogramIsIgnoredForNow() + { + var buffer = new byte[85000]; + var metrics = new List(); + + 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("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)); + } } }