Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include resource attributes as tags for Prometheus exporters #5489

Closed
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,6 +1,8 @@
Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions
Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.AllowedResourceAttributesFilter.get -> System.Predicate<string>
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.AllowedResourceAttributesFilter.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Add resource attributes as tags for Prometheus exporters with filter
([#3087](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5407))
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved

## 1.8.0-rc.1

Released 2024-Mar-27
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusExporter.cs" Link="Includes/PrometheusExporter.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusExporterEventSource.cs" Link="Includes/PrometheusExporterEventSource.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusExporterOptions.cs" Link="Includes/PrometheusExporterOptions.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusResourceTagCollection.cs" Link="Includes/PrometheusResourceTagCollection.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusSerializer.cs" Link="Includes/PrometheusSerializer.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusSerializerExt.cs" Link="Includes/PrometheusSerializerExt.cs" />
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusType.cs" Link="Includes/PrometheusType.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,14 @@ public int ScrapeResponseCacheDurationMilliseconds
set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value;
}

/// <summary>
/// Gets or sets the allowed resource attributes filter. Default value: null (no attributes allowed).
/// </summary>
public Predicate<string> AllowedResourceAttributesFilter
Copy link
Member

Choose a reason for hiding this comment

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

I understand this implementation was inspired from Java's. It appears both Java and Go have implemented this as a predicate. That said, the spec is not opinionated in how this is implemented:

The configuration SHOULD allow the user to select which resource attributes to copy (e.g. include / exclude or regular expression based).

I think I'd prefer a simple include list. Something like:

/// <summary>
/// Gets or sets the resource attributes to include on each metric point. By default no resource attributes are included.
/// </summary>
public string[] IncludedResourceAttributes

{
get => this.ExporterOptions.AllowedResourceAttributesFilter;
set => this.ExporterOptions.AllowedResourceAttributesFilter = value;
}

internal PrometheusExporterOptions ExporterOptions { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
OpenTelemetry.Exporter.PrometheusHttpListenerOptions
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.AllowedResourceAttributesFilter.get -> System.Predicate<string>
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.AllowedResourceAttributesFilter.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.get -> System.Collections.Generic.IReadOnlyCollection<string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Add resource attributes as tags for Prometheus exporters with filter
([#3087](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5407))

## 1.8.0-rc.1

Released 2024-Mar-27
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ private ExportResult OnCollect(Batch<Metric> metrics)

try
{
var resourceTags = new PrometheusResourceTagCollection(this.exporter.Resource, this.exporter.AllowedResourceAttributesFilter);

if (this.exporter.OpenMetricsRequested)
{
cursor = this.WriteTargetInfo();
Expand Down Expand Up @@ -230,6 +232,7 @@ private ExportResult OnCollect(Batch<Metric> metrics)
cursor,
metric,
this.GetPrometheusMetric(metric),
resourceTags,
this.exporter.OpenMetricsRequested);

break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public PrometheusExporter(PrometheusExporterOptions options)

this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds;
this.DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters;
this.AllowedResourceAttributesFilter = options.AllowedResourceAttributesFilter;

this.CollectionManager = new PrometheusCollectionManager(this);
}
Expand Down Expand Up @@ -59,6 +60,8 @@ internal Func<Batch<Metric>, ExportResult> OnExport

internal Resource Resource => this.resource ??= this.ParentProvider.GetResource();

internal Predicate<string> AllowedResourceAttributesFilter { get; set; }

/// <inheritdoc/>
public override ExportResult Export(in Batch<Metric> metrics)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ 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 the allowed resource attributes filter. Default value: null (no attributes allowed).
/// </summary>
public Predicate<string> AllowedResourceAttributesFilter { get; set; } = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using OpenTelemetry.Resources;

namespace OpenTelemetry.Exporter.Prometheus;

internal readonly struct PrometheusResourceTagCollection
{
private readonly Resource resource;
private readonly Predicate<string> resourceAttributeFilter;

public PrometheusResourceTagCollection(Resource resource, Predicate<string> resourceAttributeFilter = null)
{
this.resource = resource;
this.resourceAttributeFilter = resourceAttributeFilter;
}

public IEnumerable<KeyValuePair<string, object>> Attributes
{
get
{
if (this.resource == null || this.resourceAttributeFilter == null)
{
return Enumerable.Empty<KeyValuePair<string, object>>();
}

var attributeFilter = this.resourceAttributeFilter;

return this.resource?.Attributes
.Where(attribute => attributeFilter(attribute.Key));
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

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

If we did keep this as a predicate, we could avoid evaluating the predicate on every single export. The configured resource is immutable. It does not change once the exporter is instantiated.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,19 @@ public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool use
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool writeEnclosingBraces = true)
public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, PrometheusResourceTagCollection resourceTags = default, bool writeEnclosingBraces = true)
{
if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'{');
}

foreach (var resourceAttribute in resourceTags.Attributes)
{
cursor = WriteLabel(buffer, cursor, resourceAttribute.Key, resourceAttribute.Value);
buffer[cursor++] = unchecked((byte)',');
}

cursor = WriteLabel(buffer, cursor, "otel_scope_name", metric.MeterName);
buffer[cursor++] = unchecked((byte)',');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static bool CanWriteMetric(Metric metric)
return true;
}

public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false)
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, PrometheusResourceTagCollection resourceTags = default, bool openMetricsRequested = false)
{
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric);
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric);
Expand All @@ -36,7 +36,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

