Skip to content

Commit

Permalink
[prometheus] Support meter-level tags (#5837)
Browse files Browse the repository at this point in the history
Co-authored-by: Cijo Thomas <[email protected]>
  • Loading branch information
robertcoltheart and cijothomas authored Sep 23, 2024
1 parent c1a1931 commit 60b7d9b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 43 deletions.
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,15 @@ public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTa
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,43 +249,53 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader(
}

[Fact]
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats()
public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse_WithMeterTags()
{
using var host = await StartTestHostAsync(
app => app.UseOpenTelemetryPrometheusScrapingEndpoint());

var tags = new KeyValuePair<string, object?>[]
var meterTags = new KeyValuePair<string, object?>[]
{
new("key1", "value1"),
new("key2", "value2"),
new("meterKey1", "value1"),
new("meterKey2", "value2"),
};

using var meter = new Meter(MeterName, MeterVersion);

var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
acceptHeader: "text/plain",
meterTags: meterTags);
}

var counter = meter.CreateCounter<double>("counter_double", unit: "By");
counter.Add(100.18D, tags);
counter.Add(0.99D, tags);
[Fact]
public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader_WithMeterTags()
{
var meterTags = new KeyValuePair<string, object?>[]
{
new("meterKey1", "value1"),
new("meterKey2", "value2"),
};

var testCases = new bool[] { true, false, true, true, false };
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
acceptHeader: "application/openmetrics-text; version=1.0.0",
meterTags: meterTags);
}

using var client = host.GetTestClient();
[Fact]
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_NoMeterTags()
{
await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats();
}

foreach (var testCase in testCases)
[Fact]
public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_WithMeterTags()
{
var meterTags = new KeyValuePair<string, object?>[]
{
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();
await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(meterTags);
}

[Fact]
Expand All @@ -312,6 +322,45 @@ public async Task PrometheusExporterMiddlewareIntegration_TestBufferSizeIncrease
await host.StopAsync();
}

private static async Task RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(KeyValuePair<string, object?>[]? meterTags = null)
{
using var host = await StartTestHostAsync(
app => app.UseOpenTelemetryPrometheusScrapingEndpoint());

var counterTags = new KeyValuePair<string, object?>[]
{
new("key1", "value1"),
new("key2", "value2"),
};

using var meter = new Meter(MeterName, MeterVersion, meterTags);

var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();

var counter = meter.CreateCounter<double>("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<IApplicationBuilder> configure,
Expand All @@ -320,27 +369,28 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
bool registerMeterProvider = true,
Action<PrometheusAspNetCoreOptions>? configureOptions = null,
bool skipMetrics = false,
string acceptHeader = "application/openmetrics-text")
string acceptHeader = "application/openmetrics-text",
KeyValuePair<string, object?>[]? meterTags = null)
{
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");

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

var tags = new KeyValuePair<string, object?>[]
var counterTags = new KeyValuePair<string, object?>[]
{
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<double>("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();
Expand All @@ -356,7 +406,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(

if (!skipMetrics)
{
await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics);
await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics, meterTags);
}
else
{
Expand All @@ -368,7 +418,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
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<string, object?>[]? meterTags)
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(response.Content.Headers.Contains("Last-Modified"));
Expand All @@ -382,6 +432,10 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht
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
Expand All @@ -394,14 +448,14 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionH
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0");
}

[Fact]
public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics_WithMeterTags()
{
var tags = new KeyValuePair<string, object?>[]
{
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<string, object?>[]
{
new("meter1", "value1"),
new("meter2", "value2"),
};

await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags);
}

[Fact]
public void PrometheusHttpListenerThrowsOnStart()
{
Expand Down Expand Up @@ -236,15 +260,15 @@ private static MeterProvider BuildMeterProvider(Meter meter, IEnumerable<KeyValu
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<string, object?>[]? 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<string, object?>[]
var counterTags = new KeyValuePair<string, object?>[]
{
new("key1", "value1"),
new("key2", "value2"),
Expand All @@ -253,8 +277,8 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
var counter = meter.CreateCounter<double>("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();
Expand All @@ -280,6 +304,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
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
Expand All @@ -291,11 +319,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
+ $"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);
Expand Down

0 comments on commit 60b7d9b

Please sign in to comment.