Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
15ba450
[Exporter.Prometheus] Fix scope metadata
martincostello May 1, 2026
db4ca48
[Exporter.Prometheus] Update test cases
martincostello May 1, 2026
50942c7
[Exporter.Prometheus] Update CHANGELOGs
martincostello May 1, 2026
60a53b7
[Exporter.Prometheus] Address feedback
martincostello May 1, 2026
65db37d
Merge branch 'main' into fix-scope-metadata
martincostello May 1, 2026
a51c12a
[Exporter.Prometheus] Extend coverage
martincostello May 1, 2026
48c6919
Merge branch 'main' into fix-scope-metadata
martincostello May 4, 2026
9402121
Merge branch 'main' into fix-scope-metadata
martincostello May 5, 2026
ab0849c
Merge branch 'main' into fix-scope-metadata
martincostello May 5, 2026
926f714
[Exporter.Prometheus] Update CHANGELOGs
martincostello May 5, 2026
25e7374
Merge branch 'main' into fix-scope-metadata
martincostello May 13, 2026
00ab131
Merge branch 'main' into fix-scope-metadata
martincostello May 18, 2026
c5d2abb
Merge branch 'main' into fix-scope-metadata
martincostello May 19, 2026
9dfc038
[Exporter.Prometheus] Address feedback
martincostello May 19, 2026
a5a99b4
[Exporter.Prometheus] Fix CHANGELOGs
martincostello May 19, 2026
a9846cd
Merge branch 'main' into fix-scope-metadata
martincostello May 22, 2026
92924ae
Merge branch 'main' into fix-scope-metadata
martincostello May 30, 2026
691f9ce
[Exporter.Prometheus] Fix-up merge
martincostello May 30, 2026
36bbbe9
[Exporter.Prometheus] Address feedback
martincostello May 30, 2026
98141c1
Merge branch 'main' into fix-scope-metadata
martincostello Jun 1, 2026
44fa775
[Exporter.Prometheus] Fix SA1203
martincostello Jun 1, 2026
34ac7ea
[Exporter.Prometheus] Address feedback
martincostello Jun 1, 2026
0658b03
Merge branch 'main' into fix-scope-metadata
martincostello Jun 2, 2026
191f148
[Exporter.Prometheus] Address feedback
martincostello Jun 2, 2026
cb9605c
[Exporter.Prometheus] Snapshot tests
martincostello Jun 2, 2026
a8f4d61
[Exporter.Prometheus] Add more snapshots
martincostello Jun 2, 2026
cc8c90a
[Exporter.Prometheus] Fix snapshots
martincostello Jun 3, 2026
2842649
Merge branch 'main' into prometheus-verify-tests
martincostello Jun 3, 2026
bd2f85b
Merge branch 'main' into prometheus-verify-tests
martincostello Jun 3, 2026
aba3121
[Exporter.Prometheus] Fix build
martincostello Jun 3, 2026
e5e871c
[Exporter.Prometheus] Address feedback
martincostello Jun 3, 2026
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 @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="Testcontainers" />
<PackageReference Include="Verify.Xunit" />
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -57,4 +58,8 @@
<EmbeddedResource Include="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\prometheus.Dockerfile" LogicalName="prometheus.Dockerfile" />
</ItemGroup>

<ItemGroup>
<AssemblyMetadata Include="PrometheusSerializerTestsSnapshotsPath" Value="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\snapshots" />
</ItemGroup>
Comment thread
martincostello marked this conversation as resolved.

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<ItemGroup>
<PackageReference Include="Testcontainers" />
<PackageReference Include="Verify.Xunit" />
</ItemGroup>

<ItemGroup>
Expand All @@ -51,4 +52,8 @@
<EmbeddedResource Include="*.Dockerfile" LogicalName="%(FileName)%(Extension)" />
</ItemGroup>

<ItemGroup>
<AssemblyMetadata Include="PrometheusSerializerTestsSnapshotsPath" Value="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)/snapshots'))" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
#if PROMETHEUS_HTTP_LISTENER
using OpenTelemetry.Tests;
#endif