// Counter and Gauge
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, resourceTags);

buffer[cursor++] = unchecked((byte)' ');

Expand Down Expand Up @@ -87,7 +87,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{");
cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false);
cursor = WriteTags(buffer, cursor, metric, tags, resourceTags, writeEnclosingBraces: false);

cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\"");

Expand All @@ -113,7 +113,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
// Histogram sum
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, resourceTags);

buffer[cursor++] = unchecked((byte)' ');

Expand All @@ -127,7 +127,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
// Histogram count
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count");
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags, resourceTags);

buffer[cursor++] = unchecked((byte)' ');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private static MetricReader BuildPrometheusHttpListenerMetricReader(
{
ScrapeResponseCacheDurationMilliseconds = 0,
DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters,
AllowedResourceAttributesFilter = options.AllowedResourceAttributesFilter,
});

var reader = new BaseExportingMetricReader(exporter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ public IReadOnlyCollection<string> UriPrefixes
this.uriPrefixes = value;
}
}

/// <summary>
/// Gets or sets the allowed resource attributes filter. Default value: null (no attributes allowed).
/// </summary>
public Predicate<string> AllowedResourceAttributesFilter { get; set; } = null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,16 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader(
acceptHeader: "application/openmetrics-text; version=1.0.0");
}

[Fact]
public Task PrometheusExporterMiddlewareIntegration_AddResourceAttributesAsTags()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
configureOptions: o => o.AllowedResourceAttributesFilter = s => s == "service.name",
addServiceNameResourceTag: true);
}

private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
string path,
Action<IApplicationBuilder> configure,
Expand All @@ -256,7 +266,8 @@ 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",
bool addServiceNameResourceTag = false)
{
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");

Expand Down Expand Up @@ -325,6 +336,10 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(

string content = await response.Content.ReadAsStringAsync();

var resourceTagAttributes = addServiceNameResourceTag
? "service_name='my_service',"
: string.Empty;

string expected = requestOpenMetrics
? "# TYPE target info\n"
+ "# HELP target Target metadata\n"
Expand All @@ -333,10 +348,10 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ $"counter_double_total{{{resourceTagAttributes}otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# EOF\n"
: "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ $"counter_double_total{{{resourceTagAttributes}otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ "# EOF\n";

var matches = Regex.Matches(content, ("^" + expected + "$").Replace('\'', '"'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionH
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0");
}

[Fact]
public async Task PrometheusExporterHttpServerIntegration_AddResourceAttributeAsTag()
{
await this.RunPrometheusExporterHttpServerIntegrationTest(addServiceNameResourceTag: true);
}

[Fact]
public void PrometheusHttpListenerThrowsOnStart()
{
Expand Down Expand Up @@ -155,7 +161,7 @@ private static void TestPrometheusHttpListenerUriPrefixOptions(string[] uriPrefi
});
}

private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text")
private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", bool addServiceNameResourceTag = false)
{
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");

Expand All @@ -180,6 +186,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
.AddPrometheusHttpListener(options =>
{
options.UriPrefixes = new string[] { address };

if (addServiceNameResourceTag)
{
options.AllowedResourceAttributesFilter = s => s == "service.name";
}
})
.Build();

Expand Down Expand Up @@ -234,6 +245,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri

var content = await response.Content.ReadAsStringAsync();

var resourceTagAttributes = addServiceNameResourceTag
? "service_name='my_service',"
: string.Empty;

var expected = requestOpenMetrics
? "# TYPE target info\n"
+ "# HELP target Target metadata\n"
Expand All @@ -242,10 +257,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ $"counter_double_total{{{resourceTagAttributes}otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# EOF\n"
: "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ $"counter_double_total{{{resourceTagAttributes}otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ "# EOF\n";

Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics.Metrics;
using System.Text;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
using Xunit;

Expand Down Expand Up @@ -653,8 +654,38 @@ public void HistogramOneDimensionWithScopeVersion()
Encoding.UTF8.GetString(buffer, 0, cursor));
}

private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false)
[Fact]
public void SumWithResourceAttributes()
{
var buffer = new byte[85000];
var metrics = new List<Metric>();

var resource = ResourceBuilder.CreateEmpty().AddService("my_service");

using var meter = new Meter(Utils.GetCurrentMethodName());
using var provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddInMemoryExporter(metrics)
.SetResourceBuilder(resource)
.Build();

var counter = meter.CreateUpDownCounter<double>("test_updown_counter");
counter.Add(10);
counter.Add(-11);

provider.ForceFlush();

var cursor = WriteMetric(buffer, 0, metrics[0], true, new PrometheusResourceTagCollection(resource.Build(), s => s == "service.name"));
Assert.Matches(
("^"
+ "# TYPE test_updown_counter gauge\n"
+ $"test_updown_counter{{service_name='my_service',otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\\.\\d{{3}}\n"
+ "$").Replace('\'', '"'),
Encoding.UTF8.GetString(buffer, 0, cursor));
}

private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false, PrometheusResourceTagCollection resourceTags = default)
{
return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric, false), useOpenMetrics);
return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric, false), resourceTags, useOpenMetrics);
}
}