diff --git a/.github/workflows/ci-concurrency.yml b/.github/workflows/ci-concurrency.yml index 2734a1df94b..1a0a5bcbd0d 100644 --- a/.github/workflows/ci-concurrency.yml +++ b/.github/workflows/ci-concurrency.yml @@ -35,7 +35,7 @@ jobs: - name: Publish Artifacts if: always() && !cancelled() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }}-${{ matrix.project }}-${{ matrix.version }}-coyoteoutput path: '**/*_CoyoteOutput.*' diff --git a/.github/workflows/publish-packages-1.0.yml b/.github/workflows/publish-packages-1.0.yml index 41cad730f48..450cc398567 100644 --- a/.github/workflows/publish-packages-1.0.yml +++ b/.github/workflows/publish-packages-1.0.yml @@ -36,7 +36,7 @@ jobs: run: dotnet pack OpenTelemetry.proj --configuration Release --no-build --no-restore - name: Publish Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ github.ref_name }}-packages path: '**/bin/**/*.*nupkg' diff --git a/LICENSE b/LICENSE.TXT similarity index 100% rename from LICENSE rename to LICENSE.TXT diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 8a158c98cd6..52835a5b4ca 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -15,11 +15,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\ci-concurrency.yml = .github\workflows\ci-concurrency.yml CONTRIBUTING.md = CONTRIBUTING.md global.json = global.json - LICENSE = LICENSE NuGet.config = NuGet.config + LICENSE.TXT = LICENSE.TXT OpenTelemetry.proj = OpenTelemetry.proj README.md = README.md VERSIONING.md = VERSIONING.md + THIRD-PARTY-NOTICES.TXT = THIRD-PARTY-NOTICES.TXT EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7CB2F02E-03FA-4FFF-89A5-C51F107623FD}" diff --git a/README.md b/README.md index 6762bb2f441..6aa8ca5368d 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,12 @@ Here are the [instrumentation libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library): * [ASP.NET Core](./src/OpenTelemetry.Instrumentation.AspNetCore/README.md) -* [Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md) -* [HTTP clients](./src/OpenTelemetry.Instrumentation.Http/README.md) -* [SQL client](./src/OpenTelemetry.Instrumentation.SqlClient/README.md) +* gRPC client: + [Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md) +* HTTP clients: [System.Net.Http.HttpClient and + System.Net.HttpWebRequest](./src/OpenTelemetry.Instrumentation.Http/README.md) +* SQL clients: [Microsoft.Data.SqlClient and + System.Data.SqlClient](./src/OpenTelemetry.Instrumentation.SqlClient/README.md) Here are the [exporter libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#exporter-library): diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT new file mode 100644 index 00000000000..b65f5723308 --- /dev/null +++ b/THIRD-PARTY-NOTICES.TXT @@ -0,0 +1,31 @@ +OpenTelemetry .NET uses third-party libraries or other resources that may be +distributed under licenses different than the OpenTelemetry .NET software. + +The attached notices are provided for information only. + +License notice for .NET +------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build/Common.prod.props b/build/Common.prod.props index 555776982f0..95c9f0c63ea 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -13,13 +13,15 @@ Observability;OpenTelemetry;Monitoring;Telemetry;Tracing;Metrics;Logging opentelemetry-icon-color.png https://opentelemetry.io - Apache-2.0 OpenTelemetry Authors Copyright The OpenTelemetry Authors - true $(Build_ArtifactStagingDirectory) true snupkg + Apache-2.0 + true + $(RepoRoot)\LICENSE.TXT + $(RepoRoot)\THIRD-PARTY-NOTICES.TXT diff --git a/docs/metrics/README.md b/docs/metrics/README.md index 20bb5945b30..4499281cdd8 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -2,20 +2,23 @@ ## Best Practices -- Instruments SHOULD only be created once and reused throughout the application - lifetime. This - [example](../../docs/metrics/getting-started-console/Program.cs) shows how an - instrument is created a `static` field and then used in the application. You - could also look at this ASP.NET Core - [example](../../examples/AspNetCore/Program.cs) which shows a more Dependency - Injection friendly way of doing this by extracting the `Meter` and an - instrument into a dedicated class called - [Instrumentation](../../examples/AspNetCore/Instrumentation.cs) which is then - added as a `Singleton` service. - -- When emitting metrics with tags, DO NOT change the order in which you provide - tags. Changing the order of tag keys would increase the time taken by the SDK - to record the measurement. +### Instruments should be singleton + +Instruments SHOULD only be created once and reused throughout the application +lifetime. This [example](../../docs/metrics/getting-started-console/Program.cs) +shows how an instrument is created as a `static` field and then used in the +application. You could also look at this ASP.NET Core +[example](../../examples/AspNetCore/Program.cs) which shows a more Dependency +Injection friendly way of doing this by extracting the `Meter` and an instrument +into a dedicated class called +[Instrumentation](../../examples/AspNetCore/Instrumentation.cs) which is then +added as a `Singleton` service. + +### Ordering of Tags + +When emitting metrics with tags, DO NOT change the order in which you provide +tags. Changing the order of tag keys would increase the time taken by the SDK to +record the measurement. ```csharp // If you emit the tag keys in this order: name -> color -> taste, stick to this order of tag keys for subsequent measurements. @@ -27,6 +30,8 @@ MyFruitCounter.Add(5, new("name", "apple"), new("color", "red"), new("taste", "s MyFruitCounter.Add(7, new("color", "red"), new("name", "apple"), new("taste", "sweet")); // <--- DON'T DO THIS ``` +### Use TagList where appropriate + For the best performance, it is highly recommended to pass in tags in certain ways so allocations are only happening on the stack rather than the heap, which eliminates pressure on the GC (garbage collector): @@ -49,7 +54,6 @@ var tags = new TagList // Uses a TagList as there are more than three tags counter.Add(100, tags); // <--- DO THIS - // Avoid the below mentioned approaches when there are more than three tags var tag1 = new KeyValuePair("DimName1", "DimValue1"); var tag2 = new KeyValuePair("DimName2", "DimValue2"); @@ -63,11 +67,16 @@ counter.Add(100, readOnlySpanOfTags); // <--- DON'T DO THIS ``` - When emitting metrics with more than eight tags, the SDK allocates memory on - the hot-path. You SHOULD try to keep the number of tags less than or equal to - eight. Check if you can extract any shared tags such as `MachineName`, - `Environment` etc. into `Resource` attributes. Refer to this - [doc](../../docs/metrics/customizing-the-sdk/README.md#resource) for more - information. +the hot-path. You SHOULD try to keep the number of tags less than or equal to +eight. If you are exceeding this, check if you can model some of the tags as +Resource, as [shown here](#modeling-static-tags-as-resource). + +### Modeling static tags as Resource + +Tags such as `MachineName`, `Environment` etc. which are static throughout the +process lifetime should be be modeled as `Resource`, instead of adding them to +each metric measurement. Refer to this +[doc](./customizing-the-sdk/README.md#resource) for details and examples. ## Common issues that lead to missing metrics diff --git a/docs/metrics/customizing-the-sdk/Program.cs b/docs/metrics/customizing-the-sdk/Program.cs index 36539bd6d57..48c9a975311 100644 --- a/docs/metrics/customizing-the-sdk/Program.cs +++ b/docs/metrics/customizing-the-sdk/Program.cs @@ -16,7 +16,12 @@ public class Program public static void Main() { using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(res => res.AddService("example-service")) + .ConfigureResource(resource => resource.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resource => resource.AddService("MyServiceName")) .AddMeter(Meter1.Name) .AddMeter(Meter2.Name) diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index fb41c28cdd9..148d470f0aa 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -577,14 +577,27 @@ is to use a resource indicating this [Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) and [Telemetry SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk). -The `ConfigureResource` method on `MeterProviderBuilder` can be used to set a -configure the resource on the provider. When the provider is built, it -automatically builds the final `Resource` from the configured `ResourceBuilder`. -There can only be a single `Resource` associated with a -provider. It is not possible to change the resource builder *after* the provider -is built, by calling the `Build()` method on the `MeterProviderBuilder`. +The `ConfigureResource` method on `MeterProviderBuilder` can be used to +configure the resource on the provider. `ConfigureResource` accepts an `Action` +to configure the `ResourceBuilder`. Multiple calls to `ConfigureResource` can be +made. When the provider is built, it builds the final `Resource` combining all +the `ConfigureResource` calls. There can only be a single `Resource` associated +with a provider. It is not possible to change the resource builder *after* the +provider is built, by calling the `Build()` method on the +`MeterProviderBuilder`. + `ResourceBuilder` offers various methods to construct resource comprising of -multiple attributes from various sources. +attributes from various sources. For example, `AddService()` adds +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +resource. `AddAttributes` can be used to add any additional attributes to the +`Resource`. It also allows adding `ResourceDetector`s. + +It is recommended to model attributes that are static throughout the lifetime of +the process as Resources, instead of adding them as attributes(tags) on each +measurement. + +Follow [this](../../trace/extending-the-sdk/README.md#resource-detector) document +to learn about writing custom resource detectors. The snippet below shows configuring the `Resource` associated with the provider. @@ -594,7 +607,12 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(r => r.AddService("MyServiceName")) + .ConfigureResource(r => r.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resourceBuilder => resourceBuilder.AddService("service-name")) .Build(); ``` diff --git a/docs/trace/README.md b/docs/trace/README.md index e9925cf6565..2ef0ff07340 100644 --- a/docs/trace/README.md +++ b/docs/trace/README.md @@ -2,7 +2,7 @@ ## Best Practices -### ActivitySource singleton +### ActivitySource should be singleton `ActivitySource` SHOULD only be created once and reused throughout the application lifetime. This @@ -31,6 +31,14 @@ care of propagating/restoring the context across process boundaries. If the you need, it is generally recommended to enrich the existing Activity with that information, as opposed to creating a new one. +### Modelling static tags as Resource + +Tags such as `MachineName`, `Environment` etc. which are static throughout the +process lifetime should be be modelled as `Resource`, instead of adding them +to each `Activity`. Refer to this +[doc](./customizing-the-sdk/README.md#resource) for details and +examples. + ## Common issues that lead to missing traces - The `ActivitySource` used to create the `Activity` is not added to the diff --git a/docs/trace/customizing-the-sdk/Program.cs b/docs/trace/customizing-the-sdk/Program.cs index 5da5da584f6..fead3aa0be2 100644 --- a/docs/trace/customizing-the-sdk/Program.cs +++ b/docs/trace/customizing-the-sdk/Program.cs @@ -33,8 +33,12 @@ public static void Main() // The following adds subscription to activities from all Activity Sources // whose name starts with "AbcCompany.XyzProduct.". .AddSource("AbcCompany.XyzProduct.*") - .ConfigureResource(resourceBuilder => resourceBuilder.AddTelemetrySdk()) - .ConfigureResource(r => r.AddService("MyServiceName")) + .ConfigureResource(resource => resource.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resource => resource.AddService("MyServiceName")) .AddConsoleExporter() .Build(); diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index fd16648c820..76f4145d2e3 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -303,12 +303,14 @@ provider is built, by calling the `Build()` method on the `TracerProviderBuilder`. `ResourceBuilder` offers various methods to construct resource comprising of -multiple attributes from various sources. Examples include `AddTelemetrySdk()` -which adds [Telemetry -Sdk](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk) -resource, and `AddService()` which adds +attributes from various sources. For example, `AddService()` adds [Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) -resource. It also allows adding `ResourceDetector`s. +resource. `AddAttributes` can be used to add any additional attribute to the +`Resource`. It also allows adding `ResourceDetector`s. + +It is recommended to model attributes that are static throughout the lifetime of +the process as Resources, instead of adding them as attributes(tags) on each +`Activity`. Follow [this](../extending-the-sdk/README.md#resource-detector) document to learn about writing custom resource detectors. @@ -321,7 +323,11 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureResource(resourceBuilder => resourceBuilder.AddTelemetrySdk()) + .ConfigureResource(r => r.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) .ConfigureResource(resourceBuilder => resourceBuilder.AddService("service-name")) .Build(); ``` diff --git a/examples/AspNetCore/Instrumentation.cs b/examples/AspNetCore/Instrumentation.cs index 190a7d24a6b..4b0ede1157f 100644 --- a/examples/AspNetCore/Instrumentation.cs +++ b/examples/AspNetCore/Instrumentation.cs @@ -22,7 +22,7 @@ public Instrumentation() string? version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); this.ActivitySource = new ActivitySource(ActivitySourceName, version); this.meter = new Meter(MeterName, version); - this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", "The number of days where the temperature is below freezing"); + this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", description: "The number of days where the temperature is below freezing"); } public ActivitySource ActivitySource { get; } diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 203352a3f75..f7426c0fe37 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -19,4 +19,15 @@ + + + + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index 809994f353e..ba6c9ba7024 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased * Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) +* For requests with OpenMetrics format, scope info is automatically added + ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086) + [#5182](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5182)) ## 1.7.0-rc.1 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index 16185be88da..aa2981a5e16 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased * Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) +* For requests with OpenMetrics format, scope info is automatically added + ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086) + [#5182](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5182)) ## 1.7.0-rc.1 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index e60047739ee..b8b4f4888ae 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -14,6 +14,7 @@ internal sealed class PrometheusCollectionManager private readonly int scrapeResponseCacheDurationMilliseconds; private readonly Func, ExportResult> onCollectRef; private readonly Dictionary metricsCache; + private readonly HashSet scopes; private int metricsCacheCount; private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap) private int globalLockState; @@ -29,6 +30,7 @@ public PrometheusCollectionManager(PrometheusExporter exporter) this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds; this.onCollectRef = this.OnCollect; this.metricsCache = new Dictionary(); + this.scopes = new HashSet(); } #if NET6_0_OR_GREATER @@ -170,6 +172,40 @@ private ExportResult OnCollect(Batch metrics) try { + if (this.exporter.OpenMetricsRequested) + { + this.scopes.Clear(); + + foreach (var metric in metrics) + { + if (PrometheusSerializer.CanWriteMetric(metric)) + { + if (this.scopes.Add(metric.MeterName)) + { + try + { + cursor = PrometheusSerializer.WriteScopeInfo(this.buffer, cursor, metric.MeterName); + + break; + } + catch (IndexOutOfRangeException) + { + if (!this.IncreaseBufferSize()) + { + // there are two cases we might run into the following condition: + // 1. we have many metrics to be exported - in this case we probably want + // to put some upper limit and allow the user to configure it. + // 2. we got an IndexOutOfRangeException which was triggered by some other + // code instead of the buffer[cursor++] - in this case we should give up + // at certain point rather than allocating like crazy. + throw; + } + } + } + } + } + } + foreach (var metric in metrics) { if (!PrometheusSerializer.CanWriteMetric(metric)) @@ -194,12 +230,6 @@ private ExportResult OnCollect(Batch metrics) { if (!this.IncreaseBufferSize()) { - // there are two cases we might run into the following condition: - // 1. we have many metrics to be exported - in this case we probably want - // to put some upper limit and allow the user to configure it. - // 2. we got an IndexOutOfRangeException which was triggered by some other - // code instead of the buffer[cursor++] - in this case we should give up - // at certain point rather than allocating like crazy. throw; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 7c3fc57ba0d..69365d4e0ff 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter.Prometheus; @@ -313,6 +314,31 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric return cursor; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteScopeInfo(byte[] buffer, int cursor, string scopeName) + { + if (string.IsNullOrEmpty(scopeName)) + { + return cursor; + } + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE otel_scope_info info"); + buffer[cursor++] = ASCII_LINEFEED; + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP otel_scope_info Scope metadata"); + buffer[cursor++] = ASCII_LINEFEED; + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "otel_scope_info"); + buffer[cursor++] = unchecked((byte)'{'); + cursor = WriteLabel(buffer, cursor, "otel_scope_name", scopeName); + buffer[cursor++] = unchecked((byte)'}'); + buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = unchecked((byte)'1'); + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics) { @@ -339,6 +365,37 @@ public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool use return WriteLong(buffer, cursor, value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool writeEnclosingBraces = true) + { + if (writeEnclosingBraces) + { + buffer[cursor++] = unchecked((byte)'{'); + } + + cursor = WriteLabel(buffer, cursor, "otel_scope_name", metric.MeterName); + buffer[cursor++] = unchecked((byte)','); + + if (!string.IsNullOrEmpty(metric.MeterVersion)) + { + cursor = WriteLabel(buffer, cursor, "otel_scope_version", metric.MeterVersion); + buffer[cursor++] = unchecked((byte)','); + } + + foreach (var tag in tags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + + if (writeEnclosingBraces) + { + buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + } + + return cursor; + } + private static string MapPrometheusType(PrometheusType type) { return type switch diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index fa9885a44b2..1523ef7c160 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -32,24 +32,11 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe { foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var tags = metricPoint.Tags; var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); // Counter and Gauge cursor = WriteMetricName(buffer, cursor, prometheusMetric); - - if (tags.Count > 0) - { - buffer[cursor++] = unchecked((byte)'{'); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } - - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. - } + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); buffer[cursor++] = unchecked((byte)' '); @@ -100,12 +87,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe cursor = WriteMetricName(buffer, cursor, prometheusMetric); cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } + cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false); cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\""); @@ -131,19 +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"); - - if (tags.Count > 0) - { - buffer[cursor++] = unchecked((byte)'{'); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } - - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. - } + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); buffer[cursor++] = unchecked((byte)' '); @@ -157,19 +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"); - - if (tags.Count > 0) - { - buffer[cursor++] = unchecked((byte)'{'); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } - - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. - } + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); buffer[cursor++] = unchecked((byte)' '); diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index faa14974dfe..f760d475655 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +* Fixed an issue where `LogRecord.Attributes` (or `LogRecord.StateValues` alias) + could become out of sync with `LogRecord.State` if either is set directly via + the public setters. This was done to further mitigate issues introduced in + 1.5.0 causing attributes added using custom processor(s) to be missing after + upgrading. For details see: + [#5169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5169) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs index 621a66de30c..acdc08f4fdd 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs @@ -79,7 +79,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except LogRecordData.SetActivityContext(ref data, activity); - var attributes = record.Attributes = + var attributes = record.AttributeData = ProcessState(record, ref iloggerData, in state, this.options.IncludeAttributes, this.options.ParseStateValues); if (!TryGetOriginalFormatFromAttributes(attributes, out var originalFormat)) @@ -133,7 +133,7 @@ internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, } } - private static IReadOnlyList>? ProcessState( + internal static IReadOnlyList>? ProcessState( LogRecord logRecord, ref LogRecord.LogRecordILoggerData iLoggerData, in TState state, diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs index a2c06667614..877ab83d1dd 100644 --- a/src/OpenTelemetry/Logs/LogRecord.cs +++ b/src/OpenTelemetry/Logs/LogRecord.cs @@ -18,6 +18,7 @@ public sealed class LogRecord { internal LogRecordData Data; internal LogRecordILoggerData ILoggerData; + internal IReadOnlyList>? AttributeData; internal List>? AttributeStorage; internal List? ScopeStorage; internal int PoolReferenceCount = int.MaxValue; @@ -75,7 +76,7 @@ internal LogRecord( this.Data.Body = template; } - this.Attributes = stateValues; + this.AttributeData = stateValues; } } @@ -228,13 +229,30 @@ public string? Body /// through . /// Set to when is enabled. + /// are automatically updated if is set directly. /// /// [Obsolete("State cannot be accessed safely outside of an ILogger.Log call stack. Use Attributes instead to safely access the data attached to a LogRecord. State will be removed in a future version.")] public object? State { get => this.ILoggerData.State; - set => this.ILoggerData.State = value; + set + { + if (ReferenceEquals(this.ILoggerData.State, value)) + { + return; + } + + if (this.AttributeData is not null) + { + this.AttributeData = OpenTelemetryLogger.ProcessState(this, ref this.ILoggerData, value, includeAttributes: true, parseStateValues: false); + } + else + { + this.ILoggerData.State = value; + } + } } /// @@ -252,15 +270,37 @@ public object? State /// Gets or sets the attributes attached to the log. /// /// - /// Note: Set when is enabled and - /// log record state implements or + /// Set when is enabled and log + /// record state implements or of s /// (where TKey is string and TValue is object) or is enabled - /// otherwise . + /// otherwise . + /// is automatically updated if are set directly. + /// /// - public IReadOnlyList>? Attributes { get; set; } + public IReadOnlyList>? Attributes + { + get => this.AttributeData; + set + { + if (ReferenceEquals(this.AttributeData, value)) + { + return; + } + + if (this.ILoggerData.State is not null) + { + this.ILoggerData.State = value; + } + + this.AttributeData = value; + } + } /// /// Gets or sets the log . @@ -411,7 +451,7 @@ internal LogRecord Copy() { Data = this.Data, ILoggerData = this.ILoggerData.Copy(), - Attributes = this.Attributes == null ? null : new List>(this.Attributes), + AttributeData = this.AttributeData is null ? null : new List>(this.AttributeData), Logger = this.Logger, }; } @@ -422,7 +462,7 @@ internal LogRecord Copy() /// private void BufferLogAttributes() { - var attributes = this.Attributes; + var attributes = this.AttributeData; if (attributes == null || attributes == this.AttributeStorage) { return; @@ -437,7 +477,7 @@ private void BufferLogAttributes() // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2905. attributeStorage.AddRange(attributes); - this.Attributes = attributeStorage; + this.AttributeData = attributeStorage; } /// diff --git a/src/OpenTelemetry/Logs/LoggerSdk.cs b/src/OpenTelemetry/Logs/LoggerSdk.cs index 85f48473690..a0bc47300d0 100644 --- a/src/OpenTelemetry/Logs/LoggerSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerSdk.cs @@ -38,7 +38,7 @@ public override void EmitLog(in LogRecordData data, in LogRecordAttributeList at logRecord.Logger = this; - logRecord.Attributes = attributes.Export(ref logRecord.AttributeStorage); + logRecord.AttributeData = attributes.Export(ref logRecord.AttributeStorage); processor.OnEnd(logRecord); diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 631c4fb01da..1ca4baa1114 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -1033,7 +1033,6 @@ private void UpdateDouble(double value, ReadOnlySpan - diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 8f71478088d..7e1a6c80bae 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -19,6 +19,8 @@ namespace OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests; public sealed class PrometheusExporterMiddlewareTests { + private const string MeterVersion = "1.0.1"; + private static readonly string MeterName = Utils.GetCurrentMethodName(); [Fact] @@ -281,7 +283,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( new KeyValuePair("key2", "value2"), }; - using var meter = new Meter(MeterName); + using var meter = new Meter(MeterName, MeterVersion); var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -320,11 +322,14 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string content = await response.Content.ReadAsStringAsync(); string expected = requestOpenMetrics - ? "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n" + ? "# TYPE otel_scope_info info\n" + + "# 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" + "# EOF\n" : "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n" + + $"counter_double_total{{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 fc0f7d0fb74..2d0271d84e1 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -14,7 +14,9 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests; public class PrometheusHttpListenerTests { - private readonly string meterName = Utils.GetCurrentMethodName(); + private const string MeterVersion = "1.0.1"; + + private static readonly string MeterName = Utils.GetCurrentMethodName(); [Theory] [InlineData("http://+:9464")] @@ -99,7 +101,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri string address = null; MeterProvider provider = null; - using var meter = new Meter(this.meterName); + using var meter = new Meter(MeterName, MeterVersion); while (retryAttempts-- != 0) { @@ -110,7 +112,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri { provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) + .AddPrometheusHttpListener(options => + { + options.UriPrefixes = new string[] { address }; + }) .Build(); break; @@ -165,11 +170,14 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri var content = await response.Content.ReadAsStringAsync(); var expected = requestOpenMetrics - ? "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\\.\\d{3}\n" + ? "# TYPE otel_scope_info info\n" + + "# 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" + "# EOF\n" : "# TYPE counter_double_total counter\n" - + "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n" + + $"counter_double_total{{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 4c221e0a557..55c4412b27e 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -31,7 +31,7 @@ public void GaugeZeroDimension() Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" - + "test_gauge 123 \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -57,7 +57,7 @@ public void GaugeZeroDimensionWithDescription() ("^" + "# TYPE test_gauge gauge\n" + "# HELP test_gauge Hello, world!\n" - + "test_gauge 123 \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -83,7 +83,7 @@ public void GaugeZeroDimensionWithUnit() ("^" + "# TYPE test_gauge_seconds gauge\n" + "# UNIT test_gauge_seconds seconds\n" - + "test_gauge_seconds 123 \\d+\n" + + $"test_gauge_seconds{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -110,7 +110,7 @@ public void GaugeZeroDimensionWithDescriptionAndUnit() + "# TYPE test_gauge_seconds gauge\n" + "# UNIT test_gauge_seconds seconds\n" + "# HELP test_gauge_seconds Hello, world!\n" - + "test_gauge_seconds 123 \\d+\n" + + $"test_gauge_seconds{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -137,7 +137,7 @@ public void GaugeOneDimension() Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" - + "test_gauge{tagKey='tagValue'} 123 \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',tagKey='tagValue'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -164,7 +164,7 @@ public void GaugeBoolDimension() Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" - + "test_gauge{tagKey='true'} 123 \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',tagKey='true'}} 123 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -194,9 +194,9 @@ public void GaugeDoubleSubnormal() Assert.Matches( ("^" + "# TYPE test_gauge gauge\n" - + "test_gauge{x='1',y='2'} -Inf \\d+\n" - + "test_gauge{x='3',y='4'} \\+Inf \\d+\n" - + "test_gauge{x='5',y='6'} Nan \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} -Inf \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='3',y='4'}} \\+Inf \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='5',y='6'}} Nan \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -223,7 +223,7 @@ public void SumDoubleInfinities() Assert.Matches( ("^" + "# TYPE test_counter_total counter\n" - + "test_counter_total \\+Inf \\d+\n" + + $"test_counter_total{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} \\+Inf \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -250,7 +250,7 @@ public void SumNonMonotonicDouble() Assert.Matches( ("^" + "# TYPE test_updown_counter gauge\n" - + "test_updown_counter -1 \\d+\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -277,24 +277,24 @@ public void HistogramZeroDimension() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 2 \\d+\n" - + "test_histogram_bucket{le='250'} 2 \\d+\n" - + "test_histogram_bucket{le='500'} 2 \\d+\n" - + "test_histogram_bucket{le='750'} 2 \\d+\n" - + "test_histogram_bucket{le='1000'} 2 \\d+\n" - + "test_histogram_bucket{le='2500'} 2 \\d+\n" - + "test_histogram_bucket{le='5000'} 2 \\d+\n" - + "test_histogram_bucket{le='7500'} 2 \\d+\n" - + "test_histogram_bucket{le='10000'} 2 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum 118 \\d+\n" - + "test_histogram_count 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 2 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -321,24 +321,24 @@ public void HistogramOneDimension() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{x='1',le='0'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='5'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='10'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='25'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='50'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='75'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='100'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='250'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='750'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='1000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='2500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='5000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='7500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='10000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum{x='1'} 118 \\d+\n" - + "test_histogram_count{x='1'} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 2 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -365,24 +365,24 @@ public void HistogramTwoDimensions() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{x='1',y='2',le='0'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='5'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='10'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='25'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='50'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='75'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='100'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='250'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='750'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='1000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='2500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='5000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='7500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='10000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum{x='1',y='2'} 118 \\d+\n" - + "test_histogram_count{x='1',y='2'} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} 2 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -410,24 +410,24 @@ public void HistogramInfinities() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 1 \\d+\n" - + "test_histogram_bucket{le='250'} 1 \\d+\n" - + "test_histogram_bucket{le='500'} 1 \\d+\n" - + "test_histogram_bucket{le='750'} 1 \\d+\n" - + "test_histogram_bucket{le='1000'} 1 \\d+\n" - + "test_histogram_bucket{le='2500'} 1 \\d+\n" - + "test_histogram_bucket{le='5000'} 1 \\d+\n" - + "test_histogram_bucket{le='7500'} 1 \\d+\n" - + "test_histogram_bucket{le='10000'} 1 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" - + "test_histogram_sum \\+Inf \\d+\n" - + "test_histogram_count 3 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 3 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} \\+Inf \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 3 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -455,24 +455,24 @@ public void HistogramNaN() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 1 \\d+\n" - + "test_histogram_bucket{le='250'} 1 \\d+\n" - + "test_histogram_bucket{le='500'} 1 \\d+\n" - + "test_histogram_bucket{le='750'} 1 \\d+\n" - + "test_histogram_bucket{le='1000'} 1 \\d+\n" - + "test_histogram_bucket{le='2500'} 1 \\d+\n" - + "test_histogram_bucket{le='5000'} 1 \\d+\n" - + "test_histogram_bucket{le='7500'} 1 \\d+\n" - + "test_histogram_bucket{le='10000'} 1 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" - + "test_histogram_sum Nan \\d+\n" - + "test_histogram_count 3 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 3 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} Nan \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 3 \\d+\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } @@ -521,13 +521,13 @@ public void SumWithOpenMetricsFormat() Assert.Matches( ("^" + "# TYPE test_updown_counter gauge\n" - + "test_updown_counter -1 \\d+\\.\\d{3}\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\\.\\d{{3}}\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } [Fact] - public void HistogramOneDimensionWithScopeInfo() + public void HistogramOneDimensionWithOpenMetricsFormat() { var buffer = new byte[85000]; var metrics = new List(); @@ -548,24 +548,107 @@ public void HistogramOneDimensionWithScopeInfo() Assert.Matches( ("^" + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{x='1',le='0'} 0 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='5'} 0 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='10'} 0 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='25'} 1 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='50'} 1 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='75'} 1 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='100'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='250'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='500'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='750'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='1000'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='2500'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='5000'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='7500'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='10000'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_bucket{x='1',le='\\+Inf'} 2 \\d+\\.\\d{3}\n" - + "test_histogram_sum{x='1'} 118 \\d+\\.\\d{3}\n" - + "test_histogram_count{x='1'} 2 \\d+\\.\\d{3}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='0'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='25'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='50'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='75'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='100'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='250'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='750'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='1000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='2500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='7500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='\\+Inf'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 118 \\d+\\.\\d{{3}}\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 2 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void ScopeInfo() + { + var buffer = new byte[85000]; + + var cursor = PrometheusSerializer.WriteScopeInfo(buffer, 0, "test_meter"); + + Assert.Matches( + ("^" + + "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + "otel_scope_info{otel_scope_name='test_meter'} 1\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumWithScopeVersion() + { + var buffer = new byte[85000]; + var metrics = new List(); + using var meter = new Meter(Utils.GetCurrentMethodName(), "1.0.0"); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + var counter = meter.CreateUpDownCounter("test_updown_counter"); + counter.Add(10); + counter.Add(-11); + provider.ForceFlush(); + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0'}} -1 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimensionWithScopeVersion() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName(), "1.0.0"); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='0'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='25'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='50'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='75'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='100'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='250'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='750'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='1000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='2500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='7500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='\\+Inf'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 118 \\d+\\.\\d{{3}}\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 2 \\d+\\.\\d{{3}}\n" + "$").Replace('\'', '"'), Encoding.UTF8.GetString(buffer, 0, cursor)); } diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs new file mode 100644 index 00000000000..24acd8869a7 --- /dev/null +++ b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs @@ -0,0 +1,266 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace OpenTelemetry.Logs.Tests; + +public class LogRecordStateProcessorTests +{ + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetStateTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 0 : parseStateValues ? 1 : 3, + stateExpectedCount: !includeAttributes || parseStateValues ? 1 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.State = new List>(state) + { + new("enrichedData", "OTel"), + }; + } + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetStateToUnsupportedTypeTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: 0, + stateExpectedCount: 0, + out var state, + out var attributes); + + Assert.True(exportedItems[0].State is CustomState); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.State = new CustomState("OTel"); + } + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetAttributesTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 1 : 3, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.Attributes = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + } + } + + [Theory] + [InlineData(true, false, 0)] + [InlineData(false, true, 0)] + [InlineData(true, true, 0)] + [InlineData(false, false, 0)] + [InlineData(true, false, 1)] + [InlineData(false, true, 1)] + [InlineData(true, true, 1)] + [InlineData(false, false, 1)] + public void LogProcessorSetAttributesAndStateMixedTest(bool includeAttributes, bool parseStateValues, int order) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 1 : 3, + stateExpectedCount: !includeAttributes ? 1 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + if (order == 0) + { + logRecord.State = logRecord.Attributes = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + } + else + { + var newState = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + + logRecord.State = newState; + logRecord.Attributes = newState; + } + } + } + + private static ILoggerFactory CreateLoggerFactory( + bool includeAttributes, + bool parseStateValues, + List exportedItems, + Action onEndAction) + { + return LoggerFactory.Create(logging => logging + .AddOpenTelemetry(options => + { + options.IncludeAttributes = includeAttributes; + options.ParseStateValues = parseStateValues; + + options + .AddProcessor(new LogRecordStateProcessor(onEndAction)) + .AddInMemoryExporter(exportedItems); + })); + } + + private static void AssertStateAndAttributes( + LogRecord logRecord, + int attributesExpectedCount, + int stateExpectedCount, + [NotNull] out IReadOnlyList>? state, + [NotNull] out IReadOnlyList>? attributes) + { + state = logRecord.State as IReadOnlyList>; + attributes = logRecord.Attributes; + + if (stateExpectedCount > 0) + { + Assert.NotNull(state); + Assert.Equal(stateExpectedCount, state.Count); + } + else + { + Assert.Null(state); + state = Array.Empty>(); + } + + if (attributesExpectedCount > 0) + { + Assert.NotNull(attributes); + Assert.Equal(attributesExpectedCount, attributes.Count); + } + else + { + Assert.Null(attributes); + attributes = Array.Empty>(); + } + } + + private sealed class LogRecordStateProcessor : BaseProcessor + { + private readonly Action onEndAction; + + public LogRecordStateProcessor(Action onEndAction) + { + this.onEndAction = onEndAction; + } + + public override void OnEnd(LogRecord data) + { + this.onEndAction(data); + + base.OnEnd(data); + } + } + + private sealed class CustomState(string enrichedData) + { + public string EnrichedData { get; } = enrichedData; + } +}