namespace OpenTelemetry.Exporter.Prometheus.Tests;

Expand All @@ -22,7 +25,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon
using var cts = new CancellationTokenSource(testTimeout);

var cacheEnabled = scrapeResponseCacheDurationMilliseconds != 0;
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
Expand Down Expand Up @@ -203,7 +206,7 @@ async Task<Task<Response>[]> CollectInParallelAsync(bool advanceClock)
[Fact]
public async Task EnterCollectWaitsForActiveReadersToExit()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();
#if PROMETHEUS_HTTP_LISTENER
using var provider = CreateMeterProviderWithRandomPort(meter);
#elif PROMETHEUS_ASPNETCORE
Expand Down Expand Up @@ -298,6 +301,7 @@ public async Task OpenMetricsDoesNotEmitScopeInfoMetricFamily()
using var meter = new Meter("test_meter", "1.0.0", [new("library.mascot", "dotnetbot")], scope: null);

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -320,10 +324,7 @@ public async Task OpenMetricsDoesNotEmitScopeInfoMetricFamily()
response.OpenMetricsView.Offset,
response.OpenMetricsView.Count);

Assert.DoesNotContain("# TYPE otel_scope info", output, StringComparison.Ordinal);
Assert.DoesNotContain("# HELP otel_scope Scope metadata", output, StringComparison.Ordinal);
Assert.DoesNotContain("otel_scope_info{", output, StringComparison.Ordinal);
Assert.Contains("counter_1_total{otel_scope_name=\"test_meter\",otel_scope_version=\"1.0.0\",otel_scope_library_mascot=\"dotnetbot\"} 1", output, StringComparison.Ordinal);
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -337,6 +338,7 @@ public async Task OpenMetricsDoesNotReserveOtelScopeMetricFamilyNames()
using var meter = new Meter("test_meter", "1.0.0");

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -360,10 +362,7 @@ public async Task OpenMetricsDoesNotReserveOtelScopeMetricFamilyNames()
response.OpenMetricsView.Offset,
response.OpenMetricsView.Count);

Assert.Contains("# TYPE otel_scope gauge", output, StringComparison.Ordinal);
Assert.Contains("otel_scope{otel_scope_name=\"test_meter\",otel_scope_version=\"1.0.0\"} 1", output, StringComparison.Ordinal);
Assert.Contains("# TYPE otel_scope_info gauge", output, StringComparison.Ordinal);
Assert.Contains("otel_scope_info{otel_scope_name=\"test_meter\",otel_scope_version=\"1.0.0\"} 2", output, StringComparison.Ordinal);
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -374,9 +373,10 @@ public async Task OpenMetricsDoesNotReserveOtelScopeMetricFamilyNames()
[Fact]
public async Task DuplicateMetricMetadataIsWrittenOncePerScrape()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -401,13 +401,7 @@ public async Task DuplicateMetricMetadataIsWrittenOncePerScrape()
var view = response.PlainTextView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'.
Assert.Single(Regex.Matches(output, "^# TYPE test_metric_bytes_total counter$", RegexOptions.Multiline).Cast<Match>());
Assert.Single(Regex.Matches(output, "^# UNIT test_metric_bytes_total bytes$", RegexOptions.Multiline).Cast<Match>());
Assert.Single(Regex.Matches(output, "^# HELP test_metric_bytes_total Test help$", RegexOptions.Multiline).Cast<Match>());
#pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'.
Assert.Contains("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"a\"} 1", output, StringComparison.Ordinal);
Assert.Contains("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"b\"} 2", output, StringComparison.Ordinal);
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -418,9 +412,10 @@ public async Task DuplicateMetricMetadataIsWrittenOncePerScrape()
[Fact]
public async Task MetricMetadataDiscoveredLaterIsWrittenBeforeSamples()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -445,19 +440,7 @@ public async Task MetricMetadataDiscoveredLaterIsWrittenBeforeSamples()
var view = response.PlainTextView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

