Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the current tests have been updated to validate MeterTags. Is there a test that validates null MeterTags?"

{
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