Skip to content

Commit 60b7d9b

Browse files
[prometheus] Support meter-level tags (open-telemetry#5837)
Co-authored-by: Cijo Thomas <[email protected]>
1 parent c1a1931 commit 60b7d9b

File tree

5 files changed

+140
-43
lines changed

5 files changed

+140
-43
lines changed

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Notes](../../RELEASENOTES.md).
77

88
## Unreleased
99

10+
* Added meter-level tags to Prometheus exporter
11+
([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837))
12+
1013
## 1.9.0-beta.2
1114

1215
Released 2024-Jun-24

src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Notes](../../RELEASENOTES.md).
77

88
## Unreleased
99

10+
* Added meter-level tags to Prometheus exporter
11+
([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837))
12+
1013
## 1.9.0-beta.2
1114

1215
Released 2024-Jun-24

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,15 @@ public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTa
404404
buffer[cursor++] = unchecked((byte)',');
405405
}
406406

407+
if (metric.MeterTags != null)
408+
{
409+
foreach (var tag in metric.MeterTags)
410+
{
411+
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
412+
buffer[cursor++] = unchecked((byte)',');
413+
}
414+
}
415+
407416
foreach (var tag in tags)
408417
{
409418
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);

test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -249,43 +249,53 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader(
249249
}
250250

251251
[Fact]
252-
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats()
252+
public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse_WithMeterTags()
253253
{
254-
using var host = await StartTestHostAsync(
255-
app => app.UseOpenTelemetryPrometheusScrapingEndpoint());
256-
257-
var tags = new KeyValuePair<string, object?>[]
254+
var meterTags = new KeyValuePair<string, object?>[]
258255
{
259-
new("key1", "value1"),
260-
new("key2", "value2"),
256+
new("meterKey1", "value1"),
257+
new("meterKey2", "value2"),
261258
};
262259

263-
using var meter = new Meter(MeterName, MeterVersion);
264-
265-
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
260+
return RunPrometheusExporterMiddlewareIntegrationTest(
261+
"/metrics",
262+
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
263+
acceptHeader: "text/plain",
264+
meterTags: meterTags);
265+
}
266266

267-
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
268-
counter.Add(100.18D, tags);
269-
counter.Add(0.99D, tags);
267+
[Fact]
268+
public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader_WithMeterTags()
269+
{
270+
var meterTags = new KeyValuePair<string, object?>[]
271+
{
272+
new("meterKey1", "value1"),
273+
new("meterKey2", "value2"),
274+
};
270275

271-
var testCases = new bool[] { true, false, true, true, false };
276+
return RunPrometheusExporterMiddlewareIntegrationTest(
277+
"/metrics",
278+
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
279+
acceptHeader: "application/openmetrics-text; version=1.0.0",
280+
meterTags: meterTags);
281+
}
272282

273-
using var client = host.GetTestClient();
283+
[Fact]
284+
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_NoMeterTags()
285+
{
286+
await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats();
287+
}
274288

275-
foreach (var testCase in testCases)
289+
[Fact]
290+
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_WithMeterTags()
291+
{
292+
var meterTags = new KeyValuePair<string, object?>[]
276293
{
277-
using var request = new HttpRequestMessage
278-
{
279-
Headers = { { "Accept", testCase ? "application/openmetrics-text" : "text/plain" } },
280-
RequestUri = new Uri("/metrics", UriKind.Relative),
281-
Method = HttpMethod.Get,
282-
};
283-
using var response = await client.SendAsync(request);
284-
var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
285-
await VerifyAsync(beginTimestamp, endTimestamp, response, testCase);
286-
}
294+
new("meterKey1", "value1"),
295+
new("meterKey2", "value2"),
296+
};
287297

288-
await host.StopAsync();
298+
await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(meterTags);
289299
}
290300

291301
[Fact]
@@ -312,6 +322,45 @@ public async Task PrometheusExporterMiddlewareIntegration_TestBufferSizeIncrease
312322
await host.StopAsync();
313323
}
314324

325+
private static async Task RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(KeyValuePair<string, object?>[]? meterTags = null)
326+
{
327+
using var host = await StartTestHostAsync(
328+
app => app.UseOpenTelemetryPrometheusScrapingEndpoint());
329+
330+
var counterTags = new KeyValuePair<string, object?>[]
331+
{
332+
new("key1", "value1"),
333+
new("key2", "value2"),
334+
};
335+
336+
using var meter = new Meter(MeterName, MeterVersion, meterTags);
337+
338+
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
339+
340+
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
341+
counter.Add(100.18D, counterTags);
342+
counter.Add(0.99D, counterTags);
343+
344+
var testCases = new bool[] { true, false, true, true, false };
345+
346+
using var client = host.GetTestClient();
347+
348+
foreach (var testCase in testCases)
349+
{
350+
using var request = new HttpRequestMessage
351+
{
352+
Headers = { { "Accept", testCase ? "application/openmetrics-text" : "text/plain" } },
353+
RequestUri = new Uri("/metrics", UriKind.Relative),
354+
Method = HttpMethod.Get,
355+
};
356+
using var response = await client.SendAsync(request);
357+
var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
358+
await VerifyAsync(beginTimestamp, endTimestamp, response, testCase, meterTags);
359+
}
360+
361+
await host.StopAsync();
362+
}
363+
315364
private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
316365
string path,
317366
Action<IApplicationBuilder> configure,
@@ -320,27 +369,28 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
320369
bool registerMeterProvider = true,
321370
Action<PrometheusAspNetCoreOptions>? configureOptions = null,
322371
bool skipMetrics = false,
323-
string acceptHeader = "application/openmetrics-text")
372+
string acceptHeader = "application/openmetrics-text",
373+
KeyValuePair<string, object?>[]? meterTags = null)
324374
{
325375
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");
326376

327377
using var host = await StartTestHostAsync(configure, configureServices, registerMeterProvider, configureOptions);
328378

329-
var tags = new KeyValuePair<string, object?>[]
379+
var counterTags = new KeyValuePair<string, object?>[]
330380
{
331381
new("key1", "value1"),
332382
new("key2", "value2"),
333383
};
334384

335-
using var meter = new Meter(MeterName, MeterVersion);
385+
using var meter = new Meter(MeterName, MeterVersion, meterTags);
336386

337387
var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
338388

339389
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
340390
if (!skipMetrics)
341391
{
342-
counter.Add(100.18D, tags);
343-
counter.Add(0.99D, tags);
392+
counter.Add(100.18D, counterTags);
393+
counter.Add(0.99D, counterTags);
344394
}
345395

346396
using var client = host.GetTestClient();
@@ -356,7 +406,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
356406

357407
if (!skipMetrics)
358408
{
359-
await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics);
409+
await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics, meterTags);
360410
}
361411
else
362412
{
@@ -368,7 +418,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
368418
await host.StopAsync();
369419
}
370420

