From d452ab2f50d50d3926e735d976caddaa7f79da1c Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Thu, 7 Dec 2023 22:04:19 +1100 Subject: [PATCH] Add tests and ability to disable scope info --- examples/AspNetCore/Instrumentation.cs | 2 +- .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../PrometheusAspNetCoreOptions.cs | 9 ++ .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../Internal/PrometheusCollectionManager.cs | 5 +- .../Internal/PrometheusExporter.cs | 3 + .../Internal/PrometheusExporterOptions.cs | 5 + .../Internal/PrometheusSerializer.cs | 6 +- .../Internal/PrometheusSerializerExt.cs | 10 +- ...pListenerMeterProviderBuilderExtensions.cs | 6 +- .../PrometheusHttpListenerOptions.cs | 4 +- .../PrometheusExporterMiddlewareTests.cs | 67 +++++++++-- .../PrometheusHttpListenerTests.cs | 66 +++++++++-- .../PrometheusSerializerTests.cs | 105 ++++++++++++++---- 14 files changed, 237 insertions(+), 55 deletions(-) diff --git a/examples/AspNetCore/Instrumentation.cs b/examples/AspNetCore/Instrumentation.cs index e4583887e0..62d93dbcae 100644 --- a/examples/AspNetCore/Instrumentation.cs +++ b/examples/AspNetCore/Instrumentation.cs @@ -35,7 +35,7 @@ public Instrumentation() string? version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); this.ActivitySource = new ActivitySource(ActivitySourceName, version); this.meter = new Meter(MeterName, version); - this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", "The number of days where the temperature is below freezing"); + this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", description: "The number of days where the temperature is below freezing"); } public ActivitySource ActivitySource { get; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 0dd120b12d..f904582a47 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -2,6 +2,8 @@ Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScopeInfoEnabled.get -> bool +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScopeInfoEnabled.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.get -> string OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeResponseCacheDurationMilliseconds.get -> int diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index aead3765af..e1ca1636c3 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs @@ -42,5 +42,14 @@ public int ScrapeResponseCacheDurationMilliseconds set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value; } + /// + /// Gets or sets a value indicating whether to export scope info. Default value: true. + /// + public bool ScopeInfoEnabled + { + get => this.ExporterOptions.ScopeInfoEnabled; + set => this.ExporterOptions.ScopeInfoEnabled = value; + } + internal PrometheusExporterOptions ExporterOptions { get; } = new(); } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index 9bc2e72461..62cb390376 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ OpenTelemetry.Exporter.PrometheusHttpListenerOptions +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.get -> bool +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.set -> void OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.get -> System.Collections.Generic.IReadOnlyCollection OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.set -> void OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index a00b0fd767..84509e8376 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -185,7 +185,7 @@ private ExportResult OnCollect(Batch metrics) try { - if (this.exporter.OpenMetricsRequested) + if (this.exporter.ScopeInfoEnabled && this.exporter.OpenMetricsRequested) { this.scopes.Clear(); @@ -237,7 +237,8 @@ private ExportResult OnCollect(Batch metrics) cursor, metric, this.GetPrometheusMetric(metric), - this.exporter.OpenMetricsRequested); + this.exporter.OpenMetricsRequested, + this.exporter.ScopeInfoEnabled); break; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index b02a3a64b6..2e8de84283 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -38,6 +38,7 @@ public PrometheusExporter(PrometheusExporterOptions options) Guard.ThrowIfNull(options); this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds; + this.ScopeInfoEnabled = options.ScopeInfoEnabled; this.CollectionManager = new PrometheusCollectionManager(this); } @@ -65,6 +66,8 @@ internal Func, ExportResult> OnExport internal bool OpenMetricsRequested { get; set; } + internal bool ScopeInfoEnabled { get; set; } + /// public override ExportResult Export(in Batch metrics) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs index 2d9a679124..af1e2f6f09 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs @@ -41,4 +41,9 @@ public int ScrapeResponseCacheDurationMilliseconds this.scrapeResponseCacheDurationMilliseconds = value; } } + + /// + /// Gets or sets a value indicating whether to export scope info. Default value: true. + /// + public bool ScopeInfoEnabled { get; set; } = true; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index cc7ac32a58..e3cee944bf 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -379,16 +379,16 @@ public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool use } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool openMetricsRequested, bool writeEnclosingBraces = true) + public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool scopeInfoEnabled, bool writeEnclosingBraces = true) { - if (tags.Count > 0 || openMetricsRequested) + if (tags.Count > 0 || scopeInfoEnabled) { if (writeEnclosingBraces) { buffer[cursor++] = unchecked((byte)'{'); } - if (openMetricsRequested) + if (scopeInfoEnabled) { cursor = WriteLabel(buffer, cursor, "otel_scope_name", metric.MeterName); buffer[cursor++] = unchecked((byte)','); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 306b116756..76396fd287 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -35,7 +35,7 @@ public static bool CanWriteMetric(Metric metric) return true; } - public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false) + public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false, bool scopeInfoEnabled = true) { cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric); cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); @@ -49,7 +49,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe // Counter and Gauge cursor = WriteMetricName(buffer, cursor, prometheusMetric); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, scopeInfoEnabled); buffer[cursor++] = unchecked((byte)' '); @@ -100,7 +100,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); - cursor = WriteTags(buffer, cursor, metric, tags, openMetricsRequested, writeEnclosingBraces: false); + cursor = WriteTags(buffer, cursor, metric, tags, scopeInfoEnabled, writeEnclosingBraces: false); cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\""); @@ -126,7 +126,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe // Histogram sum cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, scopeInfoEnabled); buffer[cursor++] = unchecked((byte)' '); @@ -140,7 +140,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe // Histogram count cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); - cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, openMetricsRequested); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, scopeInfoEnabled); buffer[cursor++] = unchecked((byte)' '); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index 62c5f386b0..20b1eabcb9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs @@ -78,7 +78,11 @@ public static MeterProviderBuilder AddPrometheusHttpListener( private static MetricReader BuildPrometheusHttpListenerMetricReader( PrometheusHttpListenerOptions options) { - var exporter = new PrometheusExporter(new PrometheusExporterOptions { ScrapeResponseCacheDurationMilliseconds = 0 }); + var exporter = new PrometheusExporter(new PrometheusExporterOptions + { + ScrapeResponseCacheDurationMilliseconds = 0, + ScopeInfoEnabled = options.ScopeInfoEnabled, + }); var reader = new BaseExportingMetricReader(exporter) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index ebc265de5f..7f926f572f 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -31,9 +31,9 @@ public class PrometheusHttpListenerOptions public string ScrapeEndpointPath { get; set; } = "/metrics"; /// - /// Gets or sets a value indicating whether to export OpenMetrics compatible scrape responses. Default value: true. + /// Gets or sets a value indicating whether to export scope info. Default value: true. /// - public bool OpenMetricsEnabled { get; set; } = true; + public bool ScopeInfoEnabled { get; set; } = true; /// /// Gets or sets the URI (Uniform Resource Identifier) prefixes to use for the http listener. diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 072b3b4511..956b16079a 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -32,6 +32,8 @@ namespace OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests; public sealed class PrometheusExporterMiddlewareTests { + private const string MeterVersion = "1.0.1"; + private static readonly string MeterName = Utils.GetCurrentMethodName(); [Fact] @@ -247,6 +249,16 @@ public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse() acceptHeader: "text/plain"); } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse_NoScopeInfo() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "text/plain", + scopeInfoEnabled: false); + } + [Fact] public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader() { @@ -256,6 +268,15 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader( acceptHeader: "application/openmetrics-text; version=1.0.0"); } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_NoScopeInfo() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + scopeInfoEnabled: false); + } + private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, @@ -264,7 +285,8 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( bool registerMeterProvider = true, Action configureOptions = null, bool skipMetrics = false, - string acceptHeader = "application/openmetrics-text") + string acceptHeader = "application/openmetrics-text", + bool scopeInfoEnabled = true) { var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); @@ -279,6 +301,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( .AddMeter(MeterName) .AddPrometheusExporter(o => { + o.ScopeInfoEnabled = scopeInfoEnabled; configureOptions?.Invoke(o); })); } @@ -294,7 +317,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( new KeyValuePair("key2", "value2"), }; - using var meter = new Meter(MeterName); + using var meter = new Meter(MeterName, MeterVersion); var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -331,14 +354,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( } string content = await response.Content.ReadAsStringAsync(); - - string expected = requestOpenMetrics - ? "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n" - + "# EOF\n" - : "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" - + "# EOF\n"; + string expected = GetExpectedContent(requestOpenMetrics, scopeInfoEnabled); var matches = Regex.Matches(content, ("^" + expected + "$").Replace('\'', '"')); @@ -357,5 +373,36 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( await host.StopAsync(); } + + private static string GetExpectedContent(bool requestedOpenMetrics, bool scopeInfoEnabled) + { + if (requestedOpenMetrics && scopeInfoEnabled) + { + return "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + + "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + "# EOF\n"; + } + + if (!requestedOpenMetrics && scopeInfoEnabled) + { + return "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + "# EOF\n"; + } + + if (requestedOpenMetrics && !scopeInfoEnabled) + { + return "# TYPE counter_double_total counter\n" + + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n" + + "# EOF\n"; + } + + return "# TYPE counter_double_total counter\n" + + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" + + "# EOF\n"; + } } #endif diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index e9e270dd5b..c1e453b125 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -27,7 +27,9 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests; public class PrometheusHttpListenerTests { - private readonly string meterName = Utils.GetCurrentMethodName(); + private const string MeterVersion = "1.0.1"; + + private static readonly string MeterName = Utils.GetCurrentMethodName(); [Theory] [InlineData("http://+:9464")] @@ -96,13 +98,25 @@ public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty); } + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics_NoScopeInfo() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, scopeInfoEnabled: false); + } + [Fact] public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader() { await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text") + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoScopeInfo() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(scopeInfoEnabled: false); + } + + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", bool scopeInfoEnabled = true) { var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); @@ -112,7 +126,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri string address = null; MeterProvider provider = null; - using var meter = new Meter(this.meterName); + using var meter = new Meter(MeterName, MeterVersion); while (retryAttempts-- != 0) { @@ -123,7 +137,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri { provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) + .AddPrometheusHttpListener(options => + { + options.ScopeInfoEnabled = scopeInfoEnabled; + options.UriPrefixes = new string[] { address }; + }) .Build(); break; @@ -176,14 +194,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri } var content = await response.Content.ReadAsStringAsync(); - - var expected = requestOpenMetrics - ? "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\\.\\d{3}\n" - + "# EOF\n" - : "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n" - + "# EOF\n"; + var expected = GetExpectedContent(requestOpenMetrics, scopeInfoEnabled); Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content); } @@ -194,4 +205,35 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri provider.Dispose(); } + + private static string GetExpectedContent(bool requestedOpenMetrics, bool scopeInfoEnabled) + { + if (requestedOpenMetrics && scopeInfoEnabled) + { + return "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + + "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + "# EOF\n"; + } + + if (!requestedOpenMetrics && scopeInfoEnabled) + { + return "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + "# EOF\n"; + } + + if (requestedOpenMetrics && !scopeInfoEnabled) + { + return "# TYPE counter_double_total counter\n" + + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n" + + "# EOF\n"; + } + + return "# TYPE counter_double_total counter\n" + + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" + + "# EOF\n"; + } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 2fd3bfceb2..9c8f8ecc2d 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -512,22 +512,6 @@ public void ExponentialHistogramIsIgnoredForNow() Assert.False(PrometheusSerializer.CanWriteMetric(metrics[0])); } - [Fact] - public void ScopeInfo() - { - var buffer = new byte[85000]; - - var cursor = PrometheusSerializer.WriteScopeInfo(buffer, 0, "test_meter"); - - Assert.Matches( - ("^" - + "# TYPE otel_scope_info info\n" - + "# HELP otel_scope_info Scope metadata\n" - + "otel_scope_info{otel_scope_name='test_meter'} 1\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - [Fact] public void SumWithOpenMetricsFormat() { @@ -556,7 +540,7 @@ public void SumWithOpenMetricsFormat() } [Fact] - public void HistogramOneDimensionWithScopeInfo() + public void HistogramOneDimensionWithOpenMetricsFormat() { var buffer = new byte[85000]; var metrics = new List(); @@ -599,8 +583,91 @@ public void HistogramOneDimensionWithScopeInfo() Encoding.UTF8.GetString(buffer, 0, cursor)); } - private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false) + [Fact] + public void ScopeInfo() + { + var buffer = new byte[85000]; + + var cursor = PrometheusSerializer.WriteScopeInfo(buffer, 0, "test_meter"); + + Assert.Matches( + ("^" + + "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + "otel_scope_info{otel_scope_name='test_meter'} 1\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumWithOpenScopeInfo() + { + var buffer = new byte[85000]; + var metrics = new List(); + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + var counter = meter.CreateUpDownCounter("test_updown_counter"); + counter.Add(10); + counter.Add(-11); + provider.ForceFlush(); + var cursor = WriteMetric(buffer, 0, metrics[0], true, true); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimensionWithScopeInfo() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName(), "1.0.0"); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true, true); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='0'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='25'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='50'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='75'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='100'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='250'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='750'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='1000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='2500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='7500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='\\+Inf'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 118 \\d+\\.\\d{{3}}\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 2 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false, bool scopeInfoEnabled = false) { - return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric), useOpenMetrics); + return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric), useOpenMetrics, scopeInfoEnabled); } }