var typeIndex = output.IndexOf("# TYPE test_metric_total counter", StringComparison.Ordinal);
var helpIndex = output.IndexOf("# HELP test_metric_total Test help", StringComparison.Ordinal);
var sampleAIndex = output.IndexOf("test_metric_total{otel_scope_name=\"" + meter.Name + "\",source=\"a\"} 1", StringComparison.Ordinal);
var sampleBIndex = output.IndexOf("test_metric_total{otel_scope_name=\"" + meter.Name + "\",source=\"b\"} 2", StringComparison.Ordinal);

Assert.True(typeIndex >= 0, "No TYPE found.");
Assert.True(helpIndex >= 0, "No HELP found.");
Assert.True(sampleAIndex >= 0, "No sample A found.");
Assert.True(sampleBIndex >= 0, "No sample B found.");
Assert.True(typeIndex < sampleAIndex, "TYPE appears after sample A.");
Assert.True(typeIndex < sampleBIndex, "TYPE appears after sample B.");
Assert.True(helpIndex < sampleAIndex, "HELP appears after sample A.");
Assert.True(helpIndex < sampleBIndex, "HELP appears after sample B.");
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -468,9 +451,10 @@ public async Task MetricMetadataDiscoveredLaterIsWrittenBeforeSamples()
[Fact]
public async Task MetricUnitDiscoveredLaterIsWrittenBeforeSamples()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -495,19 +479,7 @@ public async Task MetricUnitDiscoveredLaterIsWrittenBeforeSamples()
var view = response.PlainTextView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

var typeIndex = output.IndexOf("# TYPE test_metric_bytes_total counter", StringComparison.Ordinal);
var unitIndex = output.IndexOf("# UNIT test_metric_bytes_total bytes", StringComparison.Ordinal);
var sampleAIndex = output.IndexOf("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"a\"} 1", StringComparison.Ordinal);
var sampleBIndex = output.IndexOf("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"b\"} 2", StringComparison.Ordinal);

Assert.True(typeIndex >= 0, "No TYPE found.");
Assert.True(unitIndex >= 0, "No UNIT found.");
Assert.True(sampleAIndex >= 0, "No sample A found.");
Assert.True(sampleBIndex >= 0, "No sample B found.");
Assert.True(typeIndex < sampleAIndex, "TYPE appears after sample A.");
Assert.True(typeIndex < sampleBIndex, "TYPE appears after sample B.");
Assert.True(unitIndex < sampleAIndex, "UNIT appears after sample A.");
Assert.True(unitIndex < sampleBIndex, "UNIT appears after sample B.");
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -518,9 +490,10 @@ public async Task MetricUnitDiscoveredLaterIsWrittenBeforeSamples()
[Fact]
public async Task MetricHelpAndUnitDiscoveredTogetherLaterAreBothWrittenBeforeSamples()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -545,23 +518,7 @@ public async Task MetricHelpAndUnitDiscoveredTogetherLaterAreBothWrittenBeforeSa
var view = response.PlainTextView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

var typeIndex = output.IndexOf("# TYPE test_metric_bytes_total counter", StringComparison.Ordinal);
var unitIndex = output.IndexOf("# UNIT test_metric_bytes_total bytes", StringComparison.Ordinal);
var helpIndex = output.IndexOf("# HELP test_metric_bytes_total Test help", StringComparison.Ordinal);
var sampleAIndex = output.IndexOf("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"a\"} 1", StringComparison.Ordinal);
var sampleBIndex = output.IndexOf("test_metric_bytes_total{otel_scope_name=\"" + meter.Name + "\",source=\"b\"} 2", StringComparison.Ordinal);

Assert.True(typeIndex >= 0, "No TYPE found.");
Assert.True(unitIndex >= 0, "No UNIT found.");
Assert.True(helpIndex >= 0, "No HELP found.");
Assert.True(sampleAIndex >= 0, "No sample A found.");
Assert.True(sampleBIndex >= 0, "No sample B found.");
Assert.True(typeIndex < sampleAIndex, "TYPE appears after sample A.");
Assert.True(typeIndex < sampleBIndex, "TYPE appears after sample B.");
Assert.True(unitIndex < sampleAIndex, "UNIT appears after sample A.");
Assert.True(unitIndex < sampleBIndex, "UNIT appears after sample B.");
Assert.True(helpIndex < sampleAIndex, "HELP appears after sample A.");
Assert.True(helpIndex < sampleBIndex, "HELP appears after sample B.");
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -572,9 +529,10 @@ public async Task MetricHelpAndUnitDiscoveredTogetherLaterAreBothWrittenBeforeSa
[Fact]
public async Task ConflictingMetricTypesAreDroppedFromAScrape()
{
using var meter = new Meter(Utils.GetCurrentMethodName());
using var meter = CreateMeter();

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter.Name)
#if PROMETHEUS_HTTP_LISTENER
.AddPrometheusHttpListener()
Expand All @@ -597,9 +555,7 @@ public async Task ConflictingMetricTypesAreDroppedFromAScrape()
var view = response.OpenMetricsView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

Assert.DoesNotContain("# TYPE test_metric", output, StringComparison.Ordinal);
Assert.DoesNotContain("test_metric_total", output, StringComparison.Ordinal);
Assert.Contains("# EOF", output, StringComparison.Ordinal);
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
Expand All @@ -610,10 +566,12 @@ public async Task ConflictingMetricTypesAreDroppedFromAScrape()
[Fact]
public async Task OpenMetricsWritesMetricFamiliesContiguously()
{
using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.one");
using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.two");
var prefix = nameof(this.OpenMetricsWritesMetricFamiliesContiguously);
using var meter1 = new Meter($"{prefix}.one");
using var meter2 = new Meter($"{prefix}.two");

using var provider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource((p) => p.AddAttributes([new("service.name", "prometheus")]))
.AddMeter(meter1.Name)
.AddMeter(meter2.Name)
#if PROMETHEUS_HTTP_LISTENER
Expand All @@ -637,33 +595,16 @@ public async Task OpenMetricsWritesMetricFamiliesContiguously()
var view = response.OpenMetricsView;
var output = Encoding.UTF8.GetString(view.Array!, view.Offset, view.Count);

var testMetricTypeIndex = output.IndexOf("# TYPE test_metric gauge", StringComparison.Ordinal);

Assert.DoesNotContain("# TYPE otel_scope info", output, StringComparison.Ordinal);
Assert.DoesNotContain("# HELP otel_scope Scope metadata", output, StringComparison.Ordinal);
Assert.DoesNotContain("otel_scope_info{", output, StringComparison.Ordinal);
Assert.True(testMetricTypeIndex >= 0, "No TYPE found for test_metric.");

var nextTypeIndex = output.IndexOf("\n# TYPE ", testMetricTypeIndex + 1, StringComparison.Ordinal);
if (nextTypeIndex < 0)
{
nextTypeIndex = output.IndexOf("\n# EOF", testMetricTypeIndex + 1, StringComparison.Ordinal);
}

Assert.True(nextTypeIndex > testMetricTypeIndex, "No subsequent metric family found after test_metric.");

var testMetricBlock = output.Substring(testMetricTypeIndex, nextTypeIndex - testMetricTypeIndex);
Assert.Contains("# HELP test_metric Test help", testMetricBlock, StringComparison.Ordinal);
Assert.Contains($"test_metric{{otel_scope_name=\"{meter1.Name}\"}} 1", testMetricBlock, StringComparison.Ordinal);
Assert.Contains($"test_metric{{otel_scope_name=\"{meter2.Name}\"}} 2", testMetricBlock, StringComparison.Ordinal);
Assert.DoesNotContain("other_metric{", testMetricBlock, StringComparison.Ordinal);
await Verify(output, "txt", PrometheusSerializerTests.VerifySettings);
}
finally
{
exporter.CollectionManager.ExitCollect();
}
}

private static Meter CreateMeter([CallerMemberName] string name = "") => new(name);

#if PROMETHEUS_HTTP_LISTENER
private static MeterProvider CreateMeterProviderWithRandomPort(Meter meter)
{
Expand Down
Loading
Loading