371-
private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics)
421+
private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics, KeyValuePair<string, object?>[]? meterTags)
372422
{
373423
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
374424
Assert.True(response.Content.Headers.Contains("Last-Modified"));
@@ -382,6 +432,10 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht
382432
Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString());
383433
}
384434

435+
var additionalTags = meterTags != null && meterTags.Any()
436+
? $"{string.Join(",", meterTags.Select(x => $"{x.Key}=\"{x.Value}\""))},"
437+
: string.Empty;
438+
385439
string content = (await response.Content.ReadAsStringAsync()).ReplaceLineEndings();
386440

387441
string expected = requestOpenMetrics
@@ -394,14 +448,14 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht
394448
otel_scope_info{otel_scope_name="{{MeterName}}"} 1
395449
# TYPE counter_double_bytes counter
396450
# UNIT counter_double_bytes bytes
397-
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
451+
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+\.\d{3})
398452
# EOF
399453
400454
""".ReplaceLineEndings()
401455
: $$"""
402456
# TYPE counter_double_bytes_total counter
403457
# UNIT counter_double_bytes_total bytes
404-
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+)
458+
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+)
405459
# EOF
406460
407461
""".ReplaceLineEndings();

test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionH
8484
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0");
8585
}
8686

87+
[Fact]
88+
public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics_WithMeterTags()
89+
{
90+
var tags = new KeyValuePair<string, object?>[]
91+
{
92+
new("meter1", "value1"),
93+
new("meter2", "value2"),
94+
};
95+
96+
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, meterTags: tags);
97+
}
98+
99+
[Fact]
100+
public async Task PrometheusExporterHttpServerIntegration_OpenMetrics_WithMeterTags()
101+
{
102+
var tags = new KeyValuePair<string, object?>[]
103+
{
104+
new("meter1", "value1"),
105+
new("meter2", "value2"),
106+
};
107+
108+
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags);
109+
}
110+
87111
[Fact]
88112
public void PrometheusHttpListenerThrowsOnStart()
89113
{
@@ -236,15 +260,15 @@ private static MeterProvider BuildMeterProvider(Meter meter, IEnumerable<KeyValu
236260
return provider;
237261
}
238262

239-
private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text")
263+
private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", KeyValuePair<string, object?>[]? meterTags = null)
240264
{
241265
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");
242266

243-
using var meter = new Meter(MeterName, MeterVersion);
267+
using var meter = new Meter(MeterName, MeterVersion, meterTags);
244268

245269
var provider = BuildMeterProvider(meter, [], out var address);
246270

247-
var tags = new KeyValuePair<string, object?>[]
271+
var counterTags = new KeyValuePair<string, object?>[]
248272
{
249273
new("key1", "value1"),
250274
new("key2", "value2"),
@@ -253,8 +277,8 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
253277
var counter = meter.CreateCounter<double>("counter_double", unit: "By");
254278
if (!skipMetrics)
255279
{
256-
counter.Add(100.18D, tags);
257-
counter.Add(0.99D, tags);
280+
counter.Add(100.18D, counterTags);
281+
counter.Add(0.99D, counterTags);
258282
}
259283

260284
using HttpClient client = new HttpClient();
@@ -280,6 +304,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
280304
Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString());
281305
}
282306

307+
var additionalTags = meterTags != null && meterTags.Any()
308+
? $"{string.Join(",", meterTags.Select(x => $"{x.Key}='{x.Value}'"))},"
309+
: string.Empty;
310+
283311
var content = await response.Content.ReadAsStringAsync();
284312

285313
var expected = requestOpenMetrics
@@ -291,11 +319,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
291319
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
292320
+ "# TYPE counter_double_bytes counter\n"
293321
+ "# UNIT counter_double_bytes bytes\n"
294-
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
322+
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
295323
+ "# EOF\n"
296324
: "# TYPE counter_double_bytes_total counter\n"
297325
+ "# UNIT counter_double_bytes_total bytes\n"
298-
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
326+
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+)\n"
299327
+ "# EOF\n";
300328

301329
Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);

0 commit comments

Comments
 (0)