diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 99d1e9e4939..2890f7cf95d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.AllowedResourceAttributesFilter.get -> System.Predicate +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.AllowedResourceAttributesFilter.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.get -> bool OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index 8168dd9e043..b2c737f0964 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Add resource attributes as tags for Prometheus exporters with filter + ([#5489](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5489)) + ## 1.8.0-rc.1 Released 2024-Mar-27 diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj index a94c7fdb1ae..870dbd258c3 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -24,6 +24,7 @@ + diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index 75acb565175..a650bf79fce 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs @@ -38,5 +38,14 @@ public int ScrapeResponseCacheDurationMilliseconds set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value; } + /// + /// Gets or sets the allowed resource attributes filter. Default value: null (no attributes allowed). + /// + public Predicate AllowedResourceAttributesFilter + { + get => this.ExporterOptions.AllowedResourceAttributesFilter; + set => this.ExporterOptions.AllowedResourceAttributesFilter = value; + } + internal PrometheusExporterOptions ExporterOptions { get; } = new(); } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index d05f12424ea..555fa8f37d1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ OpenTelemetry.Exporter.PrometheusHttpListenerOptions +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.AllowedResourceAttributesFilter.get -> System.Predicate +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 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index ba522fc60a4..e842c148b5c 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Add resource attributes as tags for Prometheus exporters with filter + ([#5489](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5489)) + ## 1.8.0-rc.1 Released 2024-Mar-27 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index 27ab845164b..411e411ccc9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -173,6 +173,8 @@ private ExportResult OnCollect(Batch metrics) try { + var resourceTags = new PrometheusResourceTagCollection(this.exporter.Resource, this.exporter.AllowedResourceAttributesFilter); + if (this.exporter.OpenMetricsRequested) { cursor = this.WriteTargetInfo(); @@ -230,6 +232,7 @@ private ExportResult OnCollect(Batch metrics) cursor, metric, this.GetPrometheusMetric(metric), + resourceTags, this.exporter.OpenMetricsRequested); break; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index 292b0aa7c31..35ddb46d2a0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -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); } @@ -59,6 +60,8 @@ internal Func, ExportResult> OnExport internal Resource Resource => this.resource ??= this.ParentProvider.GetResource(); + internal Predicate AllowedResourceAttributesFilter { get; set; } + /// public override ExportResult Export(in Batch metrics) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs index ec14b88b182..e29b07cc64e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs @@ -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: . /// public bool DisableTotalNameSuffixForCounters { get; set; } + + /// + /// Gets or sets the allowed resource attributes filter. Default value: null (no attributes allowed). + /// + public Predicate AllowedResourceAttributesFilter { get; set; } = null; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusResourceTagCollection.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusResourceTagCollection.cs new file mode 100644 index 00000000000..64ddafb32b9 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusResourceTagCollection.cs @@ -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 resourceAttributeFilter; + + public PrometheusResourceTagCollection(Resource resource, Predicate resourceAttributeFilter = null) + { + this.resource = resource; + this.resourceAttributeFilter = resourceAttributeFilter; + } + + public IEnumerable> Attributes + { + get + { + if (this.resource == null || this.resourceAttributeFilter == null) + { + return Enumerable.Empty>(); + } + + var attributeFilter = this.resourceAttributeFilter; + + return this.resource?.Attributes + .Where(attribute => attributeFilter(attribute.Key)); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 719f21a0c61..2914d941be1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -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)','); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 1523ef7c160..eeb31329667 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -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); @@ -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)' '); @@ -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=\""); @@ -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)' '); @@ -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)' '); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index 929774a11f9..bdb146d4fc2 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs @@ -69,6 +69,7 @@ private static MetricReader BuildPrometheusHttpListenerMetricReader( { ScrapeResponseCacheDurationMilliseconds = 0, DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters, + AllowedResourceAttributesFilter = options.AllowedResourceAttributesFilter, }); var reader = new BaseExportingMetricReader(exporter) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index d0c6bd2edf0..3a7103fbb80 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -41,4 +41,9 @@ public IReadOnlyCollection UriPrefixes this.uriPrefixes = value; } } + + /// + /// Gets or sets the allowed resource attributes filter. Default value: null (no attributes allowed). + /// + public Predicate AllowedResourceAttributesFilter { get; set; } = null; } diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 46da01e6417..a869937e5a7 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -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 configure, @@ -256,7 +266,8 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( bool registerMeterProvider = true, Action 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"); @@ -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" @@ -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('\'', '"')); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 3a4ca1c4153..cc177429e47 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -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() { @@ -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"); @@ -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(); @@ -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" @@ -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); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 7c4a95b05f4..f276e0bf799 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics.Metrics; using System.Text; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Tests; using Xunit; @@ -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(); + + 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("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); } }