From 88b6b0e5524d684f6e6e11f4f68c5e05e41c2aec Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Thu, 22 Feb 2024 14:47:39 -0800 Subject: [PATCH 01/16] Improve OTLP Exporter doc - provide recommendation on custom AuthN (#5380) --- .../README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 290e9daa6b5..8dd42301e23 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -350,6 +350,14 @@ services.AddOpenTelemetry() })); ``` +> [!NOTE] +> `DefaultRequestHeaders` can be used for [HTTP Basic Access +Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). For +more complex authentication requirements, +[`System.Net.Http.DelegatingHandler`](https://learn.microsoft.com/dotnet/api/system.net.http.delegatinghandler) +can be used to handle token refresh, as explained +[here](https://stackoverflow.com/questions/56204350/how-to-refresh-a-token-using-ihttpclientfactory). + For users using [IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) you may also customize the named "OtlpTraceExporter" and/or "OtlpMetricExporter" @@ -362,8 +370,9 @@ services.AddHttpClient( client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); ``` -Note: The single instance returned by `HttpClientFactory` is reused by all -export requests. +> [!NOTE] +> The single instance returned by `HttpClientFactory` is reused by all export +requests. ## Troubleshooting From f5b7f9c9eb684c151aa2786818540c71f68d3988 Mon Sep 17 00:00:00 2001 From: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:03:21 -0800 Subject: [PATCH 02/16] [ci] Add push trigger and workflow_dispatch trigger (#5383) --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c032ce0ed07..ceabe9ed850 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,11 @@ name: Build on: + workflow_dispatch: + # The push trigger would run the CI workflow when any changes get merged to main branch. + # The build badge on the main README uses the CI results on the main branch to report the build status. + push: + branches: [ 'main*' ] pull_request: branches: [ 'main*' ] From c87134ddc6bc65328a30c48171799d338ad81313 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 23 Feb 2024 09:18:48 -0800 Subject: [PATCH 03/16] [ci] Add checkout for push and workflow_dispatch trigger (#5384) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceabe9ed850..dc3494d93cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: outputs: changes: ${{ steps.changes.outputs.changes }} steps: + - uses: actions/checkout@v4 - uses: AurorNZ/paths-filter@v4 id: changes with: From 7e0213ddbda52626a351742b49039c961bf06e47 Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Fri, 23 Feb 2024 12:36:31 -0800 Subject: [PATCH 04/16] Otlp Retry Part2 - Introduce transmission handler (#5367) --- .../ExportClient/BaseOtlpHttpExportClient.cs | 3 + ...penTelemetryProtocolExporterEventSource.cs | 21 ++- .../OtlpExporterTransmissionHandler.cs | 120 ++++++++++++++++++ .../OtlpExporterOptionsExtensions.cs | 10 ++ .../OtlpLogExporter.cs | 23 ++-- .../OtlpMetricExporter.cs | 25 ++-- .../OtlpTraceExporter.cs | 37 ++---- .../Exporter/OtlpGrpcExporterBenchmarks.cs | 4 +- .../Exporter/OtlpHttpExporterBenchmarks.cs | 4 +- .../OtlpLogExporterTests.cs | 10 +- .../OtlpTraceExporterTests.cs | 5 +- 11 files changed, 195 insertions(+), 67 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs index 56f0118aa87..4aad820b1e2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs @@ -38,6 +38,9 @@ protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpC /// public ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default) { + // `HttpClient.Timeout.TotalMilliseconds` would be populated with the correct timeout value for both the exporter configuration cases: + // 1. User provides their own HttpClient. This case is straightforward as the user wants to use their `HttpClient` and thereby the same client's timeout value. + // 2. If the user configures timeout via the exporter options, then the timeout set for the `HttpClient` initialized by the exporter will be set to user provided value. DateTime deadline = DateTime.UtcNow.AddMilliseconds(this.HttpClient.Timeout.TotalMilliseconds); try { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 31ed2a749f4..97fe4dcb80b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -1,9 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -#endif using System.Diagnostics.Tracing; using OpenTelemetry.Internal; @@ -33,6 +30,15 @@ public void ExportMethodException(Exception ex, bool isRetry = false) } } + [NonEvent] + public void TrySubmitRequestException(Exception ex) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.TrySubmitRequestException(ex.ToInvariantString()); + } + } + [Event(2, Message = "Exporter failed send data to collector to {0} endpoint. Data will not be sent. Exception: {1}", Level = EventLevel.Error)] public void FailedToReachCollector(string rawCollectorUri, string ex) { @@ -45,9 +51,6 @@ public void CouldNotTranslateActivity(string className, string methodName) this.WriteEvent(3, className, methodName); } -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] -#endif [Event(4, Message = "Unknown error in export method. Message: '{0}'. IsRetry: {1}", Level = EventLevel.Error)] public void ExportMethodException(string ex, bool isRetry) { @@ -83,4 +86,10 @@ public void InvalidEnvironmentVariable(string key, string value) { this.WriteEvent(11, key, value); } + + [Event(12, Message = "Unknown error in TrySubmitRequest method. Message: '{0}'", Level = EventLevel.Error)] + public void TrySubmitRequestException(string ex) + { + this.WriteEvent(12, ex); + } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs new file mode 100644 index 00000000000..71f4d5ebac1 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; + +internal class OtlpExporterTransmissionHandler +{ + public OtlpExporterTransmissionHandler(IExportClient exportClient) + { + Guard.ThrowIfNull(exportClient); + + this.ExportClient = exportClient; + } + + protected IExportClient ExportClient { get; } + + /// + /// Attempts to send an export request to the server. + /// + /// The request to send to the server. + /// if the request is sent successfully; otherwise, . + /// + public bool TrySubmitRequest(TRequest request) + { + try + { + var response = this.ExportClient.SendExportRequest(request); + if (response.Success) + { + return true; + } + + return this.OnSubmitRequestFailure(request, response); + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.TrySubmitRequestException(ex); + return false; + } + } + + /// + /// Attempts to shutdown the transmission handler, blocks the current thread + /// until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns if shutdown succeeded; otherwise, . + /// + public bool Shutdown(int timeoutMilliseconds) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + + var sw = timeoutMilliseconds == Timeout.Infinite ? null : Stopwatch.StartNew(); + + this.OnShutdown(timeoutMilliseconds); + + if (sw != null) + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + return this.ExportClient.Shutdown((int)Math.Max(timeout, 0)); + } + + return this.ExportClient.Shutdown(timeoutMilliseconds); + } + + /// + /// Fired when the transmission handler is shutdown. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + protected virtual void OnShutdown(int timeoutMilliseconds) + { + } + + /// + /// Fired when a request could not be submitted. + /// + /// The request that was attempted to send to the server. + /// . + /// If the request is resubmitted and succeeds; otherwise, . + protected virtual bool OnSubmitRequestFailure(TRequest request, ExportClientResponse response) + { + return false; + } + + /// + /// Fired when resending a request to the server. + /// + /// The request to be resent to the server. + /// . + /// If the retry succeeds; otherwise, . + protected bool TryRetryRequest(TRequest request, out ExportClientResponse response) + { + response = this.ExportClient.SendExportRequest(request); + if (!response.Success) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(response.Exception, isRetry: true); + return false; + } + + return true; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index 91a23749804..44133af1f84 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -10,6 +10,7 @@ #if NETSTANDARD2_1 || NET6_0_OR_GREATER using Grpc.Net.Client; #endif +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using LogOtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; using MetricsOtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; using TraceOtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; @@ -87,6 +88,15 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac return headers; } + public static OtlpExporterTransmissionHandler GetTraceExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetTraceExportClient(options)); + + public static OtlpExporterTransmissionHandler GetMetricsExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetMetricsExportClient(options)); + + public static OtlpExporterTransmissionHandler GetLogsExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetLogExportClient(options)); + public static IExportClient GetTraceExportClient(this OtlpExporterOptions options) => options.Protocol switch { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs index 7ee6bf74f3c..8e5c626d917 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; @@ -19,7 +19,7 @@ namespace OpenTelemetry.Exporter; /// public sealed class OtlpLogExporter : BaseExporter { - private readonly IExportClient exportClient; + private readonly OtlpExporterTransmissionHandler transmissionHandler; private readonly OtlpLogRecordTransformer otlpLogRecordTransformer; private OtlpResource.Resource? processResource; @@ -29,7 +29,7 @@ public sealed class OtlpLogExporter : BaseExporter /// /// Configuration options for the exporter. public OtlpLogExporter(OtlpExporterOptions options) - : this(options, sdkLimitOptions: new(), experimentalOptions: new(), exportClient: null) + : this(options, sdkLimitOptions: new(), experimentalOptions: new(), transmissionHandler: null) { } @@ -39,12 +39,12 @@ public OtlpLogExporter(OtlpExporterOptions options) /// Configuration options for the exporter. /// . /// . - /// Client used for sending export request. + /// . internal OtlpLogExporter( OtlpExporterOptions exporterOptions, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, - IExportClient? exportClient = null) + OtlpExporterTransmissionHandler? transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); @@ -62,14 +62,7 @@ internal OtlpLogExporter( OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); }; - if (exportClient != null) - { - this.exportClient = exportClient; - } - else - { - this.exportClient = exporterOptions!.GetLogExportClient(); - } + this.transmissionHandler = transmissionHandler ?? exporterOptions.GetLogsExportTransmissionHandler(); this.otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions!, experimentalOptions!); } @@ -89,7 +82,7 @@ public override ExportResult Export(in Batch logRecordBatch) { request = this.otlpLogRecordTransformer.BuildExportRequest(this.ProcessResource, logRecordBatch); - if (!this.exportClient.SendExportRequest(request).Success) + if (!this.transmissionHandler.TrySubmitRequest(request)) { return ExportResult.Failure; } @@ -113,6 +106,6 @@ public override ExportResult Export(in Batch logRecordBatch) /// protected override bool OnShutdown(int timeoutMilliseconds) { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + return this.transmissionHandler?.Shutdown(timeoutMilliseconds) ?? true; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index ecc97994166..a0026d1e9f4 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; @@ -16,7 +16,7 @@ namespace OpenTelemetry.Exporter; /// public class OtlpMetricExporter : BaseExporter { - private readonly IExportClient exportClient; + private readonly OtlpExporterTransmissionHandler transmissionHandler; private OtlpResource.Resource processResource; @@ -25,7 +25,7 @@ public class OtlpMetricExporter : BaseExporter /// /// Configuration options for the exporter. public OtlpMetricExporter(OtlpExporterOptions options) - : this(options, null) + : this(options, transmissionHandler: null) { } @@ -33,8 +33,10 @@ public OtlpMetricExporter(OtlpExporterOptions options) /// Initializes a new instance of the class. /// /// Configuration options for the export. - /// Client used for sending export request. - internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient exportClient = null) + /// . + internal OtlpMetricExporter( + OtlpExporterOptions options, + OtlpExporterTransmissionHandler transmissionHandler = null) { // Each of the Otlp exporters: Traces, Metrics, and Logs set the same value for `OtlpKeyValueTransformer.LogUnsupportedAttributeType` // and `ConfigurationExtensions.LogInvalidEnvironmentVariable` so it should be fine even if these exporters are used together. @@ -48,14 +50,7 @@ internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); @@ -72,7 +67,7 @@ public override ExportResult Export(in Batch metrics) { request.AddMetrics(this.ProcessResource, metrics); - if (!this.exportClient.SendExportRequest(request).Success) + if (!this.transmissionHandler.TrySubmitRequest(request)) { return ExportResult.Failure; } @@ -93,6 +88,6 @@ public override ExportResult Export(in Batch metrics) /// protected override bool OnShutdown(int timeoutMilliseconds) { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + return this.transmissionHandler.Shutdown(timeoutMilliseconds); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 5febb7f4d01..f017d075428 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; @@ -17,7 +17,7 @@ namespace OpenTelemetry.Exporter; public class OtlpTraceExporter : BaseExporter { private readonly SdkLimitOptions sdkLimitOptions; - private readonly IExportClient exportClient; + private readonly OtlpExporterTransmissionHandler transmissionHandler; private OtlpResource.Resource processResource; @@ -26,7 +26,7 @@ public class OtlpTraceExporter : BaseExporter /// /// Configuration options for the export. public OtlpTraceExporter(OtlpExporterOptions options) - : this(options, new(), null) + : this(options, sdkLimitOptions: new(), transmissionHandler: null) { } @@ -35,35 +35,22 @@ public OtlpTraceExporter(OtlpExporterOptions options) /// /// . /// . - /// Client used for sending export request. + /// . internal OtlpTraceExporter( - OtlpExporterOptions exporterOptions, - SdkLimitOptions sdkLimitOptions, - IExportClient exportClient = null) + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + OtlpExporterTransmissionHandler transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); this.sdkLimitOptions = sdkLimitOptions; - OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; + OtlpKeyValueTransformer.LogUnsupportedAttributeType = OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType; - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; + ConfigurationExtensions.LogInvalidEnvironmentVariable = OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable; - if (exportClient != null) - { - this.exportClient = exportClient; - } - else - { - this.exportClient = exporterOptions.GetTraceExportClient(); - } + this.transmissionHandler = transmissionHandler ?? exporterOptions.GetTraceExportTransmissionHandler(); } internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); @@ -80,7 +67,7 @@ public override ExportResult Export(in Batch activityBatch) { request.AddBatch(this.sdkLimitOptions, this.ProcessResource, activityBatch); - if (!this.exportClient.SendExportRequest(request).Success) + if (!this.transmissionHandler.TrySubmitRequest(request)) { return ExportResult.Failure; } @@ -101,6 +88,6 @@ public override ExportResult Export(in Batch activityBatch) /// protected override bool OnShutdown(int timeoutMilliseconds) { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + return this.transmissionHandler.Shutdown(timeoutMilliseconds); } } diff --git a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index b8cffdbdb71..f80d59d2a14 100644 --- a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs @@ -11,6 +11,8 @@ using OpenTelemetryProtocol::OpenTelemetry.Exporter; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; +using OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; namespace Benchmarks.Exporter; @@ -33,7 +35,7 @@ public void GlobalSetup() this.exporter = new OtlpTraceExporter( options, new SdkLimitOptions(), - new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient())); + new OtlpExporterTransmissionHandler(new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient()))); this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); diff --git a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs index d4560579a92..86e79812be0 100644 --- a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs @@ -12,6 +12,8 @@ using OpenTelemetryProtocol::OpenTelemetry.Exporter; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; +using OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; namespace Benchmarks.Exporter; @@ -61,7 +63,7 @@ public void GlobalSetup() this.exporter = new OtlpTraceExporter( options, new SdkLimitOptions(), - new OtlpHttpTraceExportClient(options, options.HttpClientFactory())); + new OtlpExporterTransmissionHandler(new OtlpHttpTraceExportClient(options, options.HttpClientFactory()))); this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 7181557ae5a..a01d992040b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Resources; @@ -731,13 +732,14 @@ public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() { // Arrange. var testExportClient = new TestExportClient(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( new OtlpExporterOptions(), new SdkLimitOptions(), new ExperimentalOptions(), - testExportClient); + transmissionHandler); // Act. sut.Export(emptyBatch); @@ -751,13 +753,14 @@ public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() { // Arrange. var testExportClient = new TestExportClient(throwException: true); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( new OtlpExporterOptions(), new SdkLimitOptions(), new ExperimentalOptions(), - testExportClient); + transmissionHandler); // Act. var result = sut.Export(emptyBatch); @@ -771,13 +774,14 @@ public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() { // Arrange. var testExportClient = new TestExportClient(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( new OtlpExporterOptions(), new SdkLimitOptions(), new ExperimentalOptions(), - testExportClient); + transmissionHandler); // Act. var result = sut.Export(emptyBatch); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 95f61d41297..0c7a5db76e2 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -5,6 +5,7 @@ using Google.Protobuf.Collections; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Tests; @@ -629,7 +630,9 @@ public void Shutdown_ClientShutdownIsCalled() { var exportClientMock = new TestExportClient(); - var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, exportClientMock); + var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock); + + var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, transmissionHandler); exporter.Shutdown(); From 73b6e30c1baf9b8d2300026149463ddc118844fc Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 23 Feb 2024 15:50:19 -0800 Subject: [PATCH 05/16] [tools] Stress test improvements (#5381) --- src/OpenTelemetry/AssemblyInfo.cs | 1 + .../OpenTelemetry.Tests.Stress.Logs.csproj | 10 +- .../Program.cs | 58 +++-- .../OpenTelemetry.Tests.Stress.Metrics.csproj | 12 +- .../Program.cs | 161 ++++++++++---- .../OpenTelemetry.Tests.Stress.Traces.csproj | 12 +- .../Program.cs | 42 ++-- test/OpenTelemetry.Tests.Stress/Meat.cs | 19 -- .../OpenTelemetry.Tests.Stress.csproj | 2 + test/OpenTelemetry.Tests.Stress/Program.cs | 24 ++ test/OpenTelemetry.Tests.Stress/README.md | 6 +- test/OpenTelemetry.Tests.Stress/Skeleton.cs | 172 -------------- test/OpenTelemetry.Tests.Stress/StressTest.cs | 209 ++++++++++++++++++ .../StressTestFactory.cs | 34 +++ .../StressTestNativeMethods.cs | 28 +++ .../StressTestOptions.cs | 18 ++ 16 files changed, 508 insertions(+), 300 deletions(-) delete mode 100644 test/OpenTelemetry.Tests.Stress/Meat.cs create mode 100644 test/OpenTelemetry.Tests.Stress/Program.cs delete mode 100644 test/OpenTelemetry.Tests.Stress/Skeleton.cs create mode 100644 test/OpenTelemetry.Tests.Stress/StressTest.cs create mode 100644 test/OpenTelemetry.Tests.Stress/StressTestFactory.cs create mode 100644 test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs create mode 100644 test/OpenTelemetry.Tests.Stress/StressTestOptions.cs diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs index 62254638d8a..90823131448 100644 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ b/src/OpenTelemetry/AssemblyInfo.cs @@ -16,6 +16,7 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Console" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Tests.Stress.Metrics" + AssemblyInfo.PublicKey)] #endif #if SIGNED diff --git a/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj b/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj index 1f1225d551a..e75a64bbcbf 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj +++ b/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj @@ -3,21 +3,13 @@ Exe $(TargetFrameworksForTests) - - disable - + - - - - - - diff --git a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs index 6d2cb88fad0..dececdacb51 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs @@ -1,39 +1,55 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private static ILogger logger; - private static Payload payload = new Payload(); + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } - public static void Main() + private sealed class LogsStressTest : StressTest { - using var loggerFactory = LoggerFactory.Create(builder => + private static readonly Payload Payload = new(); + private readonly ILoggerFactory loggerFactory; + private readonly ILogger logger; + + public LogsStressTest(StressTestOptions options) + : base(options) { - builder.AddOpenTelemetry(options => + this.loggerFactory = LoggerFactory.Create(builder => { - options.AddProcessor(new DummyProcessor()); + builder.AddOpenTelemetry(options => + { + options.AddProcessor(new DummyProcessor()); + }); }); - }); - logger = loggerFactory.CreateLogger(); + this.logger = this.loggerFactory.CreateLogger(); + } - Stress(prometheusPort: 9464); - } + protected override void RunWorkItemInParallel() + { + this.logger.Log( + logLevel: LogLevel.Information, + eventId: 2, + state: Payload, + exception: null, + formatter: (state, ex) => string.Empty); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() - { - logger.Log( - logLevel: LogLevel.Information, - eventId: 2, - state: payload, - exception: null, - formatter: (state, ex) => string.Empty); + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.loggerFactory.Dispose(); + } + + base.Dispose(isDisposing); + } } } diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj index a783ed18d71..d162e31f732 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj +++ b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj @@ -3,23 +3,15 @@ Exe $(TargetFrameworksForTests) - - disable - - - - - - - + + - diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs index 102ad6d1df8..f43e4d12fe8 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs @@ -2,65 +2,140 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using CommandLine; using OpenTelemetry.Metrics; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private const int ArraySize = 10; - - // Note: Uncomment the below line if you want to run Histogram stress test - private const int MaxHistogramMeasurement = 1000; + private enum MetricsStressTestType + { + /// Histogram. + Histogram, - private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName()); - private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter"); - private static readonly string[] DimensionValues = new string[ArraySize]; - private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + /// Counter. + Counter, + } - // Note: Uncomment the below line if you want to run Histogram stress test - private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram"); + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } - public static void Main() + private sealed class MetricsStressTest : StressTest { - for (int i = 0; i < ArraySize; i++) + private const int ArraySize = 10; + private const int MaxHistogramMeasurement = 1000; + + private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName()); + private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram"); + private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter"); + private static readonly string[] DimensionValues = new string[ArraySize]; + private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + private readonly MeterProvider meterProvider; + + static MetricsStressTest() { - DimensionValues[i] = $"DimValue{i}"; + for (int i = 0; i < ArraySize; i++) + { + DimensionValues[i] = $"DimValue{i}"; + } } - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(TestMeter.Name) + public MetricsStressTest(MetricsStressTestOptions options) + : base(options) + { + var builder = Sdk.CreateMeterProviderBuilder().AddMeter(TestMeter.Name); + + if (options.PrometheusTestMetricsPort != 0) + { + builder.AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusTestMetricsPort}/" }); + } + + if (options.EnableExemplars) + { + builder.SetExemplarFilter(new AlwaysOnExemplarFilter()); + } + + if (options.AddViewToFilterTags) + { + builder + .AddView("TestCounter", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }) + .AddView("TestHistogram", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }); + } - // .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddPrometheusHttpListener( - options => options.UriPrefixes = new string[] { $"http://localhost:9185/" }) - .Build(); + if (options.AddOtlpExporter) + { + builder.AddOtlpExporter((exporterOptions, readerOptions) => + { + readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.OtlpExporterExportIntervalMilliseconds; + }); + } - Stress(prometheusPort: 9464); + this.meterProvider = builder.Build(); + } + + protected override void WriteRunInformationToConsole() + { + if (this.Options.PrometheusTestMetricsPort != 0) + { + Console.Write($", testPrometheusEndpoint = http://localhost:{this.Options.PrometheusTestMetricsPort}/metrics/"); + } + } + + protected override void RunWorkItemInParallel() + { + var random = ThreadLocalRandom.Value!; + if (this.Options.TestType == MetricsStressTestType.Histogram) + { + TestHistogram.Record( + random.Next(MaxHistogramMeasurement), + new("DimName1", DimensionValues[random.Next(0, ArraySize)]), + new("DimName2", DimensionValues[random.Next(0, ArraySize)]), + new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + } + else if (this.Options.TestType == MetricsStressTestType.Counter) + { + TestCounter.Add( + 100, + new("DimName1", DimensionValues[random.Next(0, ArraySize)]), + new("DimName2", DimensionValues[random.Next(0, ArraySize)]), + new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + } + } + + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.meterProvider.Dispose(); + } + + base.Dispose(isDisposing); + } } - // Note: Uncomment the below lines if you want to run Counter stress test - // [MethodImpl(MethodImplOptions.AggressiveInlining)] - // protected static void Run() - // { - // var random = ThreadLocalRandom.Value; - // TestCounter.Add( - // 100, - // new("DimName1", DimensionValues[random.Next(0, ArraySize)]), - // new("DimName2", DimensionValues[random.Next(0, ArraySize)]), - // new("DimName3", DimensionValues[random.Next(0, ArraySize)])); - // } - - // Note: Uncomment the below lines if you want to run Histogram stress test - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class MetricsStressTestOptions : StressTestOptions { - var random = ThreadLocalRandom.Value; - TestHistogram.Record( - random.Next(MaxHistogramMeasurement), - new("DimName1", DimensionValues[random.Next(0, ArraySize)]), - new("DimName2", DimensionValues[random.Next(0, ArraySize)]), - new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + [JsonConverter(typeof(JsonStringEnumConverter))] + [Option('t', "type", HelpText = "The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram.", Required = false)] + public MetricsStressTestType TestType { get; set; } = MetricsStressTestType.Histogram; + + [Option('m', "metrics_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving test metrics while the stress test is running. Set to '0' to disable. Default value: 9185.", Required = false)] + public int PrometheusTestMetricsPort { get; set; } = 9185; + + [Option('v', "view", HelpText = "Whether or not a view should be configured to filter tags for the stress test. Default value: False.", Required = false)] + public bool AddViewToFilterTags { get; set; } + + [Option('o', "otlp", HelpText = "Whether or not an OTLP exporter should be added for the stress test. Default value: False.", Required = false)] + public bool AddOtlpExporter { get; set; } + + [Option('i', "interval", HelpText = "The OTLP exporter export interval in milliseconds. Default value: 5000.", Required = false)] + public int OtlpExporterExportIntervalMilliseconds { get; set; } = 5000; + + [Option('e', "exemplars", HelpText = "Whether or not to enable exemplars for the stress test. Default value: False.", Required = false)] + public bool EnableExemplars { get; set; } } } diff --git a/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj b/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj index 41f6d28bc55..7a32563d8b5 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj +++ b/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj @@ -6,17 +6,7 @@ - - - - - - - - - - - + diff --git a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs index 743da46b638..422a44a99ef 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs @@ -2,31 +2,45 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Runtime.CompilerServices; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private static readonly ActivitySource ActivitySource = new ActivitySource("OpenTelemetry.Tests.Stress"); - - public static void Main() + public static int Main(string[] args) { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(ActivitySource.Name) - .Build(); - - Stress(prometheusPort: 9464); + return StressTestFactory.RunSynchronously(args); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class TracesStressTest : StressTest { - using (var activity = ActivitySource.StartActivity("test")) + private static readonly ActivitySource ActivitySource = new("OpenTelemetry.Tests.Stress"); + private readonly TracerProvider tracerProvider; + + public TracesStressTest(StressTestOptions options) + : base(options) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySource.Name) + .Build(); + } + + protected override void RunWorkItemInParallel() { + using var activity = ActivitySource.StartActivity("test"); + activity?.SetTag("foo", "value"); } + + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.tracerProvider.Dispose(); + } + + base.Dispose(isDisposing); + } } } diff --git a/test/OpenTelemetry.Tests.Stress/Meat.cs b/test/OpenTelemetry.Tests.Stress/Meat.cs deleted file mode 100644 index 65e66535349..00000000000 --- a/test/OpenTelemetry.Tests.Stress/Meat.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -namespace OpenTelemetry.Tests.Stress; - -public partial class Program -{ - public static void Main() - { - Stress(concurrency: 1, prometheusPort: 9464); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() - { - } -} diff --git a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj index 60e3c917910..01af1c993ae 100644 --- a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj +++ b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj @@ -5,8 +5,10 @@ + + diff --git a/test/OpenTelemetry.Tests.Stress/Program.cs b/test/OpenTelemetry.Tests.Stress/Program.cs new file mode 100644 index 00000000000..a5f6fb8975e --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/Program.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Tests.Stress; + +public static class Program +{ + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } + + private sealed class DemoStressTest : StressTest + { + public DemoStressTest(StressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + } + } +} diff --git a/test/OpenTelemetry.Tests.Stress/README.md b/test/OpenTelemetry.Tests.Stress/README.md index 890b1d0cc9b..1f953b1def1 100644 --- a/test/OpenTelemetry.Tests.Stress/README.md +++ b/test/OpenTelemetry.Tests.Stress/README.md @@ -73,6 +73,10 @@ process_runtime_dotnet_gc_allocations_size_bytes 5485192 1658950184752 ## Writing your own stress test +> [!WARNING] +> These instructions are out of date and should NOT be followed. They will be + updated soon. + Create a simple console application with the following code: ```csharp @@ -93,7 +97,7 @@ public partial class Program } ``` -Add the [`Skeleton.cs`](./Skeleton.cs) file to your `*.csproj` file: +Add the Skeleton.cs file to your `*.csproj` file: ```xml diff --git a/test/OpenTelemetry.Tests.Stress/Skeleton.cs b/test/OpenTelemetry.Tests.Stress/Skeleton.cs deleted file mode 100644 index cd3e5af7a8a..00000000000 --- a/test/OpenTelemetry.Tests.Stress/Skeleton.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Runtime.InteropServices; -using OpenTelemetry.Metrics; - -namespace OpenTelemetry.Tests.Stress; - -public partial class Program -{ - private static volatile bool bContinue = true; - private static volatile string output = "Test results not available yet."; - - static Program() - { - } - - public static void Stress(int concurrency = 0, int prometheusPort = 0) - { -#if DEBUG - Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!"); - Console.WriteLine(); -#endif - - if (concurrency < 0) - { - throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency level should be a non-negative number."); - } - - if (concurrency == 0) - { - concurrency = Environment.ProcessorCount; - } - - using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D")); - var cntLoopsTotal = 0UL; - meter.CreateObservableCounter( - "OpenTelemetry.Tests.Stress.Loops", - () => unchecked((long)cntLoopsTotal), - description: "The total number of `Run()` invocations that are completed."); - var dLoopsPerSecond = 0D; - meter.CreateObservableGauge( - "OpenTelemetry.Tests.Stress.LoopsPerSecond", - () => dLoopsPerSecond, - description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds."); - var dCpuCyclesPerLoop = 0D; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - meter.CreateObservableGauge( - "OpenTelemetry.Tests.Stress.CpuCyclesPerLoop", - () => dCpuCyclesPerLoop, - description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds."); - } - - using var meterProvider = prometheusPort != 0 ? Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddRuntimeInstrumentation() - .AddPrometheusHttpListener( - options => options.UriPrefixes = new string[] { $"http://localhost:{prometheusPort}/" }) - .Build() : null; - - var statistics = new long[concurrency]; - var watchForTotal = Stopwatch.StartNew(); - - Parallel.Invoke( - () => - { - Console.Write($"Running (concurrency = {concurrency}"); - - if (prometheusPort != 0) - { - Console.Write($", prometheusEndpoint = http://localhost:{prometheusPort}/metrics/"); - } - - Console.WriteLine("), press to stop..."); - - var bOutput = false; - var watch = new Stopwatch(); - while (true) - { - if (Console.KeyAvailable) - { - var key = Console.ReadKey(true).Key; - - switch (key) - { - case ConsoleKey.Enter: - Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); - break; - case ConsoleKey.Escape: - bContinue = false; - return; - case ConsoleKey.Spacebar: - bOutput = !bOutput; - break; - } - - continue; - } - - if (bOutput) - { - Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); - } - - var cntLoopsOld = (ulong)statistics.Sum(); - var cntCpuCyclesOld = GetCpuCycles(); - - watch.Restart(); - Thread.Sleep(200); - watch.Stop(); - - cntLoopsTotal = (ulong)statistics.Sum(); - var cntCpuCyclesNew = GetCpuCycles(); - - var nLoops = cntLoopsTotal - cntLoopsOld; - var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld; - - dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0); - dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops; - - output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunwayTime (Seconds): {watchForTotal.Elapsed.TotalSeconds:n0} "; - Console.Title = output; - } - }, - () => - { - Parallel.For(0, concurrency, (i) => - { - statistics[i] = 0; - while (bContinue) - { - Run(); - statistics[i]++; - } - }); - }); - - watchForTotal.Stop(); - cntLoopsTotal = (ulong)statistics.Sum(); - var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); - var cntCpuCyclesTotal = GetCpuCycles(); - var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; - Console.WriteLine("Stopping the stress test..."); - Console.WriteLine($"* Total Runaway Time (seconds) {watchForTotal.Elapsed.TotalSeconds:n0}"); - Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}"); - Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}"); - Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}"); - } - - [DllImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles); - - private static ulong GetCpuCycles() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return 0; - } - - if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles)) - { - return 0; - } - - return cycles; - } -} diff --git a/test/OpenTelemetry.Tests.Stress/StressTest.cs b/test/OpenTelemetry.Tests.Stress/StressTest.cs new file mode 100644 index 00000000000..ae19c7f8ece --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTest.cs @@ -0,0 +1,209 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; +using System.Text.Json; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Tests.Stress; + +public abstract class StressTest : IDisposable + where T : StressTestOptions +{ + private volatile bool bContinue = true; + private volatile string output = "Test results not available yet."; + + protected StressTest(T options) + { + this.Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public T Options { get; } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void RunSynchronously() + { +#if DEBUG + Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!"); + Console.WriteLine(); +#endif + + var options = this.Options; + + if (options.Concurrency < 0) + { + throw new ArgumentOutOfRangeException(nameof(options.Concurrency), "Concurrency level should be a non-negative number."); + } + + if (options.Concurrency == 0) + { + options.Concurrency = Environment.ProcessorCount; + } + + using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D")); + var cntLoopsTotal = 0UL; + meter.CreateObservableCounter( + "OpenTelemetry.Tests.Stress.Loops", + () => unchecked((long)cntLoopsTotal), + description: "The total number of `Run()` invocations that are completed."); + var dLoopsPerSecond = 0D; + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.LoopsPerSecond", + () => dLoopsPerSecond, + description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds."); + var dCpuCyclesPerLoop = 0D; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.CpuCyclesPerLoop", + () => dCpuCyclesPerLoop, + description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds."); + } + + using var meterProvider = options.PrometheusInternalMetricsPort != 0 ? Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddRuntimeInstrumentation() + .AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusInternalMetricsPort}/" }) + .Build() : null; + + var statistics = new long[options.Concurrency]; + var watchForTotal = Stopwatch.StartNew(); + + TimeSpan? duration = options.DurationSeconds > 0 + ? TimeSpan.FromSeconds(options.DurationSeconds) + : null; + + Parallel.Invoke( + () => + { + Console.WriteLine($"Options: {JsonSerializer.Serialize(options)}"); + Console.WriteLine($"Run {Process.GetCurrentProcess().ProcessName}.exe --help to see available options."); + Console.Write($"Running (concurrency = {options.Concurrency}"); + + if (options.PrometheusInternalMetricsPort != 0) + { + Console.Write($", internalPrometheusEndpoint = http://localhost:{options.PrometheusInternalMetricsPort}/metrics/"); + } + + this.WriteRunInformationToConsole(); + + Console.WriteLine("), press to stop, press to toggle statistics in the console..."); + Console.WriteLine(this.output); + + var outputCursorTop = Console.CursorTop - 1; + + var bOutput = true; + var watch = new Stopwatch(); + while (true) + { + if (Console.KeyAvailable) + { + var key = Console.ReadKey(true).Key; + + switch (key) + { + case ConsoleKey.Enter: + Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), this.output)); + break; + case ConsoleKey.Escape: + this.bContinue = false; + return; + case ConsoleKey.Spacebar: + bOutput = !bOutput; + break; + } + + continue; + } + + if (bOutput) + { + var tempCursorLeft = Console.CursorLeft; + var tempCursorTop = Console.CursorTop; + Console.SetCursorPosition(0, outputCursorTop); + Console.WriteLine(this.output.PadRight(Console.BufferWidth)); + Console.SetCursorPosition(tempCursorLeft, tempCursorTop); + } + + var cntLoopsOld = (ulong)statistics.Sum(); + var cntCpuCyclesOld = StressTestNativeMethods.GetCpuCycles(); + + watch.Restart(); + Thread.Sleep(200); + watch.Stop(); + + cntLoopsTotal = (ulong)statistics.Sum(); + var cntCpuCyclesNew = StressTestNativeMethods.GetCpuCycles(); + + var nLoops = cntLoopsTotal - cntLoopsOld; + var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld; + + dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0); + dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops; + + var totalElapsedTime = watchForTotal.Elapsed; + + if (duration.HasValue) + { + this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RemainingTime (Seconds): {(duration.Value - totalElapsedTime).TotalSeconds:n0}"; + if (totalElapsedTime > duration) + { + this.bContinue = false; + return; + } + } + else + { + this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunningTime (Seconds): {totalElapsedTime.TotalSeconds:n0}"; + } + + Console.Title = this.output; + } + }, + () => + { + Parallel.For(0, options.Concurrency, (i) => + { + ref var count = ref statistics[i]; + + while (this.bContinue) + { + this.RunWorkItemInParallel(); + count++; + } + }); + }); + + watchForTotal.Stop(); + cntLoopsTotal = (ulong)statistics.Sum(); + var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); + var cntCpuCyclesTotal = StressTestNativeMethods.GetCpuCycles(); + var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; + Console.WriteLine("Stopping the stress test..."); + Console.WriteLine($"* Total Running Time (Seconds) {watchForTotal.Elapsed.TotalSeconds:n0}"); + Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}"); + Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}"); + Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}"); +#if !NETFRAMEWORK + Console.WriteLine($"* GC Total Allocated Bytes: {GC.GetTotalAllocatedBytes()}"); +#endif + } + + protected virtual void WriteRunInformationToConsole() + { + } + + protected abstract void RunWorkItemInParallel(); + + protected virtual void Dispose(bool isDisposing) + { + } +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs new file mode 100644 index 00000000000..6f3e7ff9ea7 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace OpenTelemetry.Tests.Stress; + +public static class StressTestFactory +{ + public static int RunSynchronously(string[] commandLineArguments) + where TStressTest : StressTest + { + return RunSynchronously(commandLineArguments); + } + + public static int RunSynchronously(string[] commandLineArguments) + where TStressTest : StressTest + where TStressTestOptions : StressTestOptions + { + return Parser.Default.ParseArguments(commandLineArguments) + .MapResult( + CreateStressTestAndRunSynchronously, + _ => 1); + + static int CreateStressTestAndRunSynchronously(TStressTestOptions options) + { + using var stressTest = (TStressTest)Activator.CreateInstance(typeof(TStressTest), options)!; + + stressTest.RunSynchronously(); + + return 0; + } + } +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs new file mode 100644 index 00000000000..da3df1c2864 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.InteropServices; + +namespace OpenTelemetry.Tests.Stress; + +internal static class StressTestNativeMethods +{ + public static ulong GetCpuCycles() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return 0; + } + + if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles)) + { + return 0; + } + + return cycles; + } + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles); +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs b/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs new file mode 100644 index 00000000000..2dcb2b2e47c --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace OpenTelemetry.Tests.Stress; + +public class StressTestOptions +{ + [Option('c', "concurrency", HelpText = "The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount.", Required = false)] + public int Concurrency { get; set; } + + [Option('p', "internal_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to disable. Default value: 9464.", Required = false)] + public int PrometheusInternalMetricsPort { get; set; } = 9464; + + [Option('d', "duration", HelpText = "The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0.", Required = false)] + public int DurationSeconds { get; set; } +} From 24d52c41495502fbe7d1f451c13516936a3bc8c8 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 26 Feb 2024 11:39:08 -0800 Subject: [PATCH 06/16] [tools] Stress test README updates (#5388) --- .../OpenTelemetry.Tests.Stress.Logs/README.md | 20 ++- .../README.md | 36 ++++- .../README.md | 20 ++- test/OpenTelemetry.Tests.Stress/README.md | 141 ++++++++++++------ 4 files changed, 166 insertions(+), 51 deletions(-) diff --git a/test/OpenTelemetry.Tests.Stress.Logs/README.md b/test/OpenTelemetry.Tests.Stress.Logs/README.md index 20c4b87db6e..b2e50ece933 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/README.md +++ b/test/OpenTelemetry.Tests.Stress.Logs/README.md @@ -10,5 +10,23 @@ based on the [OpenTelemetry.Tests.Stress](../OpenTelemetry.Tests.Stress/README.m Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/README.md b/test/OpenTelemetry.Tests.Stress.Metrics/README.md index 26c6ac1b1fe..3201c7c5900 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/README.md +++ b/test/OpenTelemetry.Tests.Stress.Metrics/README.md @@ -5,11 +5,6 @@ This stress test is specifically for Metrics SDK, and is based on the * [Running the stress test](#running-the-stress-test) -> [!NOTE] -> To run the stress tests for Histogram, comment out the `Run` method -for `Counter` and uncomment everything related to `Histogram` in the -[Program.cs](../OpenTelemetry.Tests.Stress.Metrics/Program.cs). - ## Running the stress test Open a console, run the following command from the current folder: @@ -17,3 +12,34 @@ Open a console, run the following command from the current folder: ```sh dotnet run --framework net8.0 --configuration Release ``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -t, --type The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram. + + -m, --metrics_port The Prometheus http listener port where Prometheus will be exposed for retrieving test metrics while the stress test is running. Set to '0' to disable. + Default value: 9185. + + -v, --view Whether or not a view should be configured to filter tags for the stress test. Default value: False. + + -o, --otlp Whether or not an OTLP exporter should be added for the stress test. Default value: False. + + -i, --interval The OTLP exporter export interval in milliseconds. Default value: 5000. + + -e, --exemplars Whether or not to enable exemplars for the stress test. Default value: False. + + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. +``` diff --git a/test/OpenTelemetry.Tests.Stress.Traces/README.md b/test/OpenTelemetry.Tests.Stress.Traces/README.md index 54998de1e76..005614d3361 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/README.md +++ b/test/OpenTelemetry.Tests.Stress.Traces/README.md @@ -10,5 +10,23 @@ based on the [OpenTelemetry.Tests.Stress](../OpenTelemetry.Tests.Stress/README.m Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` diff --git a/test/OpenTelemetry.Tests.Stress/README.md b/test/OpenTelemetry.Tests.Stress/README.md index 1f953b1def1..0aa032669fb 100644 --- a/test/OpenTelemetry.Tests.Stress/README.md +++ b/test/OpenTelemetry.Tests.Stress/README.md @@ -18,26 +18,45 @@ Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` Once the application started, you will see the performance number updates from -the console window title. +the console window title and the console window itself. + +While a test is running... -Use the `SPACE` key to toggle the console output, which is off by default. +* Use the `SPACE` key to toggle the console output, which is on by default. -Use the `ENTER` key to print the latest performance statistics. +* Use the `ENTER` key to print the latest performance statistics. -Use the `ESC` key to exit the stress test. +* Use the `ESC` key to exit the stress test. + +Example output while a test is running: ```text -Running (concurrency = 1), press to stop... -2021-09-28T18:47:17.6807622Z Loops: 17,549,732,467, Loops/Second: 738,682,519, CPU Cycles/Loop: 3 -2021-09-28T18:47:17.8846348Z Loops: 17,699,532,304, Loops/Second: 731,866,438, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.0914577Z Loops: 17,850,498,225, Loops/Second: 730,931,752, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.2992864Z Loops: 18,000,133,808, Loops/Second: 724,029,883, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.5052989Z Loops: 18,150,598,194, Loops/Second: 733,026,161, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.7116733Z Loops: 18,299,461,007, Loops/Second: 724,950,210, CPU Cycles/Loop: 3 +Options: {"Concurrency":20,"PrometheusInternalMetricsPort":9464,"DurationSeconds":0} +Run OpenTelemetry.Tests.Stress.exe --help to see available options. +Running (concurrency = 20, internalPrometheusEndpoint = http://localhost:9464/metrics/), press to stop, press to toggle statistics in the console... +Loops: 17,384,826,748, Loops/Second: 2,375,222,037, CPU Cycles/Loop: 24, RunningTime (Seconds): 7 ``` The stress test metrics are exposed via @@ -73,59 +92,91 @@ process_runtime_dotnet_gc_allocations_size_bytes 5485192 1658950184752 ## Writing your own stress test -> [!WARNING] -> These instructions are out of date and should NOT be followed. They will be - updated soon. - Create a simple console application with the following code: ```csharp -using System.Runtime.CompilerServices; +using OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - public static void Main() + public static int Main(string[] args) { - Stress(concurrency: 10, prometheusPort: 9464); + return StressTestFactory.RunSynchronously(args); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class MyStressTest : StressTest { - // add your logic here + public MyStressTest(StressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + } } } ``` -Add the Skeleton.cs file to your `*.csproj` file: +Add the following project reference to the project: ```xml - - - + ``` -Add the following packages to the project: +Now you are ready to run your own stress test. Add test logic in the +`RunWorkItemInParallel` method to measure performance. -```shell -dotnet add package OpenTelemetry.Exporter.Prometheus --prerelease -dotnet add package OpenTelemetry.Instrumentation.Runtime --prerelease -``` +To define custom options create an options class which derives from +`StressTestOptions`: -Now you are ready to run your own stress test. +```csharp +using CommandLine; +using OpenTelemetry.Tests.Stress; + +public static class Program +{ + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } + + private sealed class MyStressTest : StressTest + { + public MyStressTest(MyStressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + // Use this.Options here to access options supplied + // on the command line. + } + } + + private sealed class MyStressTestOptions : StressTestOptions + { + [Option('r', "rate", HelpText = "Add help text here for the rate option. Default value: 0.", Required = false)] + public int Rate { get; set; } = 0; + } +} +``` Some useful notes: -* You can specify the concurrency using `Stress(concurrency: {concurrency - number})`, the default value is the number of CPU cores. Keep in mind that - concurrency level does not equal to the number of threads. -* You can specify a local PrometheusExporter listening port using - `Stress(prometheusPort: {port number})`, the default value is `0`, which will - turn off the PrometheusExporter. -* You want to put `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on - `Run()`, this helps to reduce extra flushes on the CPU instruction cache. -* You might want to run the stress test under `Release` mode rather than `Debug` - mode. +* It is generally best practice to run the stress test for code compiled in + `Release` configuration rather than `Debug` configuration. `Debug` builds + typically are not optimized and contain extra code which will change the + performance characteristics of the logic under test. The stress test will + write a warning message to the console when starting if compiled with `Debug` + configuration. +* You can specify the concurrency using `-c` or `--concurrency` command line + argument, the default value if not specified is the number of CPU cores. Keep + in mind that concurrency level does not equal to the number of threads. +* You can use the duration `-d` or `--duration` command line argument to run the + stress test for a specific time period. This is useful when comparing changes + across multiple runs. ## Understanding the results @@ -134,4 +185,6 @@ Some useful notes: sliding window of few hundreds of milliseconds. * `CPU Cycles/Loop` represents the average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds. -* `Runaway Time` represents the runaway time (seconds) since the test started. +* `Total Running Time` represents the running time (seconds) since the test started. +* `GC Total Allocated Bytes` (not available on .NET Framework) shows the total + amount of memory allocated while the test was running. From 42c593d8c2a99bc90afef1e555507747af7f4a98 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 10:49:44 -0800 Subject: [PATCH 07/16] [sdk-metrics] Exemplar spec improvements (#5386) --- .../ConsoleMetricExporter.cs | 44 ++++-- .../Implementation/MetricItemExtensions.cs | 93 +++++------- .../Experimental/PublicAPI.Unshipped.txt | 27 +++- src/OpenTelemetry/CHANGELOG.md | 8 + src/OpenTelemetry/Metrics/AggregatorStore.cs | 8 +- ...AlignedHistogramBucketExemplarReservoir.cs | 88 ++--------- .../Metrics/Exemplar/Exemplar.cs | 138 ++++++++++++++++-- .../Metrics/Exemplar/ExemplarMeasurement.cs | 62 ++++++++ .../Metrics/Exemplar/ExemplarReservoir.cs | 46 +++--- .../Exemplar/FixedSizeExemplarReservoir.cs | 91 ++++++++++++ .../Exemplar/ReadOnlyExemplarCollection.cs | 125 ++++++++++++++++ .../SimpleFixedSizeExemplarReservoir.cs | 120 +++------------ src/OpenTelemetry/Metrics/MetricPoint.cs | 65 +++++---- .../Metrics/MetricPointOptionalComponents.cs | 9 +- .../ReadOnlyFilteredTagCollection.cs | 120 +++++++++++++++ src/OpenTelemetry/ReadOnlyTagCollection.cs | 21 +-- .../Metrics/MetricExemplarTests.cs | 13 +- .../Metrics/MetricTestsBase.cs | 9 +- 18 files changed, 730 insertions(+), 357 deletions(-) create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs create mode 100644 src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 4899f0d923e..f3f4b0ba5f9 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -188,30 +188,44 @@ public override ExportResult Export(in Batch batch) } var exemplarString = new StringBuilder(); - foreach (var exemplar in metricPoint.GetExemplars()) + if (metricPoint.TryGetExemplars(out var exemplars)) { - if (exemplar.Timestamp != default) + foreach (ref readonly var exemplar in exemplars) { - exemplarString.Append("Value: "); - exemplarString.Append(exemplar.DoubleValue); - exemplarString.Append(" Timestamp: "); + exemplarString.Append("Timestamp: "); exemplarString.Append(exemplar.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); - exemplarString.Append(" TraceId: "); - exemplarString.Append(exemplar.TraceId); - exemplarString.Append(" SpanId: "); - exemplarString.Append(exemplar.SpanId); + if (metricType.IsDouble()) + { + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.DoubleValue); + } + else if (metricType.IsLong()) + { + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.LongValue); + } - if (exemplar.FilteredTags != null && exemplar.FilteredTags.Count > 0) + if (exemplar.TraceId != default) { - exemplarString.Append(" Filtered Tags : "); + exemplarString.Append(" TraceId: "); + exemplarString.Append(exemplar.TraceId.ToHexString()); + exemplarString.Append(" SpanId: "); + exemplarString.Append(exemplar.SpanId.ToHexString()); + } - foreach (var tag in exemplar.FilteredTags) + bool appendedTagString = false; + foreach (var tag in exemplar.FilteredTags) + { + if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) + if (!appendedTagString) { - exemplarString.Append(result); - exemplarString.Append(' '); + exemplarString.Append(" Filtered Tags : "); + appendedTagString = true; } + + exemplarString.Append(result); + exemplarString.Append(' '); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index d93105e3dfb..53276e617e0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Diagnostics; using System.Runtime.CompilerServices; using Google.Protobuf; using Google.Protobuf.Collections; @@ -267,37 +268,12 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) } } - var exemplars = metricPoint.GetExemplars(); - foreach (var examplar in exemplars) + if (metricPoint.TryGetExemplars(out var exemplars)) { - if (examplar.Timestamp != default) + foreach (ref readonly var exemplar in exemplars) { - byte[] traceIdBytes = new byte[16]; - examplar.TraceId?.CopyTo(traceIdBytes); - - byte[] spanIdBytes = new byte[8]; - examplar.SpanId?.CopyTo(spanIdBytes); - - var otlpExemplar = new OtlpMetrics.Exemplar - { - TimeUnixNano = (ulong)examplar.Timestamp.ToUnixTimeNanoseconds(), - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - AsDouble = examplar.DoubleValue, - }; - - if (examplar.FilteredTags != null) - { - foreach (var tag in examplar.FilteredTags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - otlpExemplar.FilteredAttributes.Add(result); - } - } - } - - dataPoint.Exemplars.Add(otlpExemplar); + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); } } @@ -379,51 +355,48 @@ private static void AddScopeAttributes(IEnumerable> } } - /* - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) + private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + where T : struct { - var otlpExemplar = new OtlpMetrics.Exemplar(); - - if (exemplar.Value is double doubleValue) + var otlpExemplar = new OtlpMetrics.Exemplar { - otlpExemplar.AsDouble = doubleValue; - } - else if (exemplar.Value is long longValue) + TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(), + }; + + if (exemplar.TraceId != default) { - otlpExemplar.AsInt = longValue; + byte[] traceIdBytes = new byte[16]; + exemplar.TraceId.CopyTo(traceIdBytes); + + byte[] spanIdBytes = new byte[8]; + exemplar.SpanId.CopyTo(spanIdBytes); + + otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); } - else + + if (typeof(T) == typeof(long)) { - // TODO: Determine how we want to handle exceptions here. - // Do we want to just skip this exemplar and move on? - // Should we skip recording the whole metric? - throw new ArgumentException(); + otlpExemplar.AsInt = (long)(object)value; } - - otlpExemplar.TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(); - - // TODO: Do the TagEnumerationState thing. - foreach (var tag in exemplar.FilteredTags) + else if (typeof(T) == typeof(double)) { - otlpExemplar.FilteredAttributes.Add(tag.ToOtlpAttribute()); + otlpExemplar.AsDouble = (double)(object)value; } - - if (exemplar.TraceId != default) + else { - byte[] traceIdBytes = new byte[16]; - exemplar.TraceId.CopyTo(traceIdBytes); - otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + Debug.Fail("Unexpected type"); + otlpExemplar.AsDouble = Convert.ToDouble(value); } - if (exemplar.SpanId != default) + foreach (var tag in exemplar.FilteredTags) { - byte[] spanIdBytes = new byte[8]; - exemplar.SpanId.CopyTo(spanIdBytes); - otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + otlpExemplar.FilteredAttributes.Add(result); + } } return otlpExemplar; } - */ } diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index 16832495101..b530cdf98f6 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -12,16 +12,36 @@ OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void OpenTelemetry.Metrics.Exemplar OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? +OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.Metrics.Exemplar.LongValue.get -> long +OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset -OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? +OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! +OpenTelemetry.Metrics.ExemplarMeasurement +OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void +OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> +OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T +OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection? exemplars) -> bool OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Current.get -> OpenTelemetry.Metrics.Exemplar +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.GetEnumerator() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.ReadOnlyExemplarCollection() -> void OpenTelemetry.Metrics.TraceBasedExemplarFilter OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Enumerator() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.ReadOnlyFilteredTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.ReadOnlyFilteredTagCollection() -> void static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! @@ -38,7 +58,6 @@ static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTele static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>? override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 6260596c78c..5cfbd35be90 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -42,6 +42,14 @@ [IMetricsListener](https://learn.microsoft.com/dotNet/api/microsoft.extensions.diagnostics.metrics.imetricslistener). ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) +* **Experimental (pre-release builds only):** The `Exemplar.FilteredTags` + property now returns a `ReadOnlyFilteredTagCollection` instance and the + `Exemplar.LongValue` property has been added. The `MetricPoint.GetExemplars` + method has been replaced by `MetricPoint.TryGetExemplars` which outputs a + `ReadOnlyExemplarCollection` instance. These are **breaking changes** for + metrics exporters which support exemplars. + ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index a5d603589ad..fa4cefc7221 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -11,6 +11,7 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { + internal readonly HashSet? TagKeysInteresting; internal readonly bool OutputDelta; internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; internal readonly int CardinalityLimit; @@ -24,7 +25,6 @@ internal sealed class AggregatorStore private readonly object lockZeroTags = new(); private readonly object lockOverflowTag = new(); - private readonly HashSet? tagKeysInteresting; private readonly int tagsKeysInterestingCount; // This holds the reclaimed MetricPoints that are available for reuse. @@ -84,7 +84,7 @@ internal AggregatorStore( this.updateLongCallback = this.UpdateLongCustomTags; this.updateDoubleCallback = this.UpdateDoubleCustomTags; var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); - this.tagKeysInteresting = hs; + this.TagKeysInteresting = hs; this.tagsKeysInterestingCount = hs.Count; } @@ -1122,9 +1122,9 @@ private int FindMetricAggregatorsCustomTag(ReadOnlySpan /// The AlignedHistogramBucketExemplarReservoir implementation. /// -internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir +internal sealed class AlignedHistogramBucketExemplarReservoir : FixedSizeExemplarReservoir { - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; - - public AlignedHistogramBucketExemplarReservoir(int length) - { - this.runningExemplars = new Exemplar[length + 1]; - this.tempExemplars = new Exemplar[length + 1]; - } - - public override void Offer(long value, ReadOnlySpan> tags, int index = default) + public AlignedHistogramBucketExemplarReservoir(int numberOfBuckets) + : base(numberOfBuckets + 1) { - this.OfferAtBoundary(value, tags, index); } - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.OfferAtBoundary(value, tags, index); + Debug.Fail("AlignedHistogramBucketExemplarReservoir shouldn't be used with long values"); } - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) + public override void Offer(in ExemplarMeasurement measurement) { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags!.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - } - } - - return this.tempExemplars; - } - - private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index) - { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } - - if (exemplar.FilteredTags == null) - { - exemplar.FilteredTags = new List>(tags.Length); - } - else - { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } + Debug.Assert( + measurement.ExplicitBucketHistogramBucketIndex != -1, + "ExplicitBucketHistogramBucketIndex was -1"); - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); - } + this.UpdateExemplar(measurement.ExplicitBucketHistogramBucketIndex, in measurement); } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index d635a1a4ad4..b3a862aa528 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,13 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; #if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif -using System.Diagnostics; - namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES @@ -20,39 +19,150 @@ namespace OpenTelemetry.Metrics; #endif public #else -/// -/// Represents an Exemplar data. -/// -#pragma warning disable SA1623 // The property's documentation summary text should begin with: `Gets or sets` internal #endif struct Exemplar { + internal HashSet? ViewDefinedTagKeys; + + private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, Array.Empty>(), count: 0); + private int tagCount; + private KeyValuePair[]? tagStorage; + private MetricPointValueStorage valueStorage; + /// /// Gets the timestamp. /// - public DateTimeOffset Timestamp { get; internal set; } + public DateTimeOffset Timestamp { readonly get; private set; } /// /// Gets the TraceId. /// - public ActivityTraceId? TraceId { get; internal set; } + public ActivityTraceId TraceId { readonly get; private set; } /// /// Gets the SpanId. /// - public ActivitySpanId? SpanId { get; internal set; } + public ActivitySpanId SpanId { readonly get; private set; } - // TODO: Leverage MetricPointValueStorage - // and allow double/long instead of double only. + /// + /// Gets the long value. + /// + public long LongValue + { + readonly get => this.valueStorage.AsLong; + private set => this.valueStorage.AsLong = value; + } /// /// Gets the double value. /// - public double DoubleValue { get; internal set; } + public double DoubleValue + { + readonly get => this.valueStorage.AsDouble; + private set => this.valueStorage.AsDouble = value; + } /// - /// Gets the FilteredTags (i.e any tags that were dropped during aggregation). + /// Gets the filtered tags. /// - public List>? FilteredTags { get; internal set; } + /// + /// Note: represents the set of tags which were + /// supplied at measurement but dropped due to filtering configured by a + /// view (). If view tag + /// filtering is not configured will be empty. + /// + public readonly ReadOnlyFilteredTagCollection FilteredTags + { + get + { + if (this.tagCount == 0) + { + return Empty; + } + else + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); + + return new(this.ViewDefinedTagKeys, this.tagStorage!, this.tagCount); + } + } + } + + internal void Update(in ExemplarMeasurement measurement) + where T : struct + { + this.Timestamp = DateTimeOffset.UtcNow; + + if (typeof(T) == typeof(long)) + { + this.LongValue = (long)(object)measurement.Value; + } + else if (typeof(T) == typeof(double)) + { + this.DoubleValue = (double)(object)measurement.Value; + } + else + { + Debug.Fail("Invalid value type"); + this.DoubleValue = Convert.ToDouble(measurement.Value); + } + + var currentActivity = Activity.Current; + if (currentActivity != null) + { + this.TraceId = currentActivity.TraceId; + this.SpanId = currentActivity.SpanId; + } + else + { + this.TraceId = default; + this.SpanId = default; + } + + this.StoreRawTags(measurement.Tags); + } + + internal void Reset() + { + this.Timestamp = default; + } + + internal readonly bool IsUpdated() + { + return this.Timestamp != default; + } + + internal readonly void Copy(ref Exemplar destination) + { + destination.Timestamp = this.Timestamp; + destination.TraceId = this.TraceId; + destination.SpanId = this.SpanId; + destination.valueStorage = this.valueStorage; + destination.ViewDefinedTagKeys = this.ViewDefinedTagKeys; + destination.tagCount = this.tagCount; + if (destination.tagCount > 0) + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); + + destination.tagStorage = new KeyValuePair[destination.tagCount]; + Array.Copy(this.tagStorage!, 0, destination.tagStorage, 0, destination.tagCount); + } + } + + private void StoreRawTags(ReadOnlySpan> tags) + { + this.tagCount = tags.Length; + if (tags.Length == 0) + { + return; + } + + if (this.tagStorage == null || this.tagStorage.Length < this.tagCount) + { + this.tagStorage = new KeyValuePair[this.tagCount]; + } + + tags.CopyTo(this.tagStorage); + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs new file mode 100644 index 00000000000..fa1c50b98d5 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Represents an Exemplar measurement. +/// +/// +/// Measurement type. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly ref struct ExemplarMeasurement + where T : struct +{ + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = -1; + } + + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags, + int explicitBucketHistogramIndex) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = explicitBucketHistogramIndex; + } + + /// + /// Gets the measurement value. + /// + public T Value { get; } + + /// + /// Gets the measurement tags. + /// + /// + /// Note: represents the full set of tags supplied at + /// measurement regardless of any filtering configured by a view (). + /// + public ReadOnlySpan> Tags { get; } + + internal int ExplicitBucketHistogramBucketIndex { get; } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index d5b944ff656..1a19719bbfa 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -9,32 +9,38 @@ namespace OpenTelemetry.Metrics; internal abstract class ExemplarReservoir { /// - /// Offers measurement to the reservoir. + /// Gets a value indicating whether or not the should reset its state when performing + /// collection. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(long value, ReadOnlySpan> tags, int index = default); + /// + /// Note: is set to for + /// s using delta aggregation temporality and for s using cumulative + /// aggregation temporality. + /// + public bool ResetOnCollect { get; private set; } /// - /// Offers measurement to the reservoir. + /// Offers a measurement to the reservoir. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(double value, ReadOnlySpan> tags, int index = default); + /// . + public abstract void Offer(in ExemplarMeasurement measurement); + + /// + /// Offers a measurement to the reservoir. + /// + /// . + public abstract void Offer(in ExemplarMeasurement measurement); /// /// Collects all the exemplars accumulated by the Reservoir. /// - /// The actual tags that are part of the metric. Exemplars are - /// only expected to contain any filtered tags, so this will allow the reservoir - /// to prepare the filtered tags from all the tags it is given by doing the - /// equivalent of filtered tags = all tags - actual tags. - /// - /// Flag to indicate if the reservoir should be reset after this call. - /// Array of Exemplars. - public abstract Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset); + /// . + public abstract ReadOnlyExemplarCollection Collect(); + + internal virtual void Initialize(AggregatorStore aggregatorStore) + { + this.ResetOnCollect = aggregatorStore.OutputDelta; + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs new file mode 100644 index 00000000000..3d7057f85d0 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +internal abstract class FixedSizeExemplarReservoir : ExemplarReservoir +{ + private readonly Exemplar[] runningExemplars; + private readonly Exemplar[] snapshotExemplars; + + protected FixedSizeExemplarReservoir(int capacity) + { + Guard.ThrowIfOutOfRange(capacity, min: 1); + + this.runningExemplars = new Exemplar[capacity]; + this.snapshotExemplars = new Exemplar[capacity]; + this.Capacity = capacity; + } + + internal int Capacity { get; } + + /// + /// Collects all the exemplars accumulated by the Reservoir. + /// + /// . + public sealed override ReadOnlyExemplarCollection Collect() + { + var runningExemplars = this.runningExemplars; + + if (this.ResetOnCollect) + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + running.Reset(); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + else + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + + this.OnCollected(); + + return new(this.snapshotExemplars); + } + + internal sealed override void Initialize(AggregatorStore aggregatorStore) + { + var viewDefinedTagKeys = aggregatorStore.TagKeysInteresting; + + for (int i = 0; i < this.runningExemplars.Length; i++) + { + this.runningExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + this.snapshotExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + } + + base.Initialize(aggregatorStore); + } + + protected virtual void OnCollected() + { + } + + protected void UpdateExemplar(int exemplarIndex, in ExemplarMeasurement measurement) + where T : struct + { + this.runningExemplars[exemplarIndex].Update(in measurement); + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs new file mode 100644 index 00000000000..781789184a8 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of s. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyExemplarCollection +{ + private readonly Exemplar[] exemplars; + + internal ReadOnlyExemplarCollection(Exemplar[] exemplars) + { + Debug.Assert(exemplars != null, "exemplars was null"); + + this.exemplars = exemplars!; + } + + /// + /// Gets the maximum number of s in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// which s in the collection received updates. + /// + internal int MaximumCount => this.exemplars.Length; + + /// + /// Returns an enumerator that iterates through the s. + /// + /// . + public Enumerator GetEnumerator() + => new(this.exemplars); + + internal ReadOnlyExemplarCollection Copy() + { + var exemplarCopies = new Exemplar[this.exemplars.Length]; + + int i = 0; + foreach (ref readonly var exemplar in this) + { + exemplar.Copy(ref exemplarCopies[i++]); + } + + return new ReadOnlyExemplarCollection(exemplarCopies); + } + + internal IReadOnlyList ToReadOnlyList() + { + var list = new List(this.MaximumCount); + + foreach (var item in this) + { + list.Add(item); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator + { + private readonly Exemplar[] exemplars; + private int index; + + internal Enumerator(Exemplar[] exemplars) + { + this.exemplars = exemplars; + this.index = -1; + } + + /// + /// Gets the at the current position of the enumerator. + /// + public readonly ref readonly Exemplar Current + => ref this.exemplars[this.index]; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + var exemplars = this.exemplars; + + while (true) + { + var index = ++this.index; + if (index < exemplars.Length) + { + if (!exemplars[index].IsUpdated()) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs index 5324e7067d2..930b9647bb5 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -1,140 +1,56 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; - namespace OpenTelemetry.Metrics; /// /// The SimpleFixedSizeExemplarReservoir implementation. /// -internal sealed class SimpleFixedSizeExemplarReservoir : ExemplarReservoir +internal sealed class SimpleFixedSizeExemplarReservoir : FixedSizeExemplarReservoir { - private readonly int poolSize; - private readonly Random random; - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; + private readonly Random random = new(); - private long measurementsSeen; + private int measurementsSeen; public SimpleFixedSizeExemplarReservoir(int poolSize) + : base(poolSize) { - this.poolSize = poolSize; - this.runningExemplars = new Exemplar[poolSize]; - this.tempExemplars = new Exemplar[poolSize]; - this.measurementsSeen = 0; - this.random = new Random(); } - public override void Offer(long value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.Offer(value, tags); + this.Offer(in measurement); } - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.Offer(value, tags); + this.Offer(in measurement); } - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) + protected override void OnCollected() { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags!.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - } - } - // Reset internal state irrespective of temporality. // This ensures incoming measurements have fair chance // of making it to the reservoir. this.measurementsSeen = 0; - - return this.tempExemplars; } - private void Offer(double value, ReadOnlySpan> tags) + private void Offer(in ExemplarMeasurement measurement) + where T : struct { - if (this.measurementsSeen < this.poolSize) + var measurementNumber = this.measurementsSeen++; + + if (measurementNumber < this.Capacity) { - ref var exemplar = ref this.runningExemplars[this.measurementsSeen]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); + this.UpdateExemplar(measurementNumber, in measurement); } else { - // TODO: RandomNext64 is only available in .NET 6 or newer. - int upperBound = 0; - unchecked - { - upperBound = (int)this.measurementsSeen; - } - - var index = this.random.Next(0, upperBound); - if (index < this.poolSize) + var index = this.random.Next(0, measurementNumber); + if (index < this.Capacity) { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); + this.UpdateExemplar(index, in measurement); } } - - this.measurementsSeen++; - } - - private void StoreTags(ref Exemplar exemplar, ReadOnlySpan> tags) - { - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } - - if (exemplar.FilteredTags == null) - { - exemplar.FilteredTags = new List>(tags.Length); - } - else - { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } - - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); - } } } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 38ee0b18c86..65a62c3eb4a 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -102,6 +103,8 @@ internal MetricPoint( this.mpComponents = new MetricPointOptionalComponents(); } + reservoir.Initialize(aggregatorStore); + this.mpComponents.ExemplarReservoir = reservoir; } @@ -346,21 +349,18 @@ public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) /// Gets the exemplars associated with the metric point. /// /// - /// . + /// . + /// if exemplars exist; otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] public #else - /// - /// Gets the exemplars associated with the metric point. - /// - /// . [MethodImpl(MethodImplOptions.AggressiveInlining)] internal #endif - readonly Exemplar[] GetExemplars() + readonly bool TryGetExemplars([NotNullWhen(true)] out ReadOnlyExemplarCollection? exemplars) { - // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents?.Exemplars ?? Array.Empty(); + exemplars = this.mpComponents?.Exemplars; + return exemplars.HasValue; } internal readonly MetricPoint Copy() @@ -469,7 +469,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -489,7 +490,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -509,7 +511,8 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -672,7 +675,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -695,7 +699,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -718,7 +723,8 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -885,8 +891,6 @@ internal void TakeSnapshot(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -941,7 +945,6 @@ internal void TakeSnapshot(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1058,7 +1061,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsLong = this.runningValue.AsLong; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1082,7 +1085,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsDouble = this.runningValue.AsDouble; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1095,7 +1098,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsLong = this.runningValue.AsLong; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1108,7 +1111,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsDouble = this.runningValue.AsDouble; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.mpComponents.ReleaseLock(); @@ -1134,7 +1137,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; @@ -1160,7 +1163,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningSum = 0; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1191,7 +1194,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1220,7 +1223,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningMax = double.NegativeInfinity; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1306,7 +1309,8 @@ private void UpdateHistogram(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -1334,7 +1338,8 @@ private void UpdateHistogramWithMinMax(double number, ReadOnlySpan(number, tags)); } this.mpComponents.ReleaseLock(); @@ -1362,7 +1367,8 @@ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan(number, tags, i)); } } @@ -1391,7 +1397,8 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan(number, tags, i)); } histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index f028b2add56..84511b1b549 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -20,7 +20,7 @@ internal sealed class MetricPointOptionalComponents public ExemplarReservoir? ExemplarReservoir; - public Exemplar[]? Exemplars; + public ReadOnlyExemplarCollection? Exemplars; private int isCriticalSectionOccupied = 0; @@ -30,14 +30,9 @@ public MetricPointOptionalComponents Copy() { HistogramBuckets = this.HistogramBuckets?.Copy(), Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy(), + Exemplars = this.Exemplars?.Copy(), }; - if (this.Exemplars != null) - { - copy.Exemplars = new Exemplar[this.Exemplars.Length]; - Array.Copy(this.Exemplars, copy.Exemplars, this.Exemplars.Length); - } - return copy; } diff --git a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs new file mode 100644 index 00000000000..924b6e36f04 --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of tag key/value pairs which returns a filtered +/// subset of tags when enumerated. +/// +// Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to +// prevent accidental boxing. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyFilteredTagCollection +{ + private readonly HashSet? excludedKeys; + private readonly KeyValuePair[] tags; + private readonly int count; + + internal ReadOnlyFilteredTagCollection( + HashSet? excludedKeys, + KeyValuePair[] tags, + int count) + { + Debug.Assert(tags != null, "tags was null"); + Debug.Assert(count <= tags!.Length, "count was invalid"); + + this.excludedKeys = excludedKeys; + this.tags = tags; + this.count = count; + } + + /// + /// Gets the maximum number of tags in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// the filter. + /// + internal int MaximumCount => this.count; + + /// + /// Returns an enumerator that iterates through the tags. + /// + /// . + public Enumerator GetEnumerator() => new(this); + + internal IReadOnlyList> ToReadOnlyList() + { + var list = new List>(this.MaximumCount); + + foreach (var item in this) + { + list.Add(item); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly ReadOnlyFilteredTagCollection source; + private int index; + + internal Enumerator(ReadOnlyFilteredTagCollection source) + { + this.source = source; + this.index = -1; + } + + /// + /// Gets the tag at the current position of the enumerator. + /// + public readonly KeyValuePair Current + => this.source.tags[this.index]; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + while (true) + { + int index = ++this.index; + if (index < this.source.MaximumCount) + { + if (this.source.excludedKeys?.Contains(this.source.tags[index].Key) ?? false) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index 3c7dc59d770..f8582e1af99 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -40,14 +40,14 @@ public struct Enumerator internal Enumerator(ReadOnlyTagCollection source) { this.source = source; - this.index = 0; - this.Current = default; + this.index = -1; } /// /// Gets the tag at the current position of the enumerator. /// - public KeyValuePair Current { get; private set; } + public readonly KeyValuePair Current + => this.source.KeyAndValues[this.index]; /// /// Advances the enumerator to the next element of the if the enumerator has passed the end of the /// collection. - public bool MoveNext() - { - int index = this.index; - - if (index < this.source.Count) - { - this.Current = this.source.KeyAndValues[index]; - - this.index++; - return true; - } - - return false; - } + public bool MoveNext() => ++this.index < this.source.Count; } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index b5fd844f43c..356e4ac8acb 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -167,9 +167,12 @@ public void TestExemplarsFilterTags() Assert.NotNull(exemplars); foreach (var exemplar in exemplars) { - Assert.NotNull(exemplar.FilteredTags); - Assert.Contains(new("key2", "value1"), exemplar.FilteredTags); - Assert.Contains(new("key3", "value1"), exemplar.FilteredTags); + Assert.NotEqual(0, exemplar.FilteredTags.MaximumCount); + + var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); + + Assert.Contains(new("key2", "value1"), filteredTags); + Assert.Contains(new("key3", "value1"), filteredTags); } } @@ -185,14 +188,14 @@ private static double[] GenerateRandomValues(int count) return values; } - private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists) + private static void ValidateExemplars(IReadOnlyList exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists) { Assert.NotNull(exemplars); foreach (var exemplar in exemplars) { Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); Assert.Contains(exemplar.DoubleValue, measurementValues); - Assert.Null(exemplar.FilteredTags); + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); if (traceContextExists) { Assert.NotEqual(default, exemplar.TraceId); diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 0e5c1e1e53f..6d18bed47de 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -233,9 +233,14 @@ public IDisposable BuildMeterProvider( #endif } - internal static Exemplar[] GetExemplars(MetricPoint mp) + internal static IReadOnlyList GetExemplars(MetricPoint mp) { - return mp.GetExemplars().Where(exemplar => exemplar.Timestamp != default).ToArray(); + if (mp.TryGetExemplars(out var exemplars)) + { + return exemplars.Value.ToReadOnlyList(); + } + + return Array.Empty(); } #if BUILDING_HOSTING_TESTS From 44432235e5c9c97ab127b5f0b70b7b0536fdc481 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 12:04:44 -0800 Subject: [PATCH 08/16] [sdk-metrics] XML doc tweaks for exemplar experimental APIs (#5392) --- .../Builder/MeterProviderBuilderExtensions.cs | 2 +- .../AlignedHistogramBucketExemplarReservoir.cs | 6 +++++- .../Metrics/Exemplar/AlwaysOffExemplarFilter.cs | 14 +++++++------- .../Metrics/Exemplar/AlwaysOnExemplarFilter.cs | 9 +++++++-- src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs | 8 ++++++-- .../Metrics/Exemplar/ExemplarFilter.cs | 11 ++++++----- .../Metrics/Exemplar/ExemplarMeasurement.cs | 2 +- .../Metrics/Exemplar/ExemplarReservoir.cs | 6 +++++- .../Exemplar/ReadOnlyExemplarCollection.cs | 2 +- .../Exemplar/SimpleFixedSizeExemplarReservoir.cs | 6 +++++- .../Metrics/Exemplar/TraceBasedExemplarFilter.cs | 15 ++++++++------- src/OpenTelemetry/Metrics/MetricPoint.cs | 2 +- 12 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 4a6d67a5082..ee1de49604f 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -322,7 +322,7 @@ public static MeterProvider Build(this MeterProviderBuilder meterProviderBuilder /// Sets the to be used for this provider. /// This is applied to all the metrics from this provider. /// - /// + /// /// . /// ExemplarFilter to use. /// The supplied for chaining. diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs index a2612ad300a..ce99dd85f74 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -6,8 +6,12 @@ namespace OpenTelemetry.Metrics; /// -/// The AlignedHistogramBucketExemplarReservoir implementation. +/// AlignedHistogramBucketExemplarReservoir implementation. /// +/// +/// Specification: . +/// internal sealed class AlignedHistogramBucketExemplarReservoir : FixedSizeExemplarReservoir { public AlignedHistogramBucketExemplarReservoir(int numberOfBuckets) diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs index b6c7973b9d3..6939e0b96c9 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs @@ -10,19 +10,19 @@ namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES /// -/// An ExemplarFilter which makes no measurements eligible for being an Exemplar. -/// Using this ExemplarFilter is as good as disabling Exemplar feature. +/// An implementation which makes no measurements +/// eligible for becoming an . /// -/// +/// +/// +/// Specification: . +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else -/// -/// An ExemplarFilter which makes no measurements eligible for being an Exemplar. -/// Using this ExemplarFilter is as good as disabling Exemplar feature. -/// internal #endif sealed class AlwaysOffExemplarFilter : ExemplarFilter diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs index 7be5d04db0f..b81a144df8b 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs @@ -10,9 +10,14 @@ namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES /// -/// An ExemplarFilter which makes all measurements eligible for being an Exemplar. +/// An implementation which makes all measurements +/// eligible for becoming an . /// -/// +/// +/// +/// Specification: . +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index b3a862aa528..35f6042d4fc 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -11,9 +11,13 @@ namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES /// -/// Represents an Exemplar data. +/// Exemplar implementation. /// -/// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. +/// +/// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. +/// Specification: . +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs index 3af62b66c65..b0895a52b11 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs @@ -10,17 +10,18 @@ namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES /// -/// The base class for defining Exemplar Filter. +/// ExemplarFilter base implementation and contract. /// -/// +/// +/// +/// Specification: . +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else -/// -/// The base class for defining Exemplar Filter. -/// internal #endif abstract class ExemplarFilter diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs index fa1c50b98d5..8c86753e7a2 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -12,7 +12,7 @@ namespace OpenTelemetry.Metrics; /// /// Represents an Exemplar measurement. /// -/// +/// /// Measurement type. #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index 1a19719bbfa..9fd1fc1b9f8 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -4,8 +4,12 @@ namespace OpenTelemetry.Metrics; /// -/// The base class for defining Exemplar Reservoir. +/// ExemplarReservoir base implementation and contract. /// +/// +/// Specification: . +/// internal abstract class ExemplarReservoir { /// diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs index 781789184a8..5f5fdae68ad 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -13,7 +13,7 @@ namespace OpenTelemetry.Metrics; /// /// A read-only collection of s. /// -/// +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs index 930b9647bb5..34dc945fabb 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -4,8 +4,12 @@ namespace OpenTelemetry.Metrics; /// -/// The SimpleFixedSizeExemplarReservoir implementation. +/// SimpleFixedSizeExemplarReservoir implementation. /// +/// +/// Specification: . +/// internal sealed class SimpleFixedSizeExemplarReservoir : FixedSizeExemplarReservoir { private readonly Random random = new(); diff --git a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs index 6d176f8a169..915a050dc98 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs @@ -12,19 +12,20 @@ namespace OpenTelemetry.Metrics; #if EXPOSE_EXPERIMENTAL_FEATURES /// -/// An ExemplarFilter which makes those measurements eligible for being an Exemplar, -/// which are recorded in the context of a sampled parent activity (span). +/// An implementation which makes measurements +/// recorded in the context of a sampled (span) eligible +/// for becoming an . /// -/// +/// +/// +/// Specification: . +/// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else -/// -/// An ExemplarFilter which makes those measurements eligible for being an Exemplar, -/// which are recorded in the context of a sampled parent activity (span). -/// internal #endif sealed class TraceBasedExemplarFilter : ExemplarFilter diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 65a62c3eb4a..6a07a9b1e65 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -348,7 +348,7 @@ public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) /// /// Gets the exemplars associated with the metric point. /// - /// + /// /// . /// if exemplars exist; otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] From e7cbbbbf0d3db71662eb01cee5285ed613339ea2 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 27 Feb 2024 15:52:33 -0800 Subject: [PATCH 09/16] [sdk-metrics] Improve exemplar tests (#5393) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 5 +- src/OpenTelemetry/Metrics/Metric.cs | 13 +- src/OpenTelemetry/Metrics/MetricPoint.cs | 18 ++- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 16 +- .../Metrics/MetricStreamConfiguration.cs | 4 + .../Metrics/MetricExemplarTests.cs | 145 +++++++++++++----- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index fa4cefc7221..65c9c2c2df7 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -17,6 +17,7 @@ internal sealed class AggregatorStore internal readonly int CardinalityLimit; internal readonly bool EmitOverflowAttribute; internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; + internal readonly Func? ExemplarReservoirFactory; internal long DroppedMeasurements = 0; private static readonly string MetricPointCapHitFixMessage = "Consider opting in for the experimental SDK feature to emit all the throttled metrics under the overflow attribute by setting env variable OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE = true. You could also modify instrumentation to reduce the number of unique key/value pair combinations. Or use Views to drop unwanted tags. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."; @@ -59,7 +60,8 @@ internal AggregatorStore( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null) + ExemplarFilter? exemplarFilter = null, + Func? exemplarReservoirFactory = null) { this.name = metricStreamIdentity.InstrumentName; this.CardinalityLimit = cardinalityLimit; @@ -74,6 +76,7 @@ internal AggregatorStore( this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; this.StartTimeExclusive = DateTimeOffset.UtcNow; this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + this.ExemplarReservoirFactory = exemplarReservoirFactory; if (metricStreamIdentity.TagKeys == null) { this.updateLongCallback = this.UpdateLong; diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index cfd6b4e3463..ca82f8d4eaa 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -49,7 +49,8 @@ internal Metric( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null) + ExemplarFilter? exemplarFilter = null, + Func? exemplarReservoirFactory = null) { this.InstrumentIdentity = instrumentIdentity; @@ -155,7 +156,15 @@ internal Metric( throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - this.AggregatorStore = new AggregatorStore(instrumentIdentity, aggType, temporality, cardinalityLimit, emitOverflowAttribute, shouldReclaimUnusedMetricPoints, exemplarFilter); + this.AggregatorStore = new AggregatorStore( + instrumentIdentity, + aggType, + temporality, + cardinalityLimit, + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints, + exemplarFilter, + exemplarReservoirFactory); this.Temporality = temporality; } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 6a07a9b1e65..8e76e83778c 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -63,13 +63,25 @@ internal MetricPoint( this.ReferenceCount = 1; this.LookupData = lookupData; - ExemplarReservoir? reservoir = null; + var isExemplarEnabled = aggregatorStore!.IsExemplarEnabled(); + + ExemplarReservoir? reservoir; + try + { + reservoir = aggregatorStore.ExemplarReservoirFactory?.Invoke(); + } + catch + { + // TODO : Log that the factory on view threw an exception, once view exposes that capability + reservoir = null; + } + if (this.aggType == AggregationType.HistogramWithBuckets || this.aggType == AggregationType.HistogramWithMinMaxBuckets) { this.mpComponents = new MetricPointOptionalComponents(); this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); - if (aggregatorStore!.IsExemplarEnabled()) + if (isExemplarEnabled && reservoir == null) { reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds!.Length); } @@ -91,7 +103,7 @@ internal MetricPoint( this.mpComponents = null; } - if (aggregatorStore!.IsExemplarEnabled() && reservoir == null) + if (isExemplarEnabled && reservoir == null) { reservoir = new SimpleFixedSizeExemplarReservoir(DefaultSimpleReservoirPoolSize); } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 9f3b6fa10d2..6b78958e6a5 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -147,14 +147,14 @@ internal virtual List AddMetricWithViews(Instrument instrument, List? ExemplarReservoirFactory { get; set; } + internal string[]? CopiedTagKeys { get; private set; } internal int? ViewId { get; set; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index 356e4ac8acb..885e1e725fd 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -1,23 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + using System.Diagnostics; using System.Diagnostics.Metrics; using OpenTelemetry.Tests; using Xunit; -using Xunit.Abstractions; namespace OpenTelemetry.Metrics.Tests; public class MetricExemplarTests : MetricTestsBase { private const int MaxTimeToAllowForFlush = 10000; - private readonly ITestOutputHelper output; - - public MetricExemplarTests(ITestOutputHelper output) - { - this.output = output; - } [Theory] [InlineData(MetricReaderTemporalityPreference.Cumulative)] @@ -33,15 +28,21 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView( + "testCounter", + new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(2, false, null); foreach (var value in measurementValues) { - counter.Add(value); + counter.Add(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -49,14 +50,9 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - // TODO: Modify the test to better test cumulative. - // In cumulative, where SimpleFixedSizeExemplarReservoir's size is - // more than the count of new measurements, it is possible - // that the exemplar value is for a measurement that was recorded in the prior - // cycle. The current ValidateExemplars() does not handle this case. - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false); + var exemplars = GetExemplars(metricPoint.Value); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); exportedItems.Clear(); @@ -64,12 +60,11 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) { - var act = new Activity("test").Start(); - counter.Add(value); - act.Stop(); + using var act = new Activity("test").Start(); + counter.Add(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -77,12 +72,29 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + // Current design: + // First collect we saw Exemplar A & B + // Second collect we saw Exemplar C but B remained in the reservoir + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1).Take(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); } - [Fact] - public void TestExemplarsHistogram() + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality) { DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); @@ -90,18 +102,30 @@ public void TestExemplarsHistogram() using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); var histogram = meter.CreateHistogram("testHistogram"); + var buckets = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView( + "testHistogram", + new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = buckets + /* 2000 is here to test overflow measurement */ + .Concat(new double[] { 2000 }) + .Select(b => (Value: b, ExpectTraceId: false)) + .ToArray(); foreach (var value in measurementValues) { - histogram.Record(value); + histogram.Record(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -109,8 +133,9 @@ public void TestExemplarsHistogram() Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); exportedItems.Clear(); @@ -118,11 +143,11 @@ public void TestExemplarsHistogram() Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray(); + foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - histogram.Record(value); + histogram.Record(value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -130,8 +155,20 @@ public void TestExemplarsHistogram() Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(11, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); } [Fact] @@ -152,10 +189,14 @@ public void TestExemplarsFilterTags() metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(10, false, null); foreach (var value in measurementValues) { - histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1")); + histogram.Record( + value.Value, + new("key1", "value1"), + new("key2", "value1"), + new("key3", "value1")); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -176,27 +217,45 @@ public void TestExemplarsFilterTags() } } - private static double[] GenerateRandomValues(int count) + private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( + int count, + bool expectTraceId, + (double Value, bool ExpectTraceId)[]? previousValues) { var random = new Random(); - var values = new double[count]; + var values = new (double, bool)[count]; for (int i = 0; i < count; i++) { - values[i] = random.NextDouble(); + var nextValue = random.NextDouble(); + if (values.Any(m => m.Item1 == nextValue) + || previousValues?.Any(m => m.Value == nextValue) == true) + { + i--; + continue; + } + + values[i] = (nextValue, expectTraceId); } return values; } - private static void ValidateExemplars(IReadOnlyList exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists) + private static void ValidateExemplars( + IReadOnlyList exemplars, + DateTimeOffset startTime, + DateTimeOffset endTime, + (double Value, bool ExpectTraceId)[] measurementValues) { - Assert.NotNull(exemplars); + int count = 0; + foreach (var exemplar in exemplars) { Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); - Assert.Contains(exemplar.DoubleValue, measurementValues); Assert.Equal(0, exemplar.FilteredTags.MaximumCount); - if (traceContextExists) + + var measurement = measurementValues.FirstOrDefault(v => v.Value == exemplar.DoubleValue); + Assert.NotEqual(default, measurement); + if (measurement.ExpectTraceId) { Assert.NotEqual(default, exemplar.TraceId); Assert.NotEqual(default, exemplar.SpanId); @@ -206,6 +265,10 @@ private static void ValidateExemplars(IReadOnlyList exemplars, DateTim Assert.Equal(default, exemplar.TraceId); Assert.Equal(default, exemplar.SpanId); } + + count++; } + + Assert.Equal(measurementValues.Length, count); } } From 278e246111bd1100e82d2a4195ce978ea95c2e40 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 28 Feb 2024 09:59:41 -0800 Subject: [PATCH 10/16] [sdk-metrics] Support exemplars when using exponential histograms (#5396) --- src/OpenTelemetry/CHANGELOG.md | 5 + src/OpenTelemetry/Metrics/MetricPoint.cs | 78 ++- .../Metrics/MetricExemplarTests.cs | 552 ++++++++++++++++-- .../Metrics/MetricTestsBase.cs | 2 +- 4 files changed, 548 insertions(+), 89 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 5cfbd35be90..755d94e0b3b 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -50,6 +50,11 @@ metrics exporters which support exemplars. ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) +* **Experimental (pre-release builds only):** Added support for exemplars when + using Base2 Exponential Bucket Histogram Aggregation configured via the View + API. + ([#5396](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5396)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 8e76e83778c..d9a3eef3a98 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -97,6 +97,10 @@ internal MetricPoint( { this.mpComponents = new MetricPointOptionalComponents(); this.mpComponents.Base2ExponentialBucketHistogram = new Base2ExponentialBucketHistogram(exponentialHistogramMaxSize, exponentialHistogramMaxScale); + if (isExemplarEnabled && reservoir == null) + { + reservoir = new SimpleFixedSizeExemplarReservoir(Math.Min(20, exponentialHistogramMaxSize)); + } } else { @@ -558,13 +562,13 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags, i)); - } + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, i)); } this.mpComponents.ReleaseLock(); @@ -1403,26 +1409,24 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan(number, tags, i)); - } - histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); } + if (reportExemplar && isSampled) + { + Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, i)); + } + this.mpComponents.ReleaseLock(); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) { if (number < 0) { @@ -1442,12 +1446,20 @@ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan(number, tags)); + } + this.mpComponents.ReleaseLock(); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) { if (number < 0) { @@ -1470,6 +1482,16 @@ private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySp histogram.RunningMax = Math.Max(histogram.RunningMax, number); } + if (reportExemplar && isSampled) + { + Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags)); + } + this.mpComponents.ReleaseLock(); } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index 885e1e725fd..e1dd5effe54 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -23,17 +23,24 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var counter = meter.CreateCounter("testCounter"); + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView( - "testCounter", - new MetricStreamConfiguration + .AddView(i => + { + if (i.Name.StartsWith("testCounter")) { - ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), - }) + return new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + + return null; + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; @@ -42,17 +49,14 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) var measurementValues = GenerateRandomValues(2, false, null); foreach (var value in measurementValues) { - counter.Add(value.Value); + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); + ValidateFirstPhase("testCounterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testCounterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); exportedItems.Clear(); @@ -64,55 +68,194 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - counter.Add(value.Value); + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); + ValidateSecondPhase("testCounterDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.DoubleValue); + ValidateSecondPhase("testCounterLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.LongValue); - if (temporality == MetricReaderTemporalityPreference.Cumulative) + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) { - // Current design: - // First collect we saw Exemplar A & B - // Second collect we saw Exemplar C but B remained in the reservoir - Assert.Equal(2, exemplars.Count); - secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1).Take(1)).ToArray(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, getExemplarValueFunc); } - else + + void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues, + Func getExemplarValueFunc) { - Assert.Single(exemplars); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + // Current design: + // First collect we saw Exemplar A & B + // Second collect we saw Exemplar C but B remained in the reservoir + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, getExemplarValueFunc); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsObservable(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + (double Value, bool ExpectTraceId)[] measurementValues = new (double Value, bool ExpectTraceId)[] + { + (18D, false), + (19D, false), + }; + + int measurementIndex = 0; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var gaugeDouble = meter.CreateObservableGauge("testGaugeDouble", () => measurementValues[measurementIndex].Value); + var gaugeLong = meter.CreateObservableGauge("testGaugeLong", () => (long)measurementValues[measurementIndex].Value); + var counterDouble = meter.CreateObservableCounter("counterDouble", () => measurementValues[measurementIndex].Value); + var counterLong = meter.CreateObservableCounter("counterLong", () => (long)measurementValues[measurementIndex].Value); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + ValidateFirstPhase("counterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("counterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + exportedItems.Clear(); + + measurementIndex++; + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateSecondPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Take(1), getExemplarValueFunc); } - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); + static void ValidateSecondPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + // Note: Gauges are only observed when collection happens. For + // Cumulative & Delta the behavior will be the same. We will record the + // single measurement each time as the only exemplar. + + Assert.Single(exemplars); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Skip(1), getExemplarValueFunc); + } } [Theory] [InlineData(MetricReaderTemporalityPreference.Cumulative)] [InlineData(MetricReaderTemporalityPreference.Delta)] - public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality) + public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference temporality) { DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var histogram = meter.CreateHistogram("testHistogram"); + var histogramWithBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithBucketsAndMinMaxDouble"); + var histogramWithBucketsDouble = meter.CreateHistogram("histogramWithBucketsDouble"); + var histogramWithBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithBucketsAndMinMaxLong"); + var histogramWithBucketsLong = meter.CreateHistogram("histogramWithBucketsLong"); var buckets = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView( - "testHistogram", - new ExplicitBucketHistogramConfiguration + .AddView(i => + { + if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) { - Boundaries = buckets, - }) + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + RecordMinMax = false, + }; + } + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; @@ -125,17 +268,18 @@ public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality .ToArray(); foreach (var value in measurementValues) { - histogram.Record(value.Value); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsLong", testStartTime, exportedItems, measurementValues); exportedItems.Clear(); @@ -147,28 +291,314 @@ public void TestExemplarsHistogram(MetricReaderTemporalityPreference temporality foreach (var value in secondMeasurementValues) { using var act = new Activity("test").Start(); - histogram.Record(value.Value); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); + ValidateScondPhase("histogramWithBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); - if (temporality == MetricReaderTemporalityPreference.Cumulative) + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) { - Assert.Equal(11, exemplars.Count); - secondMeasurementValues = secondMeasurementValues.Concat(measurementValues.Skip(1)).ToArray(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); } - else + + static void ValidateScondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) { - Assert.Single(exemplars); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(11, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); } + } - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues); + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var histogramWithoutBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxDouble"); + var histogramWithoutBucketsDouble = meter.CreateHistogram("histogramWithoutBucketsDouble"); + var histogramWithoutBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxLong"); + var histogramWithoutBucketsLong = meter.CreateHistogram("histogramWithoutBucketsLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + RecordMinMax = false, + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(2, false, null); + foreach (var value in measurementValues) + { + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } + + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var exponentialHistogramWithMinMaxDouble = meter.CreateHistogram("exponentialHistogramWithMinMaxDouble"); + var exponentialHistogramDouble = meter.CreateHistogram("exponentialHistogramDouble"); + var exponentialHistogramWithMinMaxLong = meter.CreateHistogram("exponentialHistogramWithMinMaxLong"); + var exponentialHistogramLong = meter.CreateHistogram("exponentialHistogramLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + if (i.Name.StartsWith("exponentialHistogramWithMinMax")) + { + return new Base2ExponentialBucketHistogramConfiguration(); + } + else + { + return new Base2ExponentialBucketHistogramConfiguration() + { + RecordMinMax = false, + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(20, false, null); + foreach (var value in measurementValues) + { + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("exponentialHistogramWithMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramWithMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("exponentialHistogramWithMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramWithMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } + + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(20, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(19)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } } [Fact] @@ -226,9 +656,9 @@ private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( var values = new (double, bool)[count]; for (int i = 0; i < count; i++) { - var nextValue = random.NextDouble(); - if (values.Any(m => m.Item1 == nextValue) - || previousValues?.Any(m => m.Value == nextValue) == true) + var nextValue = random.NextDouble() * 100_000; + if (values.Any(m => m.Item1 == nextValue || m.Item1 == (long)nextValue) + || previousValues?.Any(m => m.Value == nextValue || m.Value == (long)nextValue) == true) { i--; continue; @@ -244,7 +674,8 @@ private static void ValidateExemplars( IReadOnlyList exemplars, DateTimeOffset startTime, DateTimeOffset endTime, - (double Value, bool ExpectTraceId)[] measurementValues) + IEnumerable<(double Value, bool ExpectTraceId)> measurementValues, + Func getExemplarValueFunc) { int count = 0; @@ -253,7 +684,8 @@ private static void ValidateExemplars( Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); Assert.Equal(0, exemplar.FilteredTags.MaximumCount); - var measurement = measurementValues.FirstOrDefault(v => v.Value == exemplar.DoubleValue); + var measurement = measurementValues.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar) + || (long)v.Value == getExemplarValueFunc(exemplar)); Assert.NotEqual(default, measurement); if (measurement.ExpectTraceId) { @@ -269,6 +701,6 @@ private static void ValidateExemplars( count++; } - Assert.Equal(measurementValues.Length, count); + Assert.Equal(measurementValues.Count(), count); } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 6d18bed47de..11c90c8d899 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -159,7 +159,7 @@ public static int GetNumberOfMetricPoints(List metrics) return count; } - public static MetricPoint? GetFirstMetricPoint(List metrics) + public static MetricPoint? GetFirstMetricPoint(IEnumerable metrics) { foreach (var metric in metrics) { From 05b0ca4521d04472e0682e24654260a335c22648 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 28 Feb 2024 10:56:29 -0800 Subject: [PATCH 11/16] [otlp-metrics] Support exemplars for all metric types (#5397) --- .../CHANGELOG.md | 5 + .../Implementation/MetricItemExtensions.cs | 95 ++++++-- .../OtlpMetricsExporterTests.cs | 226 ++++++++++++++++-- 3 files changed, 279 insertions(+), 47 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index f862133e31f..d5d71920bd0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -30,6 +30,11 @@ as it is mandated by the specification. ([#5316](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5268)) +* **Experimental (pre-release builds only):** Add support in + `OtlpMetricExporter` for emitting exemplars supplied on Counters, Gauges, and + ExponentialHistograms. + ([#5397](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5397)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 53276e617e0..fb9266cb613 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -158,6 +158,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsInt = metricPoint.GetSumLong(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } + } + sum.DataPoints.Add(dataPoint); } @@ -185,6 +195,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsDouble = metricPoint.GetSumDouble(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } + sum.DataPoints.Add(dataPoint); } @@ -206,6 +226,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsInt = metricPoint.GetGaugeLastValueLong(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } + } + gauge.DataPoints.Add(dataPoint); } @@ -227,6 +257,16 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) AddAttributes(metricPoint.Tags, dataPoint.Attributes); dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } + gauge.DataPoints.Add(dataPoint); } @@ -320,7 +360,14 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) dataPoint.Positive.BucketCounts.Add((ulong)bucketCount); } - // TODO: exemplars. + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } + } histogram.DataPoints.Add(dataPoint); } @@ -333,29 +380,7 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) return otlpMetric; } - private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) - { - foreach (var tag in tags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } - - private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) - { - foreach (var tag in meterTags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } - - private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + internal static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) where T : struct { var otlpExemplar = new OtlpMetrics.Exemplar @@ -399,4 +424,26 @@ private static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exempl return otlpExemplar; } + + private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + { + foreach (var tag in tags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } + } + } + + private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) + { + foreach (var tag in meterTags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } + } + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 85713222ea6..45479f92ae9 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -1,8 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Diagnostics.Metrics; using System.Reflection; +using Google.Protobuf; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -18,6 +20,12 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests { + private static readonly KeyValuePair[] KeyValues = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", 123), + }; + [Fact] public void TestAddOtlpExporter_SetsCorrectMetricReaderDefaults() { @@ -216,14 +224,17 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) [Theory] [InlineData("test_gauge", null, null, 123L, null)] [InlineData("test_gauge", null, null, null, 123.45)] + [InlineData("test_gauge", null, null, 123L, null, true)] + [InlineData("test_gauge", null, null, null, 123.45, true)] [InlineData("test_gauge", "description", "unit", 123L, null)] - public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue) + public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics) .Build(); @@ -277,29 +288,35 @@ public void TestGaugeToOtlpMetric(string name, string description, string unit, Assert.Empty(dataPoint.Attributes); - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateCounter(name, unit, description); @@ -366,31 +383,37 @@ public void TestCounterToOtlpMetric(string name, string description, string unit Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_counter", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateUpDownCounter(name, unit, description); @@ -457,24 +480,30 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -485,7 +514,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -587,31 +616,41 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (enableExemplars) + { + VerifyExemplars(null, 0, enableExemplars, d => d.Exemplars.Skip(1).FirstOrDefault(), dataPoint); + } } [Theory] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) { var metrics = new List(); using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; }) .Build(); - var attributes = ToAttributes(keysValues).ToArray(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -690,7 +729,7 @@ public void TestHistogramToOtlpMetric(string name, string description, string un Assert.Empty(dataPoint.Attributes); } - Assert.Empty(dataPoint.Exemplars); + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); } [Theory] @@ -737,14 +776,155 @@ public void TestTemporalityPreferenceConfiguration(string configValue, MetricRea Assert.Equal(expectedTemporality, temporality); } - private static IEnumerable> ToAttributes(object[] keysValues) + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) { - var keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray(); - var values = keysValues?.Where((_, index) => index % 2 != 0).ToArray(); + ActivitySource activitySource = null; + Activity activity = null; + TracerProvider tracerProvider = null; + + using var meter = new Meter(Utils.GetCurrentMethodName()); + + var exportedItems = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .AddView(i => + { + return !enableTagFiltering + ? null + : new MetricStreamConfiguration + { + TagKeys = Array.Empty(), + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + if (enableTracing) + { + activitySource = new ActivitySource(Utils.GetCurrentMethodName()); + tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySource.Name) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + activity = activitySource.StartActivity("testActivity"); + } + + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); + + counterDouble.Add(1.18D, new KeyValuePair("key1", "value1")); + counterLong.Add(18L, new KeyValuePair("key1", "value1")); + + meterProvider.ForceFlush(); + + var counterDoubleMetric = exportedItems.FirstOrDefault(m => m.Name == counterDouble.Name); + var counterLongMetric = exportedItems.FirstOrDefault(m => m.Name == counterLong.Name); - for (var i = 0; keys != null && i < keys.Length; ++i) + Assert.NotNull(counterDoubleMetric); + Assert.NotNull(counterLongMetric); + + AssertExemplars(1.18D, counterDoubleMetric); + AssertExemplars(18L, counterLongMetric); + + activity?.Dispose(); + tracerProvider?.Dispose(); + activitySource?.Dispose(); + + void AssertExemplars(T value, Metric metric) + where T : struct + { + var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); + Assert.True(metricPointEnumerator.MoveNext()); + + ref readonly var metricPoint = ref metricPointEnumerator.Current; + + var result = metricPoint.TryGetExemplars(out var exemplars); + Assert.True(result); + + var exemplarEnumerator = exemplars.Value.GetEnumerator(); + Assert.True(exemplarEnumerator.MoveNext()); + + ref readonly var exemplar = ref exemplarEnumerator.Current; + + var otlpExemplar = MetricItemExtensions.ToOtlpExemplar(value, in exemplar); + Assert.NotNull(otlpExemplar); + + Assert.NotEqual(default, otlpExemplar.TimeUnixNano); + if (!enableTracing) + { + Assert.Equal(ByteString.Empty, otlpExemplar.TraceId); + Assert.Equal(ByteString.Empty, otlpExemplar.SpanId); + } + else + { + byte[] traceIdBytes = new byte[16]; + activity.TraceId.CopyTo(traceIdBytes); + + byte[] spanIdBytes = new byte[8]; + activity.SpanId.CopyTo(spanIdBytes); + + Assert.Equal(ByteString.CopyFrom(traceIdBytes), otlpExemplar.TraceId); + Assert.Equal(ByteString.CopyFrom(spanIdBytes), otlpExemplar.SpanId); + } + + if (typeof(T) == typeof(long)) + { + Assert.Equal((long)(object)value, exemplar.LongValue); + } + else if (typeof(T) == typeof(double)) + { + Assert.Equal((double)(object)value, exemplar.DoubleValue); + } + else + { + Debug.Fail("Unexpected type"); + } + + if (!enableTagFiltering) + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.False(tagEnumerator.MoveNext()); + } + else + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.True(tagEnumerator.MoveNext()); + + var tag = tagEnumerator.Current; + Assert.Equal("key1", tag.Key); + Assert.Equal("value1", tag.Value); + } + } + } + + private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) + { + var exemplar = getExemplarFunc(state); + + if (enableExemplars) + { + Assert.NotNull(exemplar); + Assert.NotEqual(default, exemplar.TimeUnixNano); + if (longValue.HasValue) + { + Assert.Equal(longValue.Value, exemplar.AsInt); + } + else + { + Assert.Equal(doubleValue.Value, exemplar.AsDouble); + } + } + else { - yield return new KeyValuePair(keys[i].ToString(), values[i]); + Assert.Null(exemplar); } } } From 7ce0c55260ab563e09923933f2ef44c0497aa97d Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Wed, 28 Feb 2024 11:15:13 -0800 Subject: [PATCH 12/16] [sdk-metrics] Refactor duplicated update completion code into a helper in MetricPoint (#5398) --- src/OpenTelemetry/Metrics/MetricPoint.cs | 94 +++++++----------------- 1 file changed, 26 insertions(+), 68 deletions(-) diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index d9a3eef3a98..8f2f3440c63 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -445,23 +445,7 @@ internal void Update(long number) } } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; - - if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) - { - Interlocked.Decrement(ref this.ReferenceCount); - } + this.CompleteUpdate(); } internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) @@ -573,23 +557,7 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) @@ -785,23 +737,7 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan Date: Wed, 28 Feb 2024 16:40:50 -0800 Subject: [PATCH 13/16] [sdk-metrics] Refactor exemplar offer duplicated code into helper methods inside MetricPoint (#5399) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 4 +- src/OpenTelemetry/Metrics/MetricPoint.cs | 203 +++++++------------ 2 files changed, 79 insertions(+), 128 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 65c9c2c2df7..525d3ba7566 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -996,7 +996,7 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -484,15 +476,7 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -505,15 +489,7 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -522,37 +498,37 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -661,15 +629,7 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -685,15 +645,7 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); @@ -702,37 +654,37 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) { Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); @@ -1257,20 +1209,12 @@ private void UpdateHistogram(double number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); } - private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) { Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); @@ -1286,26 +1230,18 @@ private void UpdateHistogramWithMinMax(double number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); } - private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool isSampled = false) { Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); var histogramBuckets = this.mpComponents!.HistogramBuckets; - int i = histogramBuckets!.FindBucketIndex(number); + int bucketIndex = histogramBuckets!.FindBucketIndex(number); this.mpComponents.AcquireLock(); @@ -1313,29 +1249,21 @@ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan(number, tags, i)); - } + this.OfferExplicitBucketHistogramExemplarIfSampled(number, tags, bucketIndex, isSampled); this.mpComponents.ReleaseLock(); } - private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) { Debug.Assert(this.mpComponents?.HistogramBuckets != null, "histogramBuckets was null"); var histogramBuckets = this.mpComponents!.HistogramBuckets; - int i = histogramBuckets!.FindBucketIndex(number); + int bucketIndex = histogramBuckets!.FindBucketIndex(number); this.mpComponents.AcquireLock(); @@ -1343,26 +1271,18 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan(number, tags, i)); - } + this.OfferExplicitBucketHistogramExemplarIfSampled(number, tags, bucketIndex, isSampled); this.mpComponents.ReleaseLock(); } - private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) { if (number < 0) { @@ -1382,20 +1302,12 @@ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan(number, tags)); - } + this.OfferExemplarIfSampled(number, tags, isSampled); this.mpComponents.ReleaseLock(); } - private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) { if (number < 0) { @@ -1418,17 +1330,56 @@ private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySp histogram.RunningMax = Math.Max(histogram.RunningMax, number); } - if (reportExemplar && isSampled) + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void OfferExemplarIfSampled(T number, ReadOnlySpan> tags, bool isSampled) + where T : struct + { + if (isSampled) { - Debug.Assert(this.mpComponents.ExemplarReservoir != null, "ExemplarReservoir was null"); + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); // TODO: Need to ensure that the lock is always released. // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. - this.mpComponents.ExemplarReservoir!.Offer( - new ExemplarMeasurement(number, tags)); + if (typeof(T) == typeof(long)) + { + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement((long)(object)number, tags)); + } + else if (typeof(T) == typeof(double)) + { + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement((double)(object)number, tags)); + } + else + { + Debug.Fail("Unexpected type"); + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(Convert.ToDouble(number), tags)); + } } + } - this.mpComponents.ReleaseLock(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void OfferExplicitBucketHistogramExemplarIfSampled( + double number, + ReadOnlySpan> tags, + int bucketIndex, + bool isSampled) + { + if (isSampled) + { + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, bucketIndex)); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 42ecd73bd0956ae41e67894b93d1e3a89a4bbba8 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Thu, 29 Feb 2024 13:29:45 -0800 Subject: [PATCH 14/16] [sdk-metrics] ReadOnlyExemplarCollection tweaks (#5403) --- .../Experimental/PublicAPI.Unshipped.txt | 2 +- .../Exemplar/ReadOnlyExemplarCollection.cs | 28 ++++++--- src/OpenTelemetry/Metrics/MetricPoint.cs | 58 +++++++++---------- .../Metrics/MetricPointOptionalComponents.cs | 4 +- .../OtlpMetricsExporterTests.cs | 2 +- .../Metrics/MetricTestsBase.cs | 2 +- 6 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index b530cdf98f6..c174b5b3d63 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -23,7 +23,7 @@ OpenTelemetry.Metrics.ExemplarMeasurement OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T -OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection? exemplars) -> bool +OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection exemplars) -> bool OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void OpenTelemetry.Metrics.ReadOnlyExemplarCollection diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs index 5f5fdae68ad..4a43bf3ebc6 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -23,6 +23,7 @@ namespace OpenTelemetry.Metrics; #endif readonly struct ReadOnlyExemplarCollection { + internal static readonly ReadOnlyExemplarCollection Empty = new(Array.Empty()); private readonly Exemplar[] exemplars; internal ReadOnlyExemplarCollection(Exemplar[] exemplars) @@ -50,24 +51,37 @@ public Enumerator GetEnumerator() internal ReadOnlyExemplarCollection Copy() { - var exemplarCopies = new Exemplar[this.exemplars.Length]; + var maximumCount = this.MaximumCount; - int i = 0; - foreach (ref readonly var exemplar in this) + if (maximumCount > 0) { - exemplar.Copy(ref exemplarCopies[i++]); + var exemplarCopies = new Exemplar[maximumCount]; + + int i = 0; + foreach (ref readonly var exemplar in this) + { + if (exemplar.IsUpdated()) + { + exemplar.Copy(ref exemplarCopies[i++]); + } + } + + return new ReadOnlyExemplarCollection(exemplarCopies); } - return new ReadOnlyExemplarCollection(exemplarCopies); + return Empty; } internal IReadOnlyList ToReadOnlyList() { var list = new List(this.MaximumCount); - foreach (var item in this) + foreach (var exemplar in this) { - list.Add(item); + // Note: If ToReadOnlyList is ever made public it should make sure + // to take copies of exemplars or make sure the instance was first + // copied using the Copy method above. + list.Add(exemplar); } return list; diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 5be79f13942..2ccba6a307a 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -373,10 +372,10 @@ public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal #endif - readonly bool TryGetExemplars([NotNullWhen(true)] out ReadOnlyExemplarCollection? exemplars) + readonly bool TryGetExemplars(out ReadOnlyExemplarCollection exemplars) { - exemplars = this.mpComponents?.Exemplars; - return exemplars.HasValue; + exemplars = this.mpComponents?.Exemplars ?? ReadOnlyExemplarCollection.Empty; + return exemplars.MaximumCount > 0; } internal readonly MetricPoint Copy() @@ -945,13 +944,14 @@ internal void TakeSnapshot(bool outputDelta) internal void TakeSnapshotWithExemplar(bool outputDelta) { Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); + Debug.Assert(this.mpComponents!.ExemplarReservoir != null, "this.mpComponents.ExemplarReservoir was null"); switch (this.aggType) { case AggregationType.LongSumIncomingDelta: case AggregationType.LongSumIncomingCumulative: { - this.mpComponents!.AcquireLock(); + this.mpComponents.AcquireLock(); if (outputDelta) { @@ -965,7 +965,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsLong = this.runningValue.AsLong; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.mpComponents.ReleaseLock(); @@ -989,7 +989,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) this.snapshotValue.AsDouble = this.runningValue.AsDouble; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.mpComponents.ReleaseLock(); @@ -998,11 +998,11 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.LongGauge: { - this.mpComponents!.AcquireLock(); + this.mpComponents.AcquireLock(); this.snapshotValue.AsLong = this.runningValue.AsLong; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.mpComponents.ReleaseLock(); @@ -1011,11 +1011,11 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.DoubleGauge: { - this.mpComponents!.AcquireLock(); + this.mpComponents.AcquireLock(); this.snapshotValue.AsDouble = this.runningValue.AsDouble; this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.mpComponents.ReleaseLock(); @@ -1024,9 +1024,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.HistogramWithBuckets: { - Debug.Assert(this.mpComponents!.HistogramBuckets != null, "HistogramBuckets was null"); + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - var histogramBuckets = this.mpComponents!.HistogramBuckets!; + var histogramBuckets = this.mpComponents.HistogramBuckets!; this.mpComponents.AcquireLock(); @@ -1041,7 +1041,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; @@ -1052,9 +1052,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.Histogram: { - Debug.Assert(this.mpComponents!.HistogramBuckets != null, "HistogramBuckets was null"); + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - var histogramBuckets = this.mpComponents!.HistogramBuckets!; + var histogramBuckets = this.mpComponents.HistogramBuckets!; this.mpComponents.AcquireLock(); @@ -1067,7 +1067,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningSum = 0; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1077,9 +1077,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.HistogramWithMinMaxBuckets: { - Debug.Assert(this.mpComponents!.HistogramBuckets != null, "HistogramBuckets was null"); + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - var histogramBuckets = this.mpComponents!.HistogramBuckets!; + var histogramBuckets = this.mpComponents.HistogramBuckets!; this.mpComponents.AcquireLock(); @@ -1098,7 +1098,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.Snapshot(outputDelta); - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1108,9 +1108,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.HistogramWithMinMax: { - Debug.Assert(this.mpComponents!.HistogramBuckets != null, "HistogramBuckets was null"); + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - var histogramBuckets = this.mpComponents!.HistogramBuckets!; + var histogramBuckets = this.mpComponents.HistogramBuckets!; this.mpComponents.AcquireLock(); @@ -1127,7 +1127,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogramBuckets.RunningMax = double.NegativeInfinity; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1137,9 +1137,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.Base2ExponentialHistogram: { - Debug.Assert(this.mpComponents!.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + Debug.Assert(this.mpComponents.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; + var histogram = this.mpComponents.Base2ExponentialBucketHistogram!; this.mpComponents.AcquireLock(); @@ -1154,7 +1154,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogram.Reset(); } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); @@ -1164,9 +1164,9 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) case AggregationType.Base2ExponentialHistogramWithMinMax: { - Debug.Assert(this.mpComponents!.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + Debug.Assert(this.mpComponents.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; + var histogram = this.mpComponents.Base2ExponentialBucketHistogram!; this.mpComponents.AcquireLock(); @@ -1185,7 +1185,7 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) histogram.RunningMax = double.NegativeInfinity; } - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); this.MetricPointStatus = MetricPointStatus.NoCollectPending; this.mpComponents.ReleaseLock(); diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index 84511b1b549..440a9cc36b6 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -20,7 +20,7 @@ internal sealed class MetricPointOptionalComponents public ExemplarReservoir? ExemplarReservoir; - public ReadOnlyExemplarCollection? Exemplars; + public ReadOnlyExemplarCollection Exemplars = ReadOnlyExemplarCollection.Empty; private int isCriticalSectionOccupied = 0; @@ -30,7 +30,7 @@ public MetricPointOptionalComponents Copy() { HistogramBuckets = this.HistogramBuckets?.Copy(), Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy(), - Exemplars = this.Exemplars?.Copy(), + Exemplars = this.Exemplars.Copy(), }; return copy; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 45479f92ae9..1cf3d1c1778 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -849,7 +849,7 @@ void AssertExemplars(T value, Metric metric) var result = metricPoint.TryGetExemplars(out var exemplars); Assert.True(result); - var exemplarEnumerator = exemplars.Value.GetEnumerator(); + var exemplarEnumerator = exemplars.GetEnumerator(); Assert.True(exemplarEnumerator.MoveNext()); ref readonly var exemplar = ref exemplarEnumerator.Current; diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 11c90c8d899..7d72b773ea6 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -237,7 +237,7 @@ internal static IReadOnlyList GetExemplars(MetricPoint mp) { if (mp.TryGetExemplars(out var exemplars)) { - return exemplars.Value.ToReadOnlyList(); + return exemplars.ToReadOnlyList(); } return Array.Empty(); From b754b13cdba5e6d9edef2ab4e8bf43b917ff7296 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 1 Mar 2024 11:42:47 -0800 Subject: [PATCH 15/16] [sdk-metrics] ExemplarFilter updates to match latest specification (#5404) --- docs/metrics/customizing-the-sdk/README.md | 54 ++---- docs/metrics/extending-the-sdk/README.md | 39 +--- examples/AspNetCore/Program.cs | 2 +- .../Experimental/PublicAPI.Unshipped.txt | 22 +-- src/OpenTelemetry/CHANGELOG.md | 10 ++ .../Builder/MeterProviderBuilderExtensions.cs | 52 ++++-- .../Exemplar/AlwaysOffExemplarFilter.cs | 16 +- .../Exemplar/AlwaysOnExemplarFilter.cs | 19 +- .../Metrics/Exemplar/ExemplarFilter.cs | 16 +- .../Metrics/Exemplar/ExemplarFilterType.cs | 62 +++++++ .../Exemplar/TraceBasedExemplarFilter.cs | 16 +- test/Benchmarks/Metrics/ExemplarBenchmarks.cs | 169 +++++++++++++----- .../OtlpMetricsExporterTests.cs | 12 +- .../Program.cs | 2 +- .../Metrics/MetricExemplarTests.cs | 59 +++++- 15 files changed, 322 insertions(+), 228 deletions(-) create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index 14a72aaf2ac..4cb57f11693 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -412,26 +412,23 @@ exemplars. #### ExemplarFilter -`ExemplarFilter` determines which measurements are eligible to become an -Exemplar. i.e. `ExemplarFilter` determines which measurements are offered to -`ExemplarReservoir`, which makes the final decision about whether the offered -measurement gets stored as an exemplar. They can be used to control the noise -and overhead associated with Exemplar collection. +`ExemplarFilter` determines which measurements are offered to the configured +`ExemplarReservoir`, which makes the final decision about whether or not the +offered measurement gets recorded as an `Exemplar`. Generally `ExemplarFilter` +is a mechanism to control the overhead associated with `Exemplar` offering. -OpenTelemetry SDK comes with the following Filters: +OpenTelemetry SDK comes with the following `ExemplarFilters` (defined on +`ExemplarFilterType`): -* `AlwaysOnExemplarFilter` - makes all measurements eligible for being an Exemplar. -* `AlwaysOffExemplarFilter` - makes no measurements eligible for being an - Exemplar. Using this is as good as turning off Exemplar feature, and is the current +* `AlwaysOff`: Makes no measurements eligible for becoming an `Exemplar`. Using + this is as good as turning off the `Exemplar` feature and is the current default. -* `TraceBasedExemplarFilter` - makes those measurements eligible for being an -Exemplar, which are recorded in the context of a sampled parent `Activity` -(span). +* `AlwaysOn`: Makes all measurements eligible for becoming an `Exemplar`. +* `TraceBased`: Makes those measurements eligible for becoming an `Exemplar` + which are recorded in the context of a sampled `Activity` (span). -`SetExemplarFilter` method on `MeterProviderBuilder` can be used to set the -desired `ExemplarFilter`. - -The snippet below shows how to set `ExemplarFilter`. +The `SetExemplarFilter` extension method on `MeterProviderBuilder` can be used +to set the desired `ExemplarFilterType` and enable `Exemplar` collection: ```csharp using OpenTelemetry; @@ -439,31 +436,14 @@ using OpenTelemetry.Metrics; using var meterProvider = Sdk.CreateMeterProviderBuilder() // rest of config not shown - .SetExemplarFilter(new TraceBasedExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.TraceBased) .Build(); ``` -> [!NOTE] -> As of today, there is no separate toggle for enable/disable Exemplar feature. -Exemplars can be disabled by setting filter as `AlwaysOffExemplarFilter`, which -is also the default (i.e Exemplar feature is disabled by default). Users can -enable the feature by setting filter to anything other than -`AlwaysOffExemplarFilter`. For example: `.SetExemplarFilter(new TraceBasedExemplarFilter())`. - -If the built-in `ExemplarFilter`s are not meeting the needs, one may author -custom `ExemplarFilter` as shown -[here](../extending-the-sdk/README.md#exemplarfilter). A custom filter, which -eliminates all un-interesting measurements from becoming Exemplar is a -recommended way to control performance overhead associated with collecting -Exemplars. See -[benchmark](../../../test/Benchmarks/Metrics/ExemplarBenchmarks.cs) to see how -much impact can `ExemplarFilter` have on performance. - #### ExemplarReservoir -`ExemplarReservoir` receives the measurements sampled in by the `ExemplarFilter` -and is responsible for storing Exemplars. `ExemplarReservoir` ultimately decides -which measurements get stored as exemplars. The following are the default +`ExemplarReservoir` receives the measurements sampled by the `ExemplarFilter` +and is responsible for recording `Exemplar`s. The following are the default reservoirs: * `AlignedHistogramBucketExemplarReservoir` is the default reservoir used for @@ -479,7 +459,7 @@ size (currently defaulting to 1) determines the maximum number of exemplars stored. > [!NOTE] -> Currently there is no ability to change or configure Reservoir. +> Currently there is no ability to change or configure `ExemplarReservoir`. ### Instrumentation diff --git a/docs/metrics/extending-the-sdk/README.md b/docs/metrics/extending-the-sdk/README.md index 28df4c467c5..c7293ac418a 100644 --- a/docs/metrics/extending-the-sdk/README.md +++ b/docs/metrics/extending-the-sdk/README.md @@ -74,44 +74,7 @@ Not supported. ## ExemplarFilter -OpenTelemetry .NET SDK has provided the following built-in `ExemplarFilter`s: - -* [AlwaysOnExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs) -* [AlwaysOffExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs) -* [TraceBasedExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs) - -Custom exemplar filters can be implemented to achieve filtering based on other criterion: - -* `ExemplarFilter` should derive from `OpenTelemetry.ExemplarFilter` (which - belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package) - and implement the `ShouldSample` method. - -One example is a filter, which filters all measurements of value lower -than given threshold is given below. Such a filter prevents any measurements -below the given threshold from ever becoming a `Exemplar`. Such filters could -also incorporate the `TraceBasedExemplarFilter` condition as well, as storing -exemplars for non-sampled traces may be undesired. - -```csharp -public sealed class HighValueFilter : ExemplarFilter -{ - private readonly double maxValue; - - public HighValueFilter(double maxValue) - { - this.maxValue = maxValue; - } - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded && value > this.maxValue; - } - - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded && value > this.maxValue; - } -} -``` +Not supported. ## ExemplarReservoir diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs index e41e068b407..18279940976 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -85,7 +85,7 @@ builder .AddMeter(Instrumentation.MeterName) #if EXPOSE_EXPERIMENTAL_FEATURES - .SetExemplarFilter(new TraceBasedExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.TraceBased) #endif .AddRuntimeInstrumentation() .AddHttpClientInstrumentation() diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index c174b5b3d63..8c1a746f041 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -5,10 +5,6 @@ OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverit OpenTelemetry.Logs.LogRecord.Severity.set -> void OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? OpenTelemetry.Logs.LogRecord.SeverityText.set -> void -OpenTelemetry.Metrics.AlwaysOffExemplarFilter -OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void -OpenTelemetry.Metrics.AlwaysOnExemplarFilter -OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void OpenTelemetry.Metrics.Exemplar OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double OpenTelemetry.Metrics.Exemplar.Exemplar() -> void @@ -17,8 +13,10 @@ OpenTelemetry.Metrics.Exemplar.LongValue.get -> long OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Metrics.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void +OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOff = 0 -> OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOn = 1 -> OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.TraceBased = 2 -> OpenTelemetry.Metrics.ExemplarFilterType OpenTelemetry.Metrics.ExemplarMeasurement OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> @@ -33,8 +31,6 @@ OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Enumerator() -> void OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.MoveNext() -> bool OpenTelemetry.Metrics.ReadOnlyExemplarCollection.GetEnumerator() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator OpenTelemetry.Metrics.ReadOnlyExemplarCollection.ReadOnlyExemplarCollection() -> void -OpenTelemetry.Metrics.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void OpenTelemetry.ReadOnlyFilteredTagCollection OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair @@ -51,19 +47,11 @@ static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(thi static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilterType exemplarFilter = OpenTelemetry.Metrics.ExemplarFilterType.TraceBased) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 755d94e0b3b..9c8f3dc863f 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -55,6 +55,16 @@ API. ([#5396](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5396)) +* **Experimental (pre-release builds only):** Removed the `ExemplarFilter`, + `AlwaysOffExemplarFilter`, `AlwaysOnExemplarFilter`, and + `TraceBasedExemplarFilter` APIs. The `MeterProviderBuilder.SetExemplarFilter` + extension method now accepts an `ExemplarFilterType` enumeration (which + contains definitions for the supported filter types `AlwaysOff`, `AlwaysOn`, + and `TraceBased`) instead of an `ExemplarFilter` instance. This was done in + response to changes made to the [OpenTelemetry Metrics SDK + Specification](https://github.com/open-telemetry/opentelemetry-specification/pull/3820). + ([#5404](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5404)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index ee1de49604f..dbe132ae14b 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -319,36 +319,54 @@ public static MeterProvider Build(this MeterProviderBuilder meterProviderBuilder #if EXPOSE_EXPERIMENTAL_FEATURES /// - /// Sets the to be used for this provider. - /// This is applied to all the metrics from this provider. + /// Sets the to be used for this provider + /// which controls how measurements will be offered to exemplar reservoirs. + /// Default provider configuration: . /// - /// - /// . - /// ExemplarFilter to use. - /// The supplied for chaining. + /// + /// + /// Note: Use or to enable exemplars. + /// Specification: . + /// + /// . + /// to + /// use. + /// The supplied for + /// chaining. #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] #endif public #else - /// - /// Sets the to be used for this provider. - /// This is applied to all the metrics from this provider. - /// - /// . - /// ExemplarFilter to use. - /// The supplied for chaining. internal #endif - static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter) + static MeterProviderBuilder SetExemplarFilter( + this MeterProviderBuilder meterProviderBuilder, + ExemplarFilterType exemplarFilter = ExemplarFilterType.TraceBased) { - Guard.ThrowIfNull(exemplarFilter); - meterProviderBuilder.ConfigureBuilder((sp, builder) => { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter); + switch (exemplarFilter) + { + case ExemplarFilterType.AlwaysOn: + meterProviderBuilderSdk.SetExemplarFilter(new AlwaysOnExemplarFilter()); + break; + case ExemplarFilterType.AlwaysOff: + meterProviderBuilderSdk.SetExemplarFilter(new AlwaysOffExemplarFilter()); + break; + case ExemplarFilterType.TraceBased: + meterProviderBuilderSdk.SetExemplarFilter(new TraceBasedExemplarFilter()); + break; + default: + throw new NotSupportedException($"SdkExemplarFilter '{exemplarFilter}' is not supported."); + } } }); diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs index 6939e0b96c9..5f40db2de36 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs @@ -1,31 +1,17 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -using OpenTelemetry.Internal; -#endif - namespace OpenTelemetry.Metrics; -#if EXPOSE_EXPERIMENTAL_FEATURES /// /// An implementation which makes no measurements /// eligible for becoming an . /// /// -/// /// Specification: . /// -#if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif -public -#else -internal -#endif -sealed class AlwaysOffExemplarFilter : ExemplarFilter +internal sealed class AlwaysOffExemplarFilter : ExemplarFilter { /// public override bool ShouldSample(long value, ReadOnlySpan> tags) diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs index b81a144df8b..67f2e4ced5a 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs @@ -1,34 +1,17 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -using OpenTelemetry.Internal; -#endif - namespace OpenTelemetry.Metrics; -#if EXPOSE_EXPERIMENTAL_FEATURES /// /// An implementation which makes all measurements /// eligible for becoming an . /// /// -/// /// Specification: . /// -#if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif -public -#else -/// -/// An ExemplarFilter which makes all measurements eligible for being an Exemplar. -/// -internal -#endif - sealed class AlwaysOnExemplarFilter : ExemplarFilter +internal sealed class AlwaysOnExemplarFilter : ExemplarFilter { /// public override bool ShouldSample(long value, ReadOnlySpan> tags) diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs index b0895a52b11..20296b5540d 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs @@ -1,30 +1,16 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -using OpenTelemetry.Internal; -#endif - namespace OpenTelemetry.Metrics; -#if EXPOSE_EXPERIMENTAL_FEATURES /// /// ExemplarFilter base implementation and contract. /// /// -/// /// Specification: . /// -#if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif -public -#else -internal -#endif - abstract class ExemplarFilter +internal abstract class ExemplarFilter { /// /// Determines if a given measurement is eligible for being diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs new file mode 100644 index 00000000000..959d1f8e42f --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Defines the supported exemplar filters. +/// +/// +/// +/// Specification: . +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + enum ExemplarFilterType +{ + /// + /// An exemplar filter which makes no measurements eligible for becoming an + /// . + /// + /// + /// Note: Setting on a meter provider + /// effectively disables exemplars. + /// Specification: . + /// + AlwaysOff, + + /// + /// An exemplar filter which makes all measurements eligible for becoming an + /// . + /// + /// + /// Specification: . + /// + AlwaysOn, + + /// + /// An exemplar filter which makes measurements recorded in the context of a + /// sampled (span) eligible for becoming an . + /// + /// + /// Specification: . + /// + TraceBased, +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs index 915a050dc98..db1b16a0b15 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs @@ -1,34 +1,20 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -using OpenTelemetry.Internal; -#endif - using System.Diagnostics; namespace OpenTelemetry.Metrics; -#if EXPOSE_EXPERIMENTAL_FEATURES /// /// An implementation which makes measurements /// recorded in the context of a sampled (span) eligible /// for becoming an . /// /// -/// /// Specification: . /// -#if NET8_0_OR_GREATER -[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif -public -#else -internal -#endif - sealed class TraceBasedExemplarFilter : ExemplarFilter +internal sealed class TraceBasedExemplarFilter : ExemplarFilter { /// public override bool ShouldSample(long value, ReadOnlySpan> tags) diff --git a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs index 81d285ea8b4..ac1d347fba7 100644 --- a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs +++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs @@ -9,21 +9,31 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) -Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK 8.0.100 - [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - - -| Method | ExemplarFilter | Mean | Error | StdDev | Allocated | -|-------------------------- |--------------- |---------:|--------:|--------:|----------:| -| HistogramNoTagReduction | AlwaysOff | 274.2 ns | 2.94 ns | 2.60 ns | - | -| HistogramWithTagReduction | AlwaysOff | 241.6 ns | 1.78 ns | 1.58 ns | - | -| HistogramNoTagReduction | AlwaysOn | 300.9 ns | 3.10 ns | 2.90 ns | - | -| HistogramWithTagReduction | AlwaysOn | 312.9 ns | 4.81 ns | 4.50 ns | - | -| HistogramNoTagReduction | HighValueOnly | 262.8 ns | 2.24 ns | 1.99 ns | - | -| HistogramWithTagReduction | HighValueOnly | 258.3 ns | 5.12 ns | 5.03 ns | - | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3) +12th Gen Intel Core i9-12900HK, 1 CPU, 20 logical and 14 physical cores +.NET SDK 8.0.200 + [Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 + + +| Method | ExemplarConfiguration | Mean | Error | StdDev | Allocated | +|-------------------------- |---------------------- |---------:|--------:|--------:|----------:| +| HistogramNoTagReduction | AlwaysOff | 174.6 ns | 1.32 ns | 1.24 ns | - | +| HistogramWithTagReduction | AlwaysOff | 161.8 ns | 2.63 ns | 2.46 ns | - | +| CounterNoTagReduction | AlwaysOff | 141.6 ns | 2.12 ns | 1.77 ns | - | +| CounterWithTagReduction | AlwaysOff | 141.7 ns | 2.11 ns | 1.87 ns | - | +| HistogramNoTagReduction | AlwaysOn | 201.1 ns | 3.05 ns | 2.86 ns | - | +| HistogramWithTagReduction | AlwaysOn | 196.5 ns | 1.91 ns | 1.78 ns | - | +| CounterNoTagReduction | AlwaysOn | 149.7 ns | 1.42 ns | 1.33 ns | - | +| CounterWithTagReduction | AlwaysOn | 143.5 ns | 2.09 ns | 1.95 ns | - | +| HistogramNoTagReduction | TraceBased | 171.9 ns | 2.33 ns | 2.18 ns | - | +| HistogramWithTagReduction | TraceBased | 164.9 ns | 2.70 ns | 2.52 ns | - | +| CounterNoTagReduction | TraceBased | 148.1 ns | 2.76 ns | 2.58 ns | - | +| CounterWithTagReduction | TraceBased | 141.2 ns | 1.43 ns | 1.34 ns | - | +| HistogramNoTagReduction | Alway(...)pling [29] | 183.9 ns | 1.49 ns | 1.39 ns | - | +| HistogramWithTagReduction | Alway(...)pling [29] | 176.1 ns | 3.35 ns | 3.29 ns | - | +| CounterNoTagReduction | Alway(...)pling [29] | 159.3 ns | 3.12 ns | 4.38 ns | - | +| CounterWithTagReduction | Alway(...)pling [29] | 158.7 ns | 3.06 ns | 3.65 ns | - | */ namespace Benchmarks.Metrics; @@ -32,51 +42,74 @@ public class ExemplarBenchmarks { private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - private Histogram histogramWithoutTagReduction; - - private Histogram histogramWithTagReduction; - + private Histogram histogramWithoutTagReduction; + private Histogram histogramWithTagReduction; + private Counter counterWithoutTagReduction; + private Counter counterWithTagReduction; private MeterProvider meterProvider; private Meter meter; [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Test only.")] - public enum ExemplarFilterToUse + public enum ExemplarConfigurationType { AlwaysOff, AlwaysOn, - HighValueOnly, + TraceBased, + AlwaysOnWithHighValueSampling, } - [Params(ExemplarFilterToUse.AlwaysOn, ExemplarFilterToUse.AlwaysOff, ExemplarFilterToUse.HighValueOnly)] - public ExemplarFilterToUse ExemplarFilter { get; set; } + [Params(ExemplarConfigurationType.AlwaysOn, ExemplarConfigurationType.AlwaysOff, ExemplarConfigurationType.TraceBased, ExemplarConfigurationType.AlwaysOnWithHighValueSampling)] + public ExemplarConfigurationType ExemplarConfiguration { get; set; } [GlobalSetup] public void Setup() { this.meter = new Meter(Utils.GetCurrentMethodName()); - this.histogramWithoutTagReduction = this.meter.CreateHistogram("HistogramWithoutTagReduction"); - this.histogramWithTagReduction = this.meter.CreateHistogram("HistogramWithTagReduction"); + this.histogramWithoutTagReduction = this.meter.CreateHistogram("HistogramWithoutTagReduction"); + this.histogramWithTagReduction = this.meter.CreateHistogram("HistogramWithTagReduction"); + this.counterWithoutTagReduction = this.meter.CreateCounter("CounterWithoutTagReduction"); + this.counterWithTagReduction = this.meter.CreateCounter("CounterWithTagReduction"); var exportedItems = new List(); - ExemplarFilter exemplarFilter = new AlwaysOffExemplarFilter(); - if (this.ExemplarFilter == ExemplarFilterToUse.AlwaysOn) - { - exemplarFilter = new AlwaysOnExemplarFilter(); - } - else if (this.ExemplarFilter == ExemplarFilterToUse.HighValueOnly) - { - exemplarFilter = new HighValueExemplarFilter(); - } + var exemplarFilter = this.ExemplarConfiguration == ExemplarConfigurationType.TraceBased + ? ExemplarFilterType.TraceBased + : this.ExemplarConfiguration != ExemplarConfigurationType.AlwaysOff + ? ExemplarFilterType.AlwaysOn + : ExemplarFilterType.AlwaysOff; this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) .SetExemplarFilter(exemplarFilter) - .AddView("HistogramWithTagReduction", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddView(i => + { + if (i.Name.Contains("WithTagReduction")) + { + return new MetricStreamConfiguration() + { + TagKeys = new string[] { "DimName1", "DimName2", "DimName3" }, + ExemplarReservoirFactory = CreateExemplarReservoir, + }; + } + else + { + return new MetricStreamConfiguration() + { + ExemplarReservoirFactory = CreateExemplarReservoir, + }; + } + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; }) .Build(); + + ExemplarReservoir CreateExemplarReservoir() + { + return this.ExemplarConfiguration == ExemplarConfigurationType.AlwaysOnWithHighValueSampling + ? new HighValueExemplarReservoir(800D) + : null; + } } [GlobalCleanup] @@ -99,7 +132,7 @@ public void HistogramNoTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.histogramWithoutTagReduction.Record(random.Next(1000), tags); + this.histogramWithoutTagReduction.Record(random.NextDouble() * 1000D, tags); } [Benchmark] @@ -115,19 +148,71 @@ public void HistogramWithTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.histogramWithTagReduction.Record(random.Next(1000), tags); + this.histogramWithTagReduction.Record(random.NextDouble() * 1000D, tags); + } + + [Benchmark] + public void CounterNoTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList + { + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.counterWithoutTagReduction.Add(random.Next(1000), tags); + } + + [Benchmark] + public void CounterWithTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList + { + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.counterWithTagReduction.Add(random.Next(1000), tags); } - internal class HighValueExemplarFilter : ExemplarFilter + private sealed class HighValueExemplarReservoir : FixedSizeExemplarReservoir { - public override bool ShouldSample(long value, ReadOnlySpan> tags) + private readonly double threshold; + private int measurementCount; + + public HighValueExemplarReservoir(double threshold) + : base(10) + { + this.threshold = threshold; + } + + public override void Offer(in ExemplarMeasurement measurement) + { + if (measurement.Value >= this.threshold) + { + this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement); + } + } + + public override void Offer(in ExemplarMeasurement measurement) { - return value > 800; + if (measurement.Value >= this.threshold) + { + this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement); + } } - public override bool ShouldSample(double value, ReadOnlySpan> tags) + protected override void OnCollected() { - return value > 800; + this.measurementCount = 0; } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 1cf3d1c1778..7322c4d90e5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -234,7 +234,7 @@ public void TestGaugeToOtlpMetric(string name, string description, string unit, using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics) .Build(); @@ -309,7 +309,7 @@ public void TestCounterToOtlpMetric(string name, string description, string unit using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -406,7 +406,7 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -503,7 +503,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -643,7 +643,7 @@ public void TestHistogramToOtlpMetric(string name, string description, string un using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(enableExemplars ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -793,7 +793,7 @@ public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) using var meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { return !enableTagFiltering diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs index f43e4d12fe8..17360444c8e 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs @@ -56,7 +56,7 @@ public MetricsStressTest(MetricsStressTestOptions options) if (options.EnableExemplars) { - builder.SetExemplarFilter(new AlwaysOnExemplarFilter()); + builder.SetExemplarFilter(ExemplarFilterType.AlwaysOn); } if (options.AddViewToFilterTags) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index e1dd5effe54..c0927683f86 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -28,7 +28,7 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { if (i.Name.StartsWith("testCounter")) @@ -153,7 +153,7 @@ public void TestExemplarsObservable(MetricReaderTemporalityPreference temporalit using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; @@ -237,7 +237,7 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) @@ -367,7 +367,7 @@ public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreferen using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax")) @@ -495,7 +495,7 @@ public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { if (i.Name.StartsWith("exponentialHistogramWithMinMax")) @@ -601,6 +601,53 @@ static void ValidateSecondPhase( } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestTraceBasedExemplarFilter(bool enableTracing) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + var counter = meter.CreateCounter("testCounter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.TraceBased) + .AddInMemoryExporter(exportedItems)); + + if (enableTracing) + { + using var act = new Activity("test").Start(); + act.ActivityTraceFlags = ActivityTraceFlags.Recorded; + counter.Add(18); + } + else + { + counter.Add(18); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + + var metricPoint = GetFirstMetricPoint(exportedItems); + + Assert.NotNull(metricPoint); + + var exemplars = GetExemplars(metricPoint.Value); + + if (enableTracing) + { + Assert.Single(exemplars); + } + else + { + Assert.Empty(exemplars); + } + } + [Fact] public void TestExemplarsFilterTags() { @@ -612,7 +659,7 @@ public void TestExemplarsFilterTags() using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { From 97404a2ebd34ec9efac1b0fbd0d2824edd2072f1 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Fri, 1 Mar 2024 15:14:49 -0800 Subject: [PATCH 16/16] Clarify the recommendation regarding log category name (#5405) --- docs/logs/README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/logs/README.md b/docs/logs/README.md index 8172dd33569..77a46c847e8 100644 --- a/docs/logs/README.md +++ b/docs/logs/README.md @@ -107,6 +107,21 @@ Here is the rule of thumb: Minutes - Console Application](./getting-started-console/README.md) tutorial to learn more. +:heavy_check_mark: You should use dot-separated +[UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the log category +name, which makes it convenient to [filter logs](#log-filtering). A common +practice is to use fully qualified class name, and if further categorization is +desired, append a subcategory name. Refer to the [.NET official +document](https://learn.microsoft.com/dotnet/core/extensions/logging#log-category) +to learn more. + +```csharp +loggerFactory.CreateLogger(); // this is equivalent to CreateLogger("MyProduct.MyLibrary.MyClass") +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass"); // use the fully qualified class name +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.DatabaseOperations"); // append a subcategory name +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.FileOperations"); // append another subcategory name +``` + :stop_sign: You should avoid creating loggers too frequently. Although loggers are not super expensive, they still come with CPU and memory cost, and are meant to be reused throughout the application. Refer to the [logging performance @@ -186,11 +201,6 @@ instances if they are created by you. API invocation associated with the logger factory could become no-op (i.e. no logs will be emitted). -:heavy_check_mark: You should use the fully qualified class name as the log -category name. Refer to the [.NET official -document](https://learn.microsoft.com/dotnet/core/extensions/logging#log-category) -to learn more. - ## Log Correlation In OpenTelemetry, logs are automatically correlated to