Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -4,6 +4,8 @@ OpenTelemetry.Exporter.PrometheusAspNetCoreOptions
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ResourceConstantLabels.get -> System.Func<string!, bool>?
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ResourceConstantLabels.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScopeInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.get -> string?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Notes](../../RELEASENOTES.md).
disable the `target_info` metric in Prometheus metrics. Defaults to `true`.
([#7438](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7438))

* Add `PrometheusAspNetCoreOptions.ResourceConstantLabels` property to select
resource attributes to add to each metric as constant labels. Defaults to
`null` (no resource attributes are added as metric labels).
([#7471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7471))

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,20 @@ public bool TargetInfoEnabled
set => this.ExporterOptions.TargetInfoEnabled = value;
}

/// <summary>
/// Gets or sets a predicate used to select which resource attributes are added to each metric as constant labels.
/// The predicate is invoked with the resource attribute key and should return <see langword="true"/> to include the
/// attribute. Default value: <see langword="null"/> (no resource attributes are added as metric labels).
/// </summary>
/// <remarks>
/// Note: Resource attributes copied as metric labels are always included in the <c>target_info</c> metric
/// regardless of this predicate.
/// </remarks>
public Func<string, bool>? ResourceConstantLabels
{
get => this.ExporterOptions.ResourceConstantLabels;
set => this.ExporterOptions.ResourceConstantLabels = value;
}

internal PrometheusExporterOptions ExporterOptions { get; } = new();
}
20 changes: 20 additions & 0 deletions src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ Specifies whether to produce a
[`target_info`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#resource-attributes-1)
metric. Default value: `true`. Set to `false` to disable the `target_info` metric.

### ResourceConstantLabels

A predicate used to select which
[resource attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md#resource-attributes-as-metric-labels)
are added to each metric as constant labels. The predicate is invoked with the
resource attribute key and should return `true` to include the attribute.
Default value: `null` (no resource attributes are added as metric labels).
Resource attributes copied as metric labels are always included in the
`target_info` metric regardless of this predicate.

```csharp
services.AddOpenTelemetry()
.WithMetrics(builder => builder
.AddPrometheusExporter(options =>
{
// Add all resource attributes as metric labels.
options.ResourceConstantLabels = static _ => true;
}));
```

## Troubleshooting

This component uses an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCo
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ResourceConstantLabels.get -> System.Func<string!, bool>?
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ResourceConstantLabels.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ Notes](../../RELEASENOTES.md).
disable the `target_info` metric in Prometheus metrics. Defaults to `true`.
([#7438](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7438))

* Add `PrometheusHttpListenerOptions.ResourceConstantLabels` property to select
resource attributes to add to each metric as constant labels. Defaults to
`null` (no resource attributes are added as metric labels).
([#7471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7471))

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ internal sealed class PrometheusCollectionManager
private readonly PrometheusExporter exporter;
private readonly bool scopeInfoEnabled;
private readonly bool targetInfoEnabled;
private readonly Func<string, bool>? resourceConstantLabelsFilter;
private readonly TimeSpan scrapeResponseCacheDuration;
private readonly long baseTimestamp = Stopwatch.GetTimestamp();
private readonly PrometheusExporter.ExportFunc onCollectRef;
private readonly Dictionary<Metric, PrometheusMetric> metricsCache;
private int metricsCacheCount;
private IReadOnlyList<KeyValuePair<string, object>>? resourceConstantLabels;
private bool resourceConstantLabelsComputed;
private int globalLockState;
private CollectionContext? collectionContext;
private CollectionContext? onCollectContext;
Expand All @@ -33,6 +36,7 @@ public PrometheusCollectionManager(PrometheusExporter exporter)
this.exporter = exporter;
this.scopeInfoEnabled = this.exporter.ScopeInfoEnabled;
this.targetInfoEnabled = this.exporter.TargetInfoEnabled;
this.resourceConstantLabelsFilter = this.exporter.ResourceConstantLabels;
this.scrapeResponseCacheDuration = TimeSpan.FromMilliseconds(this.exporter.ScrapeResponseCacheDurationMilliseconds);
this.onCollectRef = this.OnCollect;
this.metricsCache = [];
Expand Down Expand Up @@ -357,7 +361,9 @@ private bool TryWriteResponse(in PrometheusProtocol protocol, PrometheusProtocol

var cursor = this.targetInfoEnabled ? this.WriteTargetInfo(serializer, state) : 0;
var metricStates = this.GetMetricStates(serializer, metrics);
var options = new TextFormatSerializerOptions(suppressScopeInfo: !this.scopeInfoEnabled);
var options = new TextFormatSerializerOptions(
suppressScopeInfo: !this.scopeInfoEnabled,
resourceConstantLabels: this.GetResourceConstantLabels());

foreach (var metricState in metricStates)
{
Expand Down Expand Up @@ -477,6 +483,32 @@ state.GeneratedAtElapsed is { } generatedAtElapsed &&
private PrometheusProtocolState GetProtocolState(in PrometheusProtocol protocol)
=> this.protocolStates.GetOrAdd(protocol, static _ => new());

private IReadOnlyList<KeyValuePair<string, object>>? GetResourceConstantLabels()
{
if (!this.resourceConstantLabelsComputed)
{
if (this.resourceConstantLabelsFilter is { } filter)
{
List<KeyValuePair<string, object>>? labels = null;

foreach (var attribute in this.exporter.Resource.Attributes)
{
if (filter(attribute.Key))
{
labels ??= [];
labels.Add(attribute);
}
}

this.resourceConstantLabels = labels;
}

this.resourceConstantLabelsComputed = true;
}

return this.resourceConstantLabels;
}

private int WriteTargetInfo(TextFormatSerializer serializer, PrometheusProtocolState state)
{
while (true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public PrometheusExporter(PrometheusExporterOptions options)
this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds;
this.TargetInfoEnabled = options.TargetInfoEnabled;
this.DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters;
this.ResourceConstantLabels = options.ResourceConstantLabels;

this.CollectionManager = new PrometheusCollectionManager(this);
}
Expand All @@ -52,6 +53,8 @@ public PrometheusExporter(PrometheusExporterOptions options)

internal bool DisableTotalNameSuffixForCounters { get; }

internal Func<string, bool>? ResourceConstantLabels { get; }

internal Resource Resource
{
get => field ??= this.ParentProvider.GetResource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,11 @@ public int ScrapeResponseCacheDurationMilliseconds
/// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: <see langword="false"/>.
/// </summary>
public bool DisableTotalNameSuffixForCounters { get; set; }

/// <summary>
/// Gets or sets a predicate used to select which resource attributes are added to each metric as constant labels.
/// The predicate is invoked with the resource attribute key and should return <see langword="true"/> to include the
/// attribute. Default value: <see langword="null"/> (no resource attributes are added as metric labels).
/// </summary>
public Func<string, bool>? ResourceConstantLabels { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -642,28 +642,37 @@ internal int WriteTags(
List<string>? writtenOutputKeys = null;
var wroteLabel = false;

var resourceConstantLabels = options.ResourceConstantLabels;
var hasResourceConstantLabels = resourceConstantLabels is { Count: > 0 };

if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'{');
}

if (!options.SuppressScopeInfo)
{
WriteScopeLabels();
}

if (TryWritePointTags())
// The fast path writes scope labels and point tags directly to the buffer. It cannot
// account for resource constant labels (which may collide with, and therefore need to be
// merged with, point tags), so it is skipped whenever any are present.
if (!hasResourceConstantLabels)
{
if (writeEnclosingBraces)
if (!options.SuppressScopeInfo)
{
buffer[cursor++] = unchecked((byte)'}');
WriteScopeLabels();
}
else if (wroteLabel)

if (TryWritePointTags())
{
buffer[cursor++] = unchecked((byte)',');
}
if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'}');
}
else if (wroteLabel)
{
buffer[cursor++] = unchecked((byte)',');
}

return cursor;
return cursor;
}
}

cursor = startCursor;
Expand All @@ -682,6 +691,14 @@ internal int WriteTags(
this.AddLabel(tag.Key, tag.Value, ref labels, reservedOutputKeys);
}

if (hasResourceConstantLabels)
{
foreach (var resourceLabel in resourceConstantLabels!)
{
this.AddLabel(resourceLabel.Key, resourceLabel.Value, ref labels, reservedOutputKeys);
}
}

return WriteLabels(buffer, cursor, labels, writeEnclosingBraces);

void WriteScopeLabels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ internal readonly struct TextFormatSerializerOptions
/// </summary>
public readonly bool SuppressScopeInfo; // Inverted so the default struct is the default value (false = included).

/// <summary>
/// The resource attributes to add to each metric as constant labels, or <see langword="null"/> if none.
/// </summary>
public readonly IReadOnlyList<KeyValuePair<string, object>>? ResourceConstantLabels;

/// <summary>
/// Initializes a new instance of the <see cref="TextFormatSerializerOptions"/> struct.
/// </summary>
/// <param name="suppressScopeInfo">Whether the scope information is suppressed from the scrape response.</param>
public TextFormatSerializerOptions(bool suppressScopeInfo)
/// <param name="resourceConstantLabels">The resource attributes to add to each metric as constant labels, or <see langword="null"/> if none.</param>
public TextFormatSerializerOptions(
bool suppressScopeInfo,
IReadOnlyList<KeyValuePair<string, object>>? resourceConstantLabels)
{
this.SuppressScopeInfo = suppressScopeInfo;
this.ResourceConstantLabels = resourceConstantLabels;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ private static BaseExportingMetricReader BuildPrometheusHttpListenerMetricReader
ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds,
TargetInfoEnabled = options.TargetInfoEnabled,
DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters,
ResourceConstantLabels = options.ResourceConstantLabels,
});

var reader = new BaseExportingMetricReader(exporter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,16 @@ public int ScrapeResponseCacheDurationMilliseconds
/// </summary>
public bool TargetInfoEnabled { get; set; } = true;

/// <summary>
/// Gets or sets a predicate used to select which resource attributes are added to each metric as constant labels.
/// The predicate is invoked with the resource attribute key and should return <see langword="true"/> to include the
/// attribute. Default value: <see langword="null"/> (no resource attributes are added as metric labels).
/// </summary>
/// <remarks>
/// Note: Resource attributes copied as metric labels are always included in the <c>target_info</c> metric
/// regardless of this predicate.
/// </remarks>
public Func<string, bool>? ResourceConstantLabels { get; set; }

internal Action<PrometheusHttpListenerOptions, System.Net.HttpListener>? ConfigureHttpListener { get; set; }
}
23 changes: 23 additions & 0 deletions src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ variables.
* `ScrapeEndpointPath`: Defines the Prometheus scrape endpoint path.
(default `"/metrics"`).
* `TargetInfoEnabled`: Whether to produce a `target_info` metric (default `true`).
* `ResourceConstantLabels`: A predicate selecting which resource attributes are
added to each metric as constant labels (default `null`).
* `DisableTotalNameSuffixForCounters`: Whether to disable the `_total` suffix for
counter metrics (default `false`).
* `DisableTimestamp`: Whether to disable the timestamp for metrics (default `false`).
Expand Down Expand Up @@ -115,6 +117,27 @@ Specifies whether to produce a
[`target_info`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#resource-attributes-1)
metric. Default value: `true`. Set to `false` to disable the `target_info` metric.

### ResourceConstantLabels

A predicate used to select which
[resource attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md#resource-attributes-as-metric-labels)
are added to each metric as constant labels. The predicate is invoked with the
resource attribute key and should return `true` to include the attribute.
Default value: `null` (no resource attributes are added as metric labels).
Resource attributes copied as metric labels are always included in the
`target_info` metric regardless of this predicate.

```csharp
var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(MyMeter.Name)
.AddPrometheusHttpListener(options =>
{
// Add all resource attributes as metric labels.
options.ResourceConstantLabels = static _ => true;
})
.Build();
```

## Troubleshooting

This component uses an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ public async Task RunWithTargetInfoEnabledConfigured(bool targetInfoEnabled)
await Verify(output, "text", PrometheusSerializerTests.VerifySettings).UseParameters(targetInfoEnabled);
}

[Theory]
[InlineData("all")]
[InlineData("service_name")]
[InlineData("none")]
public async Task RunWithResourceConstantLabelsConfigured(string filter)
{
var output = await RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
services => services.Configure<PrometheusAspNetCoreOptions>(o => o.ResourceConstantLabels = filter switch
{
"all" => static _ => true,
"service_name" => static key => key == "service.name",
_ => static _ => false,
}),
assertResponseContent: false);

await Verify(output, "text", PrometheusSerializerTests.VerifySettings).UseParameters(filter);
}

[Fact]
public async Task RunWithCustomScrapeEndpointPath()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ public async Task RunHttpServerWithTargetInfoEnabledConfigured(bool targetInfoEn
await Verify(output, "text", PrometheusSerializerTests.VerifySettings).UseParameters(targetInfoEnabled);
}

[Theory]
[InlineData("all")]
[InlineData("service_name")]
[InlineData("none")]
public async Task RunHttpServerWithResourceConstantLabelsConfigured(string filter)
{
var output = await RunPrometheusExporterHttpServerIntegrationTest(
configureListener: (options) =>
{
options.ResourceConstantLabels = filter switch
{
"all" => static _ => true,
"service_name" => static key => key == "service.name",
_ => static _ => false,
};
return options.Port;
},
assertResponseContent: false);

await Verify(output, "text", PrometheusSerializerTests.VerifySettings).UseParameters(filter);
}

[Fact]
public async Task RunHttpServerWithNoMetrics()
{
Expand Down
Loading