From 1c024054f2d044b41fa7bd35bb3745c874458c99 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Fri, 1 Dec 2023 11:55:09 +1100 Subject: [PATCH 01/18] Export openmetrics format for prometheus exporters --- .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../CHANGELOG.md | 2 + .../PrometheusAspNetCoreOptions.cs | 9 ++++ .../PrometheusExporterMiddleware.cs | 36 ++++++++++++- .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../CHANGELOG.md | 2 + .../Internal/PrometheusCollectionManager.cs | 18 +++++-- .../Internal/PrometheusExporter.cs | 5 ++ .../Internal/PrometheusExporterOptions.cs | 5 ++ .../Internal/PrometheusSerializer.cs | 26 ++++++++++ .../Internal/PrometheusSerializerExt.cs | 12 ++--- .../PrometheusHttpListener.cs | 34 +++++++++++- ...pListenerMeterProviderBuilderExtensions.cs | 6 ++- .../PrometheusHttpListenerOptions.cs | 5 ++ .../PrometheusExporterMiddlewareTests.cs | 52 ++++++++++++++----- .../PrometheusCollectionManagerTests.cs | 14 ++--- .../PrometheusHttpListenerTests.cs | 46 +++++++++++++--- .../PrometheusSerializerTests.cs | 31 ++++++++++- 18 files changed, 264 insertions(+), 43 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 0dd120b12da..22d97599cdd 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.OpenMetricsEnabled.get -> bool +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.OpenMetricsEnabled.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.get -> string OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.set -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index adc7f6d701d..a0b9e508df0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Export OpenMetrics format from Prometheus exporters +* ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index aead3765afc..ab47be04db4 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 OpenMetrics compatible scrape responses. Default value: true. + /// + public bool OpenMetricsEnabled + { + get => this.ExporterOptions.OpenMetricsEnabled; + set => this.ExporterOptions.OpenMetricsEnabled = value; + } + internal PrometheusExporterOptions ExporterOptions { get; } = new(); } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index bfbb178783a..d17c0772ef6 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -16,6 +16,8 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; @@ -27,6 +29,8 @@ namespace OpenTelemetry.Exporter; /// internal sealed class PrometheusExporterMiddleware { + private const string OpenMetricsMediaType = "application/openmetrics-text"; + private readonly PrometheusExporter exporter; /// @@ -64,7 +68,9 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + var openMetricsRequested = this.exporter.OpenMetricsEnabled && this.AcceptsOpenMetrics(httpContext.Request); + var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); + try { if (collectionResponse.View.Count > 0) @@ -75,7 +81,9 @@ public async Task InvokeAsync(HttpContext httpContext) #else response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); #endif - response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; + response.ContentType = openMetricsRequested + ? "application/openmetrics-text; version=1.0.0; charset=utf-8" + : "text/plain; charset=utf-8; version=0.0.4"; await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); } @@ -102,4 +110,28 @@ public async Task InvokeAsync(HttpContext httpContext) this.exporter.OnExport = null; } + + private bool AcceptsOpenMetrics(HttpRequest request) + { + var requestAccept = request.Headers[HeaderNames.Accept]; + + if (StringValues.IsNullOrEmpty(requestAccept)) + { + return false; + } + + var acceptTypes = requestAccept.ToString().Split(','); + + foreach (var acceptType in acceptTypes) + { + var acceptSubType = acceptType.Split(';').FirstOrDefault()?.Trim(); + + if (acceptSubType == OpenMetricsMediaType) + { + return true; + } + } + + return false; + } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index 9bc2e72461d..8dbcc600a05 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.OpenMetricsEnabled.get -> bool +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.OpenMetricsEnabled.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/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index 875c9b03841..2690bd80e6b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Export OpenMetrics format from Prometheus exporters + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index 4d4ef30eda1..a10ac55f42d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -45,9 +45,9 @@ public PrometheusCollectionManager(PrometheusExporter exporter) } #if NET6_0_OR_GREATER - public ValueTask EnterCollect() + public ValueTask EnterCollect(bool openMetricsRequested) #else - public Task EnterCollect() + public Task EnterCollect(bool openMetricsRequested) #endif { this.EnterGlobalLock(); @@ -93,7 +93,7 @@ public Task EnterCollect() this.ExitGlobalLock(); CollectionResponse response; - var result = this.ExecuteCollect(); + var result = this.ExecuteCollect(openMetricsRequested); if (result) { this.previousDataViewGeneratedAtUtc = DateTime.UtcNow; @@ -168,11 +168,13 @@ private void WaitForReadersToComplete() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ExecuteCollect() + private bool ExecuteCollect(bool openMetricsRequested) { this.exporter.OnExport = this.onCollectRef; + this.exporter.OpenMetricsRequested = openMetricsRequested; var result = this.exporter.Collect(Timeout.Infinite); this.exporter.OnExport = null; + this.exporter.OpenMetricsRequested = null; return result; } @@ -193,7 +195,13 @@ private ExportResult OnCollect(Batch metrics) { try { - cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric)); + cursor = PrometheusSerializer.WriteMetric( + this.buffer, + cursor, + metric, + this.GetPrometheusMetric(metric), + this.exporter.OpenMetricsRequested ?? false); + break; } catch (IndexOutOfRangeException) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index ddc3df494c9..a6595d16a15 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.OpenMetricsEnabled = options.OpenMetricsEnabled; this.CollectionManager = new PrometheusCollectionManager(this); } @@ -63,6 +64,10 @@ internal Func, ExportResult> OnExport internal int ScrapeResponseCacheDurationMilliseconds { get; } + internal bool OpenMetricsEnabled { get; } + + internal bool? OpenMetricsRequested { 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 2d9a679124f..2e2da9c7d10 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 OpenMetrics compatible scrape responses. Default value: true. + /// + public bool OpenMetricsEnabled { get; set; } = true; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 3bcd4998f12..631de4eab3d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -326,6 +326,32 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric return cursor; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics) + { + if (useOpenMetrics) + { + cursor = WriteLong(buffer, cursor, value / 1000); + buffer[cursor++] = unchecked((byte)'.'); + + long millis = value % 1000; + + if (millis < 100) + { + buffer[cursor++] = unchecked((byte)'0'); + } + + if (millis < 10) + { + buffer[cursor++] = unchecked((byte)'0'); + } + + return WriteLong(buffer, cursor, millis); + } + + return WriteLong(buffer, cursor, value); + } + private static string MapPrometheusType(PrometheusType type) { return type switch diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 0eb068b185d..d606a5ced5b 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) + public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false) { cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric); cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); @@ -94,7 +94,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); buffer[cursor++] = ASCII_LINEFEED; } @@ -136,7 +136,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe cursor = WriteLong(buffer, cursor, totalCount); buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); buffer[cursor++] = ASCII_LINEFEED; } @@ -163,7 +163,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); buffer[cursor++] = ASCII_LINEFEED; @@ -189,14 +189,12 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); buffer[cursor++] = ASCII_LINEFEED; } } - buffer[cursor++] = ASCII_LINEFEED; - return cursor; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index 112bbf26206..20c42b0b407 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -22,6 +22,8 @@ namespace OpenTelemetry.Exporter; internal sealed class PrometheusHttpListener : IDisposable { + private const string OpenMetricsMediaType = "application/openmetrics-text"; + private readonly PrometheusExporter exporter; private readonly HttpListener httpListener = new(); private readonly object syncObject = new(); @@ -148,7 +150,9 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { try { - var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + var openMetricsRequested = this.exporter.OpenMetricsEnabled && this.AcceptsOpenMetrics(context.Request); + var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); + try { context.Response.Headers.Add("Server", string.Empty); @@ -156,7 +160,9 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { context.Response.StatusCode = 200; context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); - context.Response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; + context.Response.ContentType = openMetricsRequested + ? "application/openmetrics-text; version=1.0.0; charset=utf-8" + : "text/plain; charset=utf-8; version=0.0.4"; await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); } @@ -187,4 +193,28 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { } } + + private bool AcceptsOpenMetrics(HttpListenerRequest request) + { + var requestAccept = request.Headers["Accept"]; + + if (string.IsNullOrEmpty(requestAccept)) + { + return false; + } + + var acceptTypes = requestAccept.Split(','); + + foreach (var acceptType in acceptTypes) + { + var acceptSubType = acceptType.Split(';').FirstOrDefault()?.Trim(); + + if (acceptSubType == OpenMetricsMediaType) + { + return true; + } + } + + return false; + } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index 62c5f386b06..b7f6ea804b6 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, + OpenMetricsEnabled = options.OpenMetricsEnabled, + }); var reader = new BaseExportingMetricReader(exporter) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index 51954fe15df..206d70d73e9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -49,4 +49,9 @@ public IReadOnlyCollection UriPrefixes this.uriPrefixes = value; } } + + /// + /// Gets or sets a value indicating whether to export OpenMetrics compatible scrape responses. Default value: true. + /// + public bool OpenMetricsEnabled { get; set; } = true; } diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index ea38fb59cf9..013f684226f 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -17,6 +17,7 @@ #if !NETFRAMEWORK using System.Diagnostics.Metrics; using System.Net; +using System.Net.Http.Headers; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -238,6 +239,16 @@ await RunPrometheusExporterMiddlewareIntegrationTest( registerMeterProvider: false); } + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_DisableOpenMetrics() + { + await RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + configureOptions: o => o.OpenMetricsEnabled = false, + useOpenMetrics: false); + } + private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, @@ -245,7 +256,8 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( Action validateResponse = null, bool registerMeterProvider = true, Action configureOptions = null, - bool skipMetrics = false) + bool skipMetrics = false, + bool useOpenMetrics = true) { using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => webBuilder @@ -284,7 +296,14 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( counter.Add(0.99D, tags); } - using var response = await host.GetTestClient().GetAsync(path); + using var client = host.GetTestClient(); + + if (useOpenMetrics) + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); + } + + using var response = await client.GetAsync(path); var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -292,22 +311,31 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Content.Headers.Contains("Last-Modified")); - Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + + if (useOpenMetrics) + { + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); + } + else + { + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + } string content = await response.Content.ReadAsStringAsync(); - var matches = Regex.Matches( - content, - ("^" - + "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" - + "\n" - + "# EOF\n" - + "$").Replace('\'', '"')); + string expected = useOpenMetrics + ? "# 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 matches = Regex.Matches(content, ("^" + expected + "$").Replace('\'', '"')); Assert.Single(matches); - var timestamp = long.Parse(matches[0].Groups[1].Value); + var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty)); Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp); } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs index 70b886b5202..26111263798 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs @@ -24,11 +24,13 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests; public sealed class PrometheusCollectionManagerTests { [Theory] - [InlineData(0)] // disable cache, default value for HttpListener + [InlineData(0, true)] // disable cache, default value for HttpListener + [InlineData(0, false)] // disable cache, default value for HttpListener #if PROMETHEUS_ASPNETCORE - [InlineData(300)] // default value for AspNetCore, no possibility to set on HttpListener + [InlineData(300, true)] // default value for AspNetCore, no possibility to set on HttpListener + [InlineData(300, false)] // default value for AspNetCore, no possibility to set on HttpListener #endif - public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMilliseconds) + public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMilliseconds, bool openMetricsRequested) { bool cacheEnabled = scrapeResponseCacheDurationMilliseconds != 0; using var meter = new Meter(Utils.GetCurrentMethodName()); @@ -65,7 +67,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon { collectTasks[i] = Task.Run(async () => { - var response = await exporter.CollectionManager.EnterCollect(); + var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested); try { return new Response @@ -98,7 +100,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon counter.Add(100); // This should use the cache and ignore the second counter update. - var task = exporter.CollectionManager.EnterCollect(); + var task = exporter.CollectionManager.EnterCollect(openMetricsRequested); Assert.True(task.IsCompleted); var response = await task; try @@ -129,7 +131,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon { collectTasks[i] = Task.Run(async () => { - var response = await exporter.CollectionManager.EnterCollect(); + var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested); try { return new Response diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index d964432009e..f7f8b55624c 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -16,6 +16,8 @@ using System.Diagnostics.Metrics; using System.Net; +using System.Net.Http.Headers; + #if NETFRAMEWORK using System.Net.Http; #endif @@ -90,7 +92,13 @@ public async Task PrometheusExporterHttpServerIntegration_NoMetrics() await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false) + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(useOpenMetrics: false); + } + + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool useOpenMetrics = true) { Random random = new Random(); int retryAttempts = 5; @@ -107,7 +115,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) + .AddPrometheusHttpListener(options => + { + options.OpenMetricsEnabled = useOpenMetrics; + options.UriPrefixes = new string[] { address }; + }) .Build(); } @@ -125,17 +137,39 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri } using HttpClient client = new HttpClient(); + + if (useOpenMetrics) + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); + } + using var response = await client.GetAsync($"{address}metrics"); if (!skipMetrics) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Content.Headers.Contains("Last-Modified")); - Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); - Assert.Matches( - "^# TYPE counter_double_total counter\ncounter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n\n# EOF\n$".Replace('\'', '"'), - await response.Content.ReadAsStringAsync()); + if (useOpenMetrics) + { + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); + } + else + { + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + } + + var content = await response.Content.ReadAsStringAsync(); + + var expected = useOpenMetrics + ? "# 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"; + + Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content); } else { diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 53617606cf0..3522a5890eb 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -512,8 +512,35 @@ public void ExponentialHistogramIsIgnoredForNow() Assert.False(PrometheusSerializer.CanWriteMetric(metrics[0])); } - private static int WriteMetric(byte[] buffer, int cursor, Metric metric) + [Fact] + public void SumWithOpenMetricsFormat() + { + 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); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + "test_updown_counter -1 \\d+\\.\\d{3}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false) { - return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric)); + return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric), useOpenMetrics); } } From 5a98791ebdbefe8b70b3021dba5d0bb4faf96b45 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Fri, 1 Dec 2023 11:57:52 +1100 Subject: [PATCH 02/18] Update changelog --- src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md | 2 +- src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index a0b9e508df0..871968cca50 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -* Export OpenMetrics format from Prometheus exporters +* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) * ## 1.7.0-alpha.1 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index 2690bd80e6b..eeb21d251a0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -* Export OpenMetrics format from Prometheus exporters +* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) ## 1.7.0-alpha.1 From 36eb7125e244f857c79e62b6245761005ac5a2ea Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Fri, 1 Dec 2023 12:07:58 +1100 Subject: [PATCH 03/18] Add serializer test --- .../PrometheusSerializerTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 3522a5890eb..ee837e516ca 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -539,6 +539,50 @@ public void SumWithOpenMetricsFormat() 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()); + 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); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{x='1',le='0'} 0 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='5'} 0 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='10'} 0 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='25'} 1 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='50'} 1 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='75'} 1 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='100'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='250'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='500'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='750'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='1000'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='2500'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='5000'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='7500'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='10000'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_bucket{x='1',le='\\+Inf'} 2 \\d+\\.\\d{3}\n" + + "test_histogram_sum{x='1'} 118 \\d+\\.\\d{3}\n" + + "test_histogram_count{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) { return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric), useOpenMetrics); From 945834abdd0686c5344a33fc697752533e34080e Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Sun, 3 Dec 2023 08:35:52 +1100 Subject: [PATCH 04/18] Decide content type based on accept header --- .../.publicApi/PublicAPI.Unshipped.txt | 2 -- .../PrometheusAspNetCoreOptions.cs | 9 --------- .../PrometheusExporterMiddleware.cs | 2 +- .../.publicApi/PublicAPI.Unshipped.txt | 2 -- .../Internal/PrometheusExporter.cs | 3 --- .../Internal/PrometheusExporterOptions.cs | 5 ----- .../PrometheusHttpListener.cs | 2 +- ...etheusHttpListenerMeterProviderBuilderExtensions.cs | 6 +----- .../PrometheusHttpListenerOptions.cs | 5 ----- .../PrometheusExporterMiddlewareTests.cs | 10 ---------- .../PrometheusHttpListenerTests.cs | 6 +----- 11 files changed, 4 insertions(+), 48 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 22d97599cdd..0dd120b12da 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -1,8 +1,6 @@ Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions -OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.OpenMetricsEnabled.get -> bool -OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.OpenMetricsEnabled.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.get -> string OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.set -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index ab47be04db4..aead3765afc 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs @@ -42,14 +42,5 @@ public int ScrapeResponseCacheDurationMilliseconds set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value; } - /// - /// Gets or sets a value indicating whether to export OpenMetrics compatible scrape responses. Default value: true. - /// - public bool OpenMetricsEnabled - { - get => this.ExporterOptions.OpenMetricsEnabled; - set => this.ExporterOptions.OpenMetricsEnabled = value; - } - internal PrometheusExporterOptions ExporterOptions { get; } = new(); } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index d17c0772ef6..1c32a106a7b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -68,7 +68,7 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var openMetricsRequested = this.exporter.OpenMetricsEnabled && this.AcceptsOpenMetrics(httpContext.Request); + var openMetricsRequested = this.AcceptsOpenMetrics(httpContext.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index 8dbcc600a05..9bc2e72461d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ OpenTelemetry.Exporter.PrometheusHttpListenerOptions -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.OpenMetricsEnabled.get -> bool -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.OpenMetricsEnabled.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/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index a6595d16a15..02204b7de6b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -38,7 +38,6 @@ public PrometheusExporter(PrometheusExporterOptions options) Guard.ThrowIfNull(options); this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds; - this.OpenMetricsEnabled = options.OpenMetricsEnabled; this.CollectionManager = new PrometheusCollectionManager(this); } @@ -64,8 +63,6 @@ internal Func, ExportResult> OnExport internal int ScrapeResponseCacheDurationMilliseconds { get; } - internal bool OpenMetricsEnabled { get; } - internal bool? OpenMetricsRequested { get; set; } /// diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs index 2e2da9c7d10..2d9a679124f 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs @@ -41,9 +41,4 @@ public int ScrapeResponseCacheDurationMilliseconds this.scrapeResponseCacheDurationMilliseconds = value; } } - - /// - /// Gets or sets a value indicating whether to export OpenMetrics compatible scrape responses. Default value: true. - /// - public bool OpenMetricsEnabled { get; set; } = true; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index 20c42b0b407..e76ab483719 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -150,7 +150,7 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { try { - var openMetricsRequested = this.exporter.OpenMetricsEnabled && this.AcceptsOpenMetrics(context.Request); + var openMetricsRequested = this.AcceptsOpenMetrics(context.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index b7f6ea804b6..62c5f386b06 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs @@ -78,11 +78,7 @@ public static MeterProviderBuilder AddPrometheusHttpListener( private static MetricReader BuildPrometheusHttpListenerMetricReader( PrometheusHttpListenerOptions options) { - var exporter = new PrometheusExporter(new PrometheusExporterOptions - { - ScrapeResponseCacheDurationMilliseconds = 0, - OpenMetricsEnabled = options.OpenMetricsEnabled, - }); + var exporter = new PrometheusExporter(new PrometheusExporterOptions { ScrapeResponseCacheDurationMilliseconds = 0 }); var reader = new BaseExportingMetricReader(exporter) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index 206d70d73e9..51954fe15df 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -49,9 +49,4 @@ public IReadOnlyCollection UriPrefixes this.uriPrefixes = value; } } - - /// - /// Gets or sets a value indicating whether to export OpenMetrics compatible scrape responses. Default value: true. - /// - public bool OpenMetricsEnabled { get; set; } = true; } diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 013f684226f..62b9785ae7b 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -239,16 +239,6 @@ await RunPrometheusExporterMiddlewareIntegrationTest( registerMeterProvider: false); } - [Fact] - public async Task PrometheusExporterMiddlewareIntegration_DisableOpenMetrics() - { - await RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - configureOptions: o => o.OpenMetricsEnabled = false, - useOpenMetrics: false); - } - private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index f7f8b55624c..59e9111ed1c 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -115,11 +115,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => - { - options.OpenMetricsEnabled = useOpenMetrics; - options.UriPrefixes = new string[] { address }; - }) + .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) .Build(); } From a8374af0b77ac00d6513662b54c3694f82e7d029 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Sun, 3 Dec 2023 08:38:37 +1100 Subject: [PATCH 05/18] Tidy test variable naming --- .../PrometheusExporterMiddlewareTests.cs | 8 ++++---- .../PrometheusHttpListenerTests.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 62b9785ae7b..f793c1dcd8d 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -247,7 +247,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( bool registerMeterProvider = true, Action configureOptions = null, bool skipMetrics = false, - bool useOpenMetrics = true) + bool requestOpenMetrics = true) { using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => webBuilder @@ -288,7 +288,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( using var client = host.GetTestClient(); - if (useOpenMetrics) + if (requestOpenMetrics) { client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); } @@ -302,7 +302,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Content.Headers.Contains("Last-Modified")); - if (useOpenMetrics) + if (requestOpenMetrics) { Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); } @@ -313,7 +313,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string content = await response.Content.ReadAsStringAsync(); - string expected = useOpenMetrics + string expected = requestOpenMetrics ? "# TYPE counter_double_total counter\n" + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n" + "# EOF\n" diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 59e9111ed1c..253cae68869 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -95,10 +95,10 @@ public async Task PrometheusExporterHttpServerIntegration_NoMetrics() [Fact] public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() { - await this.RunPrometheusExporterHttpServerIntegrationTest(useOpenMetrics: false); + await this.RunPrometheusExporterHttpServerIntegrationTest(requestOpenMetrics: false); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool useOpenMetrics = true) + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool requestOpenMetrics = true) { Random random = new Random(); int retryAttempts = 5; @@ -134,7 +134,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri using HttpClient client = new HttpClient(); - if (useOpenMetrics) + if (requestOpenMetrics) { client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); } @@ -146,7 +146,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Content.Headers.Contains("Last-Modified")); - if (useOpenMetrics) + if (requestOpenMetrics) { Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); } @@ -157,7 +157,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri var content = await response.Content.ReadAsStringAsync(); - var expected = useOpenMetrics + var expected = requestOpenMetrics ? "# TYPE counter_double_total counter\n" + "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\\.\\d{3}\n" + "# EOF\n" From 8b9e72eb8892da4a61eb693398576e382902c93c Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Tue, 5 Dec 2023 22:44:56 +1100 Subject: [PATCH 06/18] Use fewer allocations for media type parsing --- .../PrometheusExporterMiddleware.cs | 21 ++++++---- .../ReadOnlySpanExtensions.cs | 40 +++++++++++++++++++ .../PrometheusHttpListener.cs | 26 +++++++----- .../PrometheusExporterMiddlewareTests.cs | 30 +++++++++++++- .../PrometheusHttpListenerTests.cs | 18 +++++++-- 5 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 1c32a106a7b..6c62d6562d1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using OpenTelemetry.Exporter.Prometheus; +using OpenTelemetry.Exporter.Prometheus.AspNetCore; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; @@ -113,22 +114,26 @@ public async Task InvokeAsync(HttpContext httpContext) private bool AcceptsOpenMetrics(HttpRequest request) { - var requestAccept = request.Headers[HeaderNames.Accept]; + var acceptHeader = request.Headers[HeaderNames.Accept]; - if (StringValues.IsNullOrEmpty(requestAccept)) + if (StringValues.IsNullOrEmpty(acceptHeader)) { return false; } - var acceptTypes = requestAccept.ToString().Split(','); - - foreach (var acceptType in acceptTypes) + foreach (var accept in acceptHeader) { - var acceptSubType = acceptType.Split(';').FirstOrDefault()?.Trim(); + var value = accept.AsSpan(); - if (acceptSubType == OpenMetricsMediaType) + while (value.Length > 0) { - return true; + var headerValue = value.SplitNext(','); + var mediaType = headerValue.SplitNext(';'); + + if (mediaType.Equals(OpenMetricsMediaType, StringComparison.Ordinal)) + { + return true; + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs new file mode 100644 index 00000000000..9c8635ed420 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs @@ -0,0 +1,40 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Exporter.Prometheus.AspNetCore; + +internal static class ReadOnlySpanExtensions +{ + internal static ReadOnlySpan SplitNext(this ref ReadOnlySpan span, char character) + { + var index = span.IndexOf(character); + + if (index == -1) + { + var part = span; + span = span.Slice(span.Length); + + return part; + } + else + { + var part = span.Slice(0, index); + span = span.Slice(index + 1); + + return part; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index e76ab483719..f8d5a4c393a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -196,20 +196,14 @@ private async Task ProcessRequestAsync(HttpListenerContext context) private bool AcceptsOpenMetrics(HttpListenerRequest request) { - var requestAccept = request.Headers["Accept"]; - - if (string.IsNullOrEmpty(requestAccept)) + if (request.AcceptTypes == null) { return false; } - var acceptTypes = requestAccept.Split(','); - - foreach (var acceptType in acceptTypes) + foreach (var acceptType in request.AcceptTypes) { - var acceptSubType = acceptType.Split(';').FirstOrDefault()?.Trim(); - - if (acceptSubType == OpenMetricsMediaType) + if (this.GetMediaType(acceptType) == OpenMetricsMediaType) { return true; } @@ -217,4 +211,18 @@ private bool AcceptsOpenMetrics(HttpListenerRequest request) return false; } + + private string GetMediaType(string acceptHeader) + { + if (string.IsNullOrEmpty(acceptHeader)) + { + return string.Empty; + } + + var separatorIndex = acceptHeader.IndexOf(';'); + + return separatorIndex == -1 + ? acceptHeader + : acceptHeader.Substring(0, separatorIndex); + } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index f793c1dcd8d..6cda261f8fa 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -239,6 +239,24 @@ await RunPrometheusExporterMiddlewareIntegrationTest( registerMeterProvider: false); } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + requestOpenMetrics: false); + } + + [Fact] + public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + openMetricsVersion: "1.0.0"); + } + private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, @@ -247,7 +265,8 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( bool registerMeterProvider = true, Action configureOptions = null, bool skipMetrics = false, - bool requestOpenMetrics = true) + bool requestOpenMetrics = true, + string openMetricsVersion = null) { using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => webBuilder @@ -290,7 +309,14 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( if (requestOpenMetrics) { - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); + var mediaType = "application/openmetrics-text"; + + if (!string.IsNullOrEmpty(openMetricsVersion)) + { + mediaType += $";version={openMetricsVersion}"; + } + + client.DefaultRequestHeaders.Add("Accept", mediaType); } using var response = await client.GetAsync(path); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 253cae68869..dde4c853254 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -16,7 +16,6 @@ using System.Diagnostics.Metrics; using System.Net; -using System.Net.Http.Headers; #if NETFRAMEWORK using System.Net.Http; @@ -98,7 +97,13 @@ public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() await this.RunPrometheusExporterHttpServerIntegrationTest(requestOpenMetrics: false); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool requestOpenMetrics = true) + [Fact] + public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(openMetricsVersion: "1.0.0"); + } + + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool requestOpenMetrics = true, string openMetricsVersion = null) { Random random = new Random(); int retryAttempts = 5; @@ -136,7 +141,14 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri if (requestOpenMetrics) { - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openmetrics-text")); + var mediaType = "application/openmetrics-text"; + + if (!string.IsNullOrEmpty(openMetricsVersion)) + { + mediaType += $";version={openMetricsVersion}"; + } + + client.DefaultRequestHeaders.Add("Accept", mediaType); } using var response = await client.GetAsync($"{address}metrics"); From 6b4b86fcba29519ab8c73921d503e56a25cd6da6 Mon Sep 17 00:00:00 2001 From: Robert Coltheart <13191652+robertcoltheart@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:41:21 +1100 Subject: [PATCH 07/18] Update src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> --- .../PrometheusExporterMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 6c62d6562d1..5eb955677cf 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -114,7 +114,7 @@ public async Task InvokeAsync(HttpContext httpContext) private bool AcceptsOpenMetrics(HttpRequest request) { - var acceptHeader = request.Headers[HeaderNames.Accept]; + var acceptHeader = request.Headers.Accept; if (StringValues.IsNullOrEmpty(acceptHeader)) { From 82d1921067f9eaceeef2a67a7344ddfdb90df20c Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Wed, 6 Dec 2023 13:41:50 +1100 Subject: [PATCH 08/18] Fix build --- .../PrometheusExporterMiddleware.cs | 1 - .../PrometheusExporterMiddlewareTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 5eb955677cf..93ebffb5d5a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -17,7 +17,6 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Exporter.Prometheus.AspNetCore; using OpenTelemetry.Internal; diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 6cda261f8fa..834518a9e72 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -17,7 +17,6 @@ #if !NETFRAMEWORK using System.Diagnostics.Metrics; using System.Net; -using System.Net.Http.Headers; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; From 547782f897a91a295cde5235eb8cddc3e04185eb Mon Sep 17 00:00:00 2001 From: Robert Coltheart <13191652+robertcoltheart@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:42:41 +1100 Subject: [PATCH 09/18] Update src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> --- .../PrometheusExporterMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 93ebffb5d5a..747966dc89a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -111,7 +111,7 @@ public async Task InvokeAsync(HttpContext httpContext) this.exporter.OnExport = null; } - private bool AcceptsOpenMetrics(HttpRequest request) + private static bool AcceptsOpenMetrics(HttpRequest request) { var acceptHeader = request.Headers.Accept; From 3130372f3bdc34573015a973aa64de64f8bff677 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Wed, 6 Dec 2023 13:43:01 +1100 Subject: [PATCH 10/18] Fix build --- .../PrometheusExporterMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 747966dc89a..0c1df5ef4a6 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -68,7 +68,7 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var openMetricsRequested = this.AcceptsOpenMetrics(httpContext.Request); + var openMetricsRequested = AcceptsOpenMetrics(httpContext.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try From 36df8f82013a6c2e53fbd3ce35d8328bb7a9464a Mon Sep 17 00:00:00 2001 From: Robert Coltheart <13191652+robertcoltheart@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:07:57 +1100 Subject: [PATCH 11/18] Update src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> --- .../PrometheusHttpListener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index f8d5a4c393a..d4dbe052883 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -194,7 +194,7 @@ private async Task ProcessRequestAsync(HttpListenerContext context) } } - private bool AcceptsOpenMetrics(HttpListenerRequest request) + private static AcceptsOpenMetrics(HttpListenerRequest request) { if (request.AcceptTypes == null) { From 6558ca057d3d31f9b551567fdb149726efba752f Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Wed, 6 Dec 2023 21:28:19 +1100 Subject: [PATCH 12/18] Refactor and share more code, fix flaky tests --- ...etry.Exporter.Prometheus.AspNetCore.csproj | 1 + .../PrometheusExporterMiddleware.cs | 24 ++------- .../Internal/PrometheusCollectionManager.cs | 4 +- .../Internal/PrometheusExporter.cs | 2 +- .../Internal/PrometheusHeadersParser.cs} | 28 +++++++++-- .../PrometheusHttpListener.cs | 31 ++---------- .../PrometheusHeadersParserTests.cs | 50 +++++++++++++++++++ .../PrometheusHttpListenerTests.cs | 26 ++++++++-- 8 files changed, 107 insertions(+), 59 deletions(-) rename src/{OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs => OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs} (54%) create mode 100644 test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj index 3a421b0e63d..a94c7fdb1ae 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -28,6 +28,7 @@ + diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 0c1df5ef4a6..b16d9bc78af 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using OpenTelemetry.Exporter.Prometheus; -using OpenTelemetry.Exporter.Prometheus.AspNetCore; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; @@ -29,9 +28,8 @@ namespace OpenTelemetry.Exporter; /// internal sealed class PrometheusExporterMiddleware { - private const string OpenMetricsMediaType = "application/openmetrics-text"; - private readonly PrometheusExporter exporter; + private readonly PrometheusHeadersParser headersParser = new(); /// /// Initializes a new instance of the class. @@ -111,7 +109,7 @@ public async Task InvokeAsync(HttpContext httpContext) this.exporter.OnExport = null; } - private static bool AcceptsOpenMetrics(HttpRequest request) + private bool AcceptsOpenMetrics(HttpRequest request) { var acceptHeader = request.Headers.Accept; @@ -120,22 +118,6 @@ private static bool AcceptsOpenMetrics(HttpRequest request) return false; } - foreach (var accept in acceptHeader) - { - var value = accept.AsSpan(); - - while (value.Length > 0) - { - var headerValue = value.SplitNext(','); - var mediaType = headerValue.SplitNext(';'); - - if (mediaType.Equals(OpenMetricsMediaType, StringComparison.Ordinal)) - { - return true; - } - } - } - - return false; + return acceptHeader.Any(x => this.headersParser.AcceptsOpenMetrics(x)); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index a10ac55f42d..402c6c13df7 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -174,7 +174,7 @@ private bool ExecuteCollect(bool openMetricsRequested) this.exporter.OpenMetricsRequested = openMetricsRequested; var result = this.exporter.Collect(Timeout.Infinite); this.exporter.OnExport = null; - this.exporter.OpenMetricsRequested = null; + this.exporter.OpenMetricsRequested = false; return result; } @@ -200,7 +200,7 @@ private ExportResult OnCollect(Batch metrics) cursor, metric, this.GetPrometheusMetric(metric), - this.exporter.OpenMetricsRequested ?? false); + this.exporter.OpenMetricsRequested); break; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index 02204b7de6b..b02a3a64b67 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -63,7 +63,7 @@ internal Func, ExportResult> OnExport internal int ScrapeResponseCacheDurationMilliseconds { get; } - internal bool? OpenMetricsRequested { get; set; } + internal bool OpenMetricsRequested { get; set; } /// public override ExportResult Export(in Batch metrics) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs similarity index 54% rename from src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs rename to src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs index 9c8635ed420..7a5cd907176 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/ReadOnlySpanExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,31 @@ // limitations under the License. // -namespace OpenTelemetry.Exporter.Prometheus.AspNetCore; +namespace OpenTelemetry.Exporter.Prometheus; -internal static class ReadOnlySpanExtensions +internal class PrometheusHeadersParser { - internal static ReadOnlySpan SplitNext(this ref ReadOnlySpan span, char character) + private const string OpenMetricsMediaType = "application/openmetrics-text"; + + internal bool AcceptsOpenMetrics(string contentType) + { + var value = contentType.AsSpan(); + + while (value.Length > 0) + { + var headerValue = SplitNext(ref value, ','); + var mediaType = SplitNext(ref headerValue, ';'); + + if (mediaType.Equals(OpenMetricsMediaType.AsSpan(), StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static ReadOnlySpan SplitNext(ref ReadOnlySpan span, char character) { var index = span.IndexOf(character); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index d4dbe052883..88bc1f82063 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -22,9 +22,8 @@ namespace OpenTelemetry.Exporter; internal sealed class PrometheusHttpListener : IDisposable { - private const string OpenMetricsMediaType = "application/openmetrics-text"; - private readonly PrometheusExporter exporter; + private readonly PrometheusHeadersParser headersParser = new(); private readonly HttpListener httpListener = new(); private readonly object syncObject = new(); @@ -194,35 +193,15 @@ private async Task ProcessRequestAsync(HttpListenerContext context) } } - private static AcceptsOpenMetrics(HttpListenerRequest request) + private bool AcceptsOpenMetrics(HttpListenerRequest request) { - if (request.AcceptTypes == null) - { - return false; - } - - foreach (var acceptType in request.AcceptTypes) - { - if (this.GetMediaType(acceptType) == OpenMetricsMediaType) - { - return true; - } - } - - return false; - } + var acceptHeader = request.Headers["Accept"]; - private string GetMediaType(string acceptHeader) - { if (string.IsNullOrEmpty(acceptHeader)) { - return string.Empty; + return false; } - var separatorIndex = acceptHeader.IndexOf(';'); - - return separatorIndex == -1 - ? acceptHeader - : acceptHeader.Substring(0, separatorIndex); + return this.headersParser.AcceptsOpenMetrics(acceptHeader); } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs new file mode 100644 index 00000000000..797a5da85ad --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs @@ -0,0 +1,50 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public class PrometheusHeadersParserTests +{ + [Theory] + [InlineData("application/openmetrics-text")] + [InlineData("application/openmetrics-text; version=1.0.0")] + [InlineData("application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain,application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain; charset=utf-8,application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain, */*;q=0.8,application/openmetrics-text; version=1.0.0; charset=utf-8")] + public void ParseHeader_AcceptHeaders_OpenMetricsValid(string header) + { + var parser = new PrometheusHeadersParser(); + var result = parser.AcceptsOpenMetrics(header); + + Assert.True(result); + } + + [Theory] + [InlineData("text/plain")] + [InlineData("text/plain; charset=utf-8")] + [InlineData("text/plain; charset=utf-8; version=0.0.4")] + [InlineData("*/*;q=0.8,text/plain; charset=utf-8; version=0.0.4")] + public void ParseHeader_AcceptHeaders_OtherHeadersInvalid(string header) + { + var parser = new PrometheusHeadersParser(); + var result = parser.AcceptsOpenMetrics(header); + + Assert.False(result); + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index dde4c853254..7959884920d 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -110,7 +110,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri int port = 0; string address = null; - MeterProvider provider; + MeterProvider provider = null; using var meter = new Meter(this.meterName); while (retryAttempts-- != 0) @@ -118,10 +118,24 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri port = random.Next(2000, 5000); address = $"http://localhost:{port}/"; - provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) - .Build(); + try + { + provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) + .Build(); + + break; + } + catch + { + // ignored + } + } + + if (provider == null) + { + throw new InvalidOperationException("HttpListener could not be started"); } var tags = new KeyValuePair[] @@ -183,5 +197,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri { Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + provider.Dispose(); } } From 75e0bbbb67e46dc86ac469209556a977466c6ecf Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Wed, 6 Dec 2023 21:33:10 +1100 Subject: [PATCH 13/18] Tidy usings --- .../PrometheusHttpListenerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 7959884920d..0a85d95ddbb 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -16,7 +16,6 @@ using System.Diagnostics.Metrics; using System.Net; - #if NETFRAMEWORK using System.Net.Http; #endif From 75df04a02adc4a711046cf95f1195913bc142e5b Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Wed, 6 Dec 2023 21:51:20 +1100 Subject: [PATCH 14/18] Refactor tests --- .../PrometheusExporterMiddlewareTests.cs | 20 +++++++------------ .../PrometheusHttpListenerTests.cs | 19 +++++++----------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 834518a9e72..072b3b4511b 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -244,7 +244,7 @@ public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse() return RunPrometheusExporterMiddlewareIntegrationTest( "/metrics", app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - requestOpenMetrics: false); + acceptHeader: "text/plain"); } [Fact] @@ -253,7 +253,7 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader( return RunPrometheusExporterMiddlewareIntegrationTest( "/metrics", app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - openMetricsVersion: "1.0.0"); + acceptHeader: "application/openmetrics-text; version=1.0.0"); } private static async Task RunPrometheusExporterMiddlewareIntegrationTest( @@ -264,9 +264,10 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( bool registerMeterProvider = true, Action configureOptions = null, bool skipMetrics = false, - bool requestOpenMetrics = true, - string openMetricsVersion = null) + string acceptHeader = "application/openmetrics-text") { + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); + using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => webBuilder .UseTestServer() @@ -306,16 +307,9 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( using var client = host.GetTestClient(); - if (requestOpenMetrics) + if (!string.IsNullOrEmpty(acceptHeader)) { - var mediaType = "application/openmetrics-text"; - - if (!string.IsNullOrEmpty(openMetricsVersion)) - { - mediaType += $";version={openMetricsVersion}"; - } - - client.DefaultRequestHeaders.Add("Accept", mediaType); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); } using var response = await client.GetAsync(path); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 0a85d95ddbb..e9e270dd5b4 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -93,17 +93,19 @@ public async Task PrometheusExporterHttpServerIntegration_NoMetrics() [Fact] public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() { - await this.RunPrometheusExporterHttpServerIntegrationTest(requestOpenMetrics: false); + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty); } [Fact] public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader() { - await this.RunPrometheusExporterHttpServerIntegrationTest(openMetricsVersion: "1.0.0"); + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, bool requestOpenMetrics = true, string openMetricsVersion = null) + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text") { + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); + Random random = new Random(); int retryAttempts = 5; int port = 0; @@ -152,16 +154,9 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri using HttpClient client = new HttpClient(); - if (requestOpenMetrics) + if (!string.IsNullOrEmpty(acceptHeader)) { - var mediaType = "application/openmetrics-text"; - - if (!string.IsNullOrEmpty(openMetricsVersion)) - { - mediaType += $";version={openMetricsVersion}"; - } - - client.DefaultRequestHeaders.Add("Accept", mediaType); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); } using var response = await client.GetAsync($"{address}metrics"); From bff36015a4074ee772caa09cc957cbb88f6eaa97 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Thu, 7 Dec 2023 07:40:57 +1100 Subject: [PATCH 15/18] Fix build --- .../PrometheusExporterMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index b16d9bc78af..e4b6f6c21dc 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -66,7 +66,7 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var openMetricsRequested = AcceptsOpenMetrics(httpContext.Request); + var openMetricsRequested = this.AcceptsOpenMetrics(httpContext.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try From 8dd0cbfdc24f5dfcb86391585c59fc325bf957bd Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Thu, 7 Dec 2023 10:23:54 +1100 Subject: [PATCH 16/18] Make static and remove redundant set --- .../PrometheusExporterMiddleware.cs | 7 +++-- .../Internal/PrometheusCollectionManager.cs | 1 - .../Internal/PrometheusHeadersParser.cs | 4 +-- .../PrometheusHttpListener.cs | 27 +++++++++---------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index e4b6f6c21dc..223ed4d8c00 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -29,7 +29,6 @@ namespace OpenTelemetry.Exporter; internal sealed class PrometheusExporterMiddleware { private readonly PrometheusExporter exporter; - private readonly PrometheusHeadersParser headersParser = new(); /// /// Initializes a new instance of the class. @@ -66,7 +65,7 @@ public async Task InvokeAsync(HttpContext httpContext) try { - var openMetricsRequested = this.AcceptsOpenMetrics(httpContext.Request); + var openMetricsRequested = AcceptsOpenMetrics(httpContext.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try @@ -109,7 +108,7 @@ public async Task InvokeAsync(HttpContext httpContext) this.exporter.OnExport = null; } - private bool AcceptsOpenMetrics(HttpRequest request) + private static bool AcceptsOpenMetrics(HttpRequest request) { var acceptHeader = request.Headers.Accept; @@ -118,6 +117,6 @@ private bool AcceptsOpenMetrics(HttpRequest request) return false; } - return acceptHeader.Any(x => this.headersParser.AcceptsOpenMetrics(x)); + return acceptHeader.Any(PrometheusHeadersParser.AcceptsOpenMetrics); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index 402c6c13df7..c0356597b64 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -174,7 +174,6 @@ private bool ExecuteCollect(bool openMetricsRequested) this.exporter.OpenMetricsRequested = openMetricsRequested; var result = this.exporter.Collect(Timeout.Infinite); this.exporter.OnExport = null; - this.exporter.OpenMetricsRequested = false; return result; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs index 7a5cd907176..8548f6d16ec 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs @@ -16,11 +16,11 @@ namespace OpenTelemetry.Exporter.Prometheus; -internal class PrometheusHeadersParser +internal static class PrometheusHeadersParser { private const string OpenMetricsMediaType = "application/openmetrics-text"; - internal bool AcceptsOpenMetrics(string contentType) + internal static bool AcceptsOpenMetrics(string contentType) { var value = contentType.AsSpan(); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index 88bc1f82063..914f448df94 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -23,7 +23,6 @@ namespace OpenTelemetry.Exporter; internal sealed class PrometheusHttpListener : IDisposable { private readonly PrometheusExporter exporter; - private readonly PrometheusHeadersParser headersParser = new(); private readonly HttpListener httpListener = new(); private readonly object syncObject = new(); @@ -111,6 +110,18 @@ public void Dispose() } } + private static bool AcceptsOpenMetrics(HttpListenerRequest request) + { + var acceptHeader = request.Headers["Accept"]; + + if (string.IsNullOrEmpty(acceptHeader)) + { + return false; + } + + return PrometheusHeadersParser.AcceptsOpenMetrics(acceptHeader); + } + private void WorkerProc() { this.httpListener.Start(); @@ -149,7 +160,7 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { try { - var openMetricsRequested = this.AcceptsOpenMetrics(context.Request); + var openMetricsRequested = AcceptsOpenMetrics(context.Request); var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try @@ -192,16 +203,4 @@ private async Task ProcessRequestAsync(HttpListenerContext context) { } } - - private bool AcceptsOpenMetrics(HttpListenerRequest request) - { - var acceptHeader = request.Headers["Accept"]; - - if (string.IsNullOrEmpty(acceptHeader)) - { - return false; - } - - return this.headersParser.AcceptsOpenMetrics(acceptHeader); - } } From 188d59d6cd9d33343a65bfc816e8b0fcda405425 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Thu, 7 Dec 2023 10:33:13 +1100 Subject: [PATCH 17/18] Oops fix build --- .../PrometheusHeadersParserTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs index 797a5da85ad..bc82596f3cf 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs @@ -29,8 +29,7 @@ public class PrometheusHeadersParserTests [InlineData("text/plain, */*;q=0.8,application/openmetrics-text; version=1.0.0; charset=utf-8")] public void ParseHeader_AcceptHeaders_OpenMetricsValid(string header) { - var parser = new PrometheusHeadersParser(); - var result = parser.AcceptsOpenMetrics(header); + var result = PrometheusHeadersParser.AcceptsOpenMetrics(header); Assert.True(result); } @@ -42,8 +41,7 @@ public void ParseHeader_AcceptHeaders_OpenMetricsValid(string header) [InlineData("*/*;q=0.8,text/plain; charset=utf-8; version=0.0.4")] public void ParseHeader_AcceptHeaders_OtherHeadersInvalid(string header) { - var parser = new PrometheusHeadersParser(); - var result = parser.AcceptsOpenMetrics(header); + var result = PrometheusHeadersParser.AcceptsOpenMetrics(header); Assert.False(result); } From 015ad3bd4ff83d1d9d09a43be8fdb66145b50041 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Thu, 7 Dec 2023 10:56:04 +1100 Subject: [PATCH 18/18] Use foreach instead of linq --- .../PrometheusExporterMiddleware.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 223ed4d8c00..72145bdaed2 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -117,6 +117,14 @@ private static bool AcceptsOpenMetrics(HttpRequest request) return false; } - return acceptHeader.Any(PrometheusHeadersParser.AcceptsOpenMetrics); + foreach (var header in acceptHeader) + { + if (PrometheusHeadersParser.AcceptsOpenMetrics(header)) + { + return true; + } + } + + return false; } }