diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index 587692aa8..33cf84f17 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -7,6 +7,9 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added meter-level tags to Prometheus exporter + ([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837)) + ## 1.9.0-beta.2 Released 2024-Jun-24 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index 1893e205c..5873577fb 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -7,6 +7,9 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added meter-level tags to Prometheus exporter + ([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837)) + ## 1.9.0-beta.2 Released 2024-Jun-24 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 7623de406..b182b5215 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -404,6 +404,15 @@ internal static partial class PrometheusSerializer buffer[cursor++] = unchecked((byte)','); } + if (metric.MeterTags != null) + { + foreach (var tag in metric.MeterTags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + } + foreach (var tag in tags) { cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 3f23d764e..c34dba295 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -249,43 +249,53 @@ public sealed class PrometheusExporterMiddlewareTests } [Fact] - public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats() + public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse_WithMeterTags() { - using var host = await StartTestHostAsync( - app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); - - var tags = new KeyValuePair[] + var meterTags = new KeyValuePair[] { - new("key1", "value1"), - new("key2", "value2"), + new("meterKey1", "value1"), + new("meterKey2", "value2"), }; - using var meter = new Meter(MeterName, MeterVersion); + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "text/plain", + meterTags: meterTags); + } - var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - - var counter = meter.CreateCounter("counter_double", unit: "By"); - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); - - var testCases = new bool[] { true, false, true, true, false }; - - using var client = host.GetTestClient(); - - foreach (var testCase in testCases) + [Fact] + public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader_WithMeterTags() + { + var meterTags = new KeyValuePair[] { - using var request = new HttpRequestMessage - { - Headers = { { "Accept", testCase ? "application/openmetrics-text" : "text/plain" } }, - RequestUri = new Uri("/metrics", UriKind.Relative), - Method = HttpMethod.Get, - }; - using var response = await client.SendAsync(request); - var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - await VerifyAsync(beginTimestamp, endTimestamp, response, testCase); - } + new("meterKey1", "value1"), + new("meterKey2", "value2"), + }; - await host.StopAsync(); + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "application/openmetrics-text; version=1.0.0", + meterTags: meterTags); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_NoMeterTags() + { + await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_WithMeterTags() + { + var meterTags = new KeyValuePair[] + { + new("meterKey1", "value1"), + new("meterKey2", "value2"), + }; + + await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(meterTags); } [Fact] @@ -312,6 +322,45 @@ public sealed class PrometheusExporterMiddlewareTests await host.StopAsync(); } + private static async Task RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(KeyValuePair[]? meterTags = null) + { + using var host = await StartTestHostAsync( + app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); + + var counterTags = new KeyValuePair[] + { + new("key1", "value1"), + new("key2", "value2"), + }; + + using var meter = new Meter(MeterName, MeterVersion, meterTags); + + var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + var counter = meter.CreateCounter("counter_double", unit: "By"); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); + + var testCases = new bool[] { true, false, true, true, false }; + + using var client = host.GetTestClient(); + + foreach (var testCase in testCases) + { + using var request = new HttpRequestMessage + { + Headers = { { "Accept", testCase ? "application/openmetrics-text" : "text/plain" } }, + RequestUri = new Uri("/metrics", UriKind.Relative), + Method = HttpMethod.Get, + }; + using var response = await client.SendAsync(request); + var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + await VerifyAsync(beginTimestamp, endTimestamp, response, testCase, meterTags); + } + + await host.StopAsync(); + } + private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, @@ -320,27 +369,28 @@ public sealed class PrometheusExporterMiddlewareTests bool registerMeterProvider = true, Action? configureOptions = null, bool skipMetrics = false, - string acceptHeader = "application/openmetrics-text") + string acceptHeader = "application/openmetrics-text", + KeyValuePair[]? meterTags = null) { var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); using var host = await StartTestHostAsync(configure, configureServices, registerMeterProvider, configureOptions); - var tags = new KeyValuePair[] + var counterTags = new KeyValuePair[] { new("key1", "value1"), new("key2", "value2"), }; - using var meter = new Meter(MeterName, MeterVersion); + using var meter = new Meter(MeterName, MeterVersion, meterTags); var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var counter = meter.CreateCounter("counter_double", unit: "By"); if (!skipMetrics) { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); } using var client = host.GetTestClient(); @@ -356,7 +406,7 @@ public sealed class PrometheusExporterMiddlewareTests if (!skipMetrics) { - await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics); + await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics, meterTags); } else { @@ -368,7 +418,7 @@ public sealed class PrometheusExporterMiddlewareTests await host.StopAsync(); } - private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics) + private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics, KeyValuePair[]? meterTags) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Content.Headers.Contains("Last-Modified")); @@ -382,6 +432,10 @@ public sealed class PrometheusExporterMiddlewareTests Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString()); } + var additionalTags = meterTags != null && meterTags.Any() + ? $"{string.Join(",", meterTags.Select(x => $"{x.Key}=\"{x.Value}\""))}," + : string.Empty; + string content = (await response.Content.ReadAsStringAsync()).ReplaceLineEndings(); string expected = requestOpenMetrics @@ -394,14 +448,14 @@ public sealed class PrometheusExporterMiddlewareTests otel_scope_info{otel_scope_name="{{MeterName}}"} 1 # TYPE counter_double_bytes counter # UNIT counter_double_bytes bytes - counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3}) + counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+\.\d{3}) # EOF """.ReplaceLineEndings() : $$""" # TYPE counter_double_bytes_total counter # UNIT counter_double_bytes_total bytes - counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+) + counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+) # EOF """.ReplaceLineEndings(); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 07ee28beb..b9b620118 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -84,6 +84,30 @@ public class PrometheusHttpListenerTests await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); } + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics_WithMeterTags() + { + var tags = new KeyValuePair[] + { + new("meter1", "value1"), + new("meter2", "value2"), + }; + + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, meterTags: tags); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration_OpenMetrics_WithMeterTags() + { + var tags = new KeyValuePair[] + { + new("meter1", "value1"), + new("meter2", "value2"), + }; + + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags); + } + [Fact] public void PrometheusHttpListenerThrowsOnStart() { @@ -236,15 +260,15 @@ public class PrometheusHttpListenerTests return provider; } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text") + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", KeyValuePair[]? meterTags = null) { var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); - using var meter = new Meter(MeterName, MeterVersion); + using var meter = new Meter(MeterName, MeterVersion, meterTags); var provider = BuildMeterProvider(meter, [], out var address); - var tags = new KeyValuePair[] + var counterTags = new KeyValuePair[] { new("key1", "value1"), new("key2", "value2"), @@ -253,8 +277,8 @@ public class PrometheusHttpListenerTests var counter = meter.CreateCounter("counter_double", unit: "By"); if (!skipMetrics) { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); } using HttpClient client = new HttpClient(); @@ -280,6 +304,10 @@ public class PrometheusHttpListenerTests Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString()); } + var additionalTags = meterTags != null && meterTags.Any() + ? $"{string.Join(",", meterTags.Select(x => $"{x.Key}='{x.Value}'"))}," + : string.Empty; + var content = await response.Content.ReadAsStringAsync(); var expected = requestOpenMetrics @@ -291,11 +319,11 @@ public class PrometheusHttpListenerTests + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + "# TYPE counter_double_bytes counter\n" + "# UNIT counter_double_bytes bytes\n" - + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + "# EOF\n" : "# TYPE counter_double_bytes_total counter\n" + "# UNIT counter_double_bytes_total bytes\n" - + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+)\n" + "# EOF\n"; Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);