From c87134ddc6bc65328a30c48171799d338ad81313 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 23 Feb 2024 09:18:48 -0800 Subject: [PATCH 01/31] [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 02/31] 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 03/31] [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 04/31] [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 05/31] [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 06/31] [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 07/31] [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 08/31] [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 09/31] [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 10/31] [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 11/31] [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 12/31] [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 13/31] [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 14/31] 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 From 28ead764b62171885f33f6d911f7aa17014b5e84 Mon Sep 17 00:00:00 2001 From: Yun-Ting Lin Date: Fri, 1 Mar 2024 17:04:51 -0800 Subject: [PATCH 15/31] Throw NotSupportedException when using `SetErrorStatusOnException` in Mono Runtime and Native AOT environment. (#5374) --- src/OpenTelemetry/CHANGELOG.md | 7 ++++++- .../Builder/TracerProviderBuilderExtensions.cs | 6 ++++++ src/OpenTelemetry/Trace/ExceptionProcessor.cs | 18 ++++++------------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 9c8f3dc863f..2958b18487d 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,12 +2,17 @@ ## Unreleased +* Throw NotSupportedException when using `SetErrorStatusOnException` method for + Tracing in Mono Runtime and Native AOT environment because the dependent + `Marshal.GetExceptionPointers()` API is not supported on these platforms. + ([#5374](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5374)) + * Fixed an issue where `LogRecord.Attributes` (or `LogRecord.StateValues` alias) could become out of sync with `LogRecord.State` if either is set directly via the public setters. This was done to further mitigate issues introduced in 1.5.0 causing attributes added using custom processor(s) to be missing after upgrading. For details see: - [#5169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5169) + ([#5169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5169)) * Fixed an issue where `SimpleExemplarReservoir` was not resetting internal state for cumulative temporality. diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs index 76f23be65f4..a4c39becf16 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs @@ -24,6 +24,12 @@ public static class TracerProviderBuilderExtensions /// . /// Enabled or not. Default value is true. /// Returns for chaining. + /// + /// This method is not supported in native AOT or Mono Runtime as of .NET 8. + /// +#if NET7_0_OR_GREATER + [RequiresDynamicCode("The code for detecting exception and setting error status might not be available.")] +#endif public static TracerProviderBuilder SetErrorStatusOnException(this TracerProviderBuilder tracerProviderBuilder, bool enabled = true) { tracerProviderBuilder.ConfigureBuilder((sp, builder) => diff --git a/src/OpenTelemetry/Trace/ExceptionProcessor.cs b/src/OpenTelemetry/Trace/ExceptionProcessor.cs index 815670792d1..f0084d36cfb 100644 --- a/src/OpenTelemetry/Trace/ExceptionProcessor.cs +++ b/src/OpenTelemetry/Trace/ExceptionProcessor.cs @@ -23,19 +23,13 @@ public ExceptionProcessor() #else // When running on netstandard or similar the Marshal class is not a part of the netstandard API // but it would still most likely be available in the underlying framework, so use reflection to retrieve it. - try - { - var flags = BindingFlags.Static | BindingFlags.Public; - var method = typeof(Marshal).GetMethod("GetExceptionPointers", flags, null, Type.EmptyTypes, null) - ?? throw new InvalidOperationException("Marshal.GetExceptionPointers method could not be resolved reflectively."); - var lambda = Expression.Lambda>(Expression.Call(method)); - this.fnGetExceptionPointers = lambda.Compile(); - } - catch (Exception ex) - { - throw new NotSupportedException($"'{typeof(Marshal).FullName}.GetExceptionPointers' is not supported", ex); - } + var flags = BindingFlags.Static | BindingFlags.Public; + var method = typeof(Marshal).GetMethod("GetExceptionPointers", flags, null, Type.EmptyTypes, null) + ?? throw new InvalidOperationException("Marshal.GetExceptionPointers method could not be resolved reflectively."); + var lambda = Expression.Lambda>(Expression.Call(method)); + this.fnGetExceptionPointers = lambda.Compile(); #endif + this.fnGetExceptionPointers(); // attempt to access pointers to test for platform support } /// From fc17a5a33d6e89360db52627f293ab1997d3ed47 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 4 Mar 2024 16:44:11 -0800 Subject: [PATCH 16/31] [sdk-metrics] Remove ExemplarFilter classes (#5408) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 246 +++++++----------- .../Builder/MeterProviderBuilderExtensions.cs | 6 +- .../Builder/MeterProviderBuilderSdk.cs | 6 +- .../Exemplar/AlwaysOffExemplarFilter.cs | 27 -- .../Exemplar/AlwaysOnExemplarFilter.cs | 27 -- .../Metrics/Exemplar/ExemplarFilter.cs | 50 ---- .../Exemplar/TraceBasedExemplarFilter.cs | 30 --- src/OpenTelemetry/Metrics/Metric.cs | 2 +- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 4 +- .../Metrics/AggregatorTestsBase.cs | 2 +- 10 files changed, 108 insertions(+), 292 deletions(-) delete mode 100644 src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs delete mode 100644 src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs delete mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs delete mode 100644 src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 525d3ba7566..547d7c61964 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -20,9 +20,9 @@ internal sealed class AggregatorStore internal readonly Func? ExemplarReservoirFactory; internal long DroppedMeasurements = 0; + private const ExemplarFilterType DefaultExemplarFilter = ExemplarFilterType.AlwaysOff; 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."; private static readonly Comparison> DimensionComparisonDelegate = (x, y) => x.Key.CompareTo(y.Key); - private static readonly ExemplarFilter DefaultExemplarFilter = new AlwaysOffExemplarFilter(); private readonly object lockZeroTags = new(); private readonly object lockOverflowTag = new(); @@ -44,7 +44,7 @@ internal sealed class AggregatorStore private readonly int exponentialHistogramMaxScale; private readonly UpdateLongDelegate updateLongCallback; private readonly UpdateDoubleDelegate updateDoubleCallback; - private readonly ExemplarFilter exemplarFilter; + private readonly ExemplarFilterType exemplarFilter; private readonly Func[], int, int> lookupAggregatorStore; private int metricPointIndex = 0; @@ -60,7 +60,7 @@ internal AggregatorStore( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null, + ExemplarFilterType? exemplarFilter = null, Func? exemplarReservoirFactory = null) { this.name = metricStreamIdentity.InstrumentName; @@ -75,7 +75,6 @@ internal AggregatorStore( this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize; this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; this.StartTimeExclusive = DateTimeOffset.UtcNow; - this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; this.ExemplarReservoirFactory = exemplarReservoirFactory; if (metricStreamIdentity.TagKeys == null) { @@ -93,6 +92,13 @@ internal AggregatorStore( this.EmitOverflowAttribute = emitOverflowAttribute; + this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + Debug.Assert( + this.exemplarFilter == ExemplarFilterType.AlwaysOff + || this.exemplarFilter == ExemplarFilterType.AlwaysOn + || this.exemplarFilter == ExemplarFilterType.TraceBased, + "this.exemplarFilter had an unexpected value"); + var reservedMetricPointsCount = 1; if (emitOverflowAttribute) @@ -144,17 +150,33 @@ internal bool IsExemplarEnabled() { // Using this filter to indicate On/Off // instead of another separate flag. - return this.exemplarFilter is not AlwaysOffExemplarFilter; + return this.exemplarFilter != ExemplarFilterType.AlwaysOff; } internal void Update(long value, ReadOnlySpan> tags) { - this.updateLongCallback(value, tags); + try + { + this.updateLongCallback(value, tags); + } + catch (Exception) + { + Interlocked.Increment(ref this.DroppedMeasurements); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } } internal void Update(double value, ReadOnlySpan> tags) { - this.updateDoubleCallback(value, tags); + try + { + this.updateDoubleCallback(value, tags); + } + catch (Exception) + { + Interlocked.Increment(ref this.DroppedMeasurements); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } } internal int Snapshot() @@ -924,177 +946,111 @@ private int RemoveStaleEntriesAndGetAvailableMetricPointRare(LookupData lookupDa private void UpdateLong(long value, ReadOnlySpan> tags) { - try - { - var index = this.FindMetricAggregatorsDefault(tags); - if (index < 0) - { - Interlocked.Increment(ref this.DroppedMeasurements); - - if (this.EmitOverflowAttribute) - { - this.InitializeOverflowTagPointIfNotInitialized(); - this.metricPoints[1].Update(value); - return; - } - else - { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } - - return; - } - } + var index = this.FindMetricAggregatorsDefault(tags); - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) - { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); - } - else - { - this.metricPoints[index].Update(value); - } - } - catch (Exception) - { - Interlocked.Increment(ref this.DroppedMeasurements); - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); - } + this.UpdateLongMetricPoint(index, value, tags: default); } private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) { - try - { - var index = this.FindMetricAggregatorsCustomTag(tags); - if (index < 0) - { - Interlocked.Increment(ref this.DroppedMeasurements); + var index = this.FindMetricAggregatorsCustomTag(tags); - if (this.EmitOverflowAttribute) - { - this.InitializeOverflowTagPointIfNotInitialized(); - this.metricPoints[1].Update(value); - return; - } - else - { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } + this.UpdateLongMetricPoint(index, value, tags); + } - return; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateLongMetricPoint(int metricPointIndex, long value, ReadOnlySpan> tags) + { + if (metricPointIndex < 0) + { + Interlocked.Increment(ref this.DroppedMeasurements); - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) + if (this.EmitOverflowAttribute) { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags, shouldSample); + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); } - else + else if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) { - this.metricPoints[index].Update(value); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); } + + return; } - catch (Exception) + + var exemplarFilterType = this.exemplarFilter; + if (exemplarFilterType == ExemplarFilterType.AlwaysOff) { - Interlocked.Increment(ref this.DroppedMeasurements); - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + this.metricPoints[metricPointIndex].Update(value); + } + else if (exemplarFilterType == ExemplarFilterType.AlwaysOn) + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: true); + } + else + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: Activity.Current?.Recorded ?? false); } } private void UpdateDouble(double value, ReadOnlySpan> tags) { - try - { - var index = this.FindMetricAggregatorsDefault(tags); - if (index < 0) - { - Interlocked.Increment(ref this.DroppedMeasurements); + var index = this.FindMetricAggregatorsDefault(tags); - if (this.EmitOverflowAttribute) - { - this.InitializeOverflowTagPointIfNotInitialized(); - this.metricPoints[1].Update(value); - return; - } - else - { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } - - return; - } - } - - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) - { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); - } - else - { - this.metricPoints[index].Update(value); - } - } - catch (Exception) - { - Interlocked.Increment(ref this.DroppedMeasurements); - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); - } + this.UpdateDoubleMetricPoint(index, value, tags: default); } private void UpdateDoubleCustomTags(double value, ReadOnlySpan> tags) { - try - { - var index = this.FindMetricAggregatorsCustomTag(tags); - if (index < 0) - { - Interlocked.Increment(ref this.DroppedMeasurements); + var index = this.FindMetricAggregatorsCustomTag(tags); - if (this.EmitOverflowAttribute) - { - this.InitializeOverflowTagPointIfNotInitialized(); - this.metricPoints[1].Update(value); - return; - } - else - { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } + this.UpdateDoubleMetricPoint(index, value, tags); + } - return; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateDoubleMetricPoint(int metricPointIndex, double value, ReadOnlySpan> tags) + { + if (metricPointIndex < 0) + { + Interlocked.Increment(ref this.DroppedMeasurements); - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) + if (this.EmitOverflowAttribute) { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags, shouldSample); + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); } - else + else if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) { - this.metricPoints[index].Update(value); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); } + + return; } - catch (Exception) + + var exemplarFilterType = this.exemplarFilter; + if (exemplarFilterType == ExemplarFilterType.AlwaysOff) { - Interlocked.Increment(ref this.DroppedMeasurements); - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + this.metricPoints[metricPointIndex].Update(value); + } + else if (exemplarFilterType == ExemplarFilterType.AlwaysOn) + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: true); + } + else + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: Activity.Current?.Recorded ?? false); } } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index dbe132ae14b..6a36562aa45 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -356,13 +356,9 @@ static MeterProviderBuilder SetExemplarFilter( 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()); + meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter); break; default: throw new NotSupportedException($"SdkExemplarFilter '{exemplarFilter}' is not supported."); diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs index a3ca2a15e56..11084ec7f0d 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs @@ -39,7 +39,7 @@ public MeterProviderBuilderSdk(IServiceProvider serviceProvider) public ResourceBuilder? ResourceBuilder { get; private set; } - public ExemplarFilter? ExemplarFilter { get; private set; } + public ExemplarFilterType? ExemplarFilter { get; private set; } public MeterProvider? Provider => this.meterProvider; @@ -145,10 +145,8 @@ public MeterProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) return this; } - public MeterProviderBuilder SetExemplarFilter(ExemplarFilter exemplarFilter) + public MeterProviderBuilder SetExemplarFilter(ExemplarFilterType exemplarFilter) { - Debug.Assert(exemplarFilter != null, "exemplarFilter was null"); - this.ExemplarFilter = exemplarFilter; return this; diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs deleted file mode 100644 index 5f40db2de36..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Metrics; - -/// -/// An implementation which makes no measurements -/// eligible for becoming an . -/// -/// -/// Specification: . -/// -internal sealed class AlwaysOffExemplarFilter : ExemplarFilter -{ - /// - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return false; - } - - /// - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return false; - } -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs deleted file mode 100644 index 67f2e4ced5a..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Metrics; - -/// -/// An implementation which makes all measurements -/// eligible for becoming an . -/// -/// -/// Specification: . -/// -internal sealed class AlwaysOnExemplarFilter : ExemplarFilter -{ - /// - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return true; - } - - /// - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return true; - } -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs deleted file mode 100644 index 20296b5540d..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Metrics; - -/// -/// ExemplarFilter base implementation and contract. -/// -/// -/// Specification: . -/// -internal abstract class ExemplarFilter -{ - /// - /// Determines if a given measurement is eligible for being - /// considered for becoming Exemplar. - /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// - /// Returns - /// true to indicate this measurement is eligible to become Exemplar - /// and will be given to an ExemplarReservoir. - /// Reservoir may further sample, so a true here does not mean that this - /// measurement will become an exemplar, it just means it'll be - /// eligible for being Exemplar. - /// false to indicate this measurement is not eligible to become Exemplar - /// and will not be given to the ExemplarReservoir. - /// - public abstract bool ShouldSample(long value, ReadOnlySpan> tags); - - /// - /// Determines if a given measurement is eligible for being - /// considered for becoming Exemplar. - /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// - /// Returns - /// true to indicate this measurement is eligible to become Exemplar - /// and will be given to an ExemplarReservoir. - /// Reservoir may further sample, so a true here does not mean that this - /// measurement will become an exemplar, it just means it'll be - /// eligible for being Exemplar. - /// false to indicate this measurement is not eligible to become Exemplar - /// and will not be given to the ExemplarReservoir. - /// - public abstract bool ShouldSample(double value, ReadOnlySpan> tags); -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs deleted file mode 100644 index db1b16a0b15..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; - -namespace OpenTelemetry.Metrics; - -/// -/// An implementation which makes measurements -/// recorded in the context of a sampled (span) eligible -/// for becoming an . -/// -/// -/// Specification: . -/// -internal sealed class TraceBasedExemplarFilter : ExemplarFilter -{ - /// - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded ?? false; - } - - /// - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded ?? false; - } -} diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index ca82f8d4eaa..32b107c5b2f 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -49,7 +49,7 @@ internal Metric( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null, + ExemplarFilterType? exemplarFilter = null, Func? exemplarReservoirFactory = null) { this.InstrumentIdentity = instrumentIdentity; diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 6b78958e6a5..629ef4c33a6 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -24,7 +24,7 @@ public abstract partial class MetricReader private int metricIndex = -1; private bool emitOverflowAttribute; - private ExemplarFilter? exemplarFilter; + private ExemplarFilterType? exemplarFilter; internal static void DeactivateMetric(Metric metric) { @@ -171,7 +171,7 @@ internal virtual List AddMetricWithViews(Instrument instrument, List Date: Tue, 5 Mar 2024 09:57:29 -0800 Subject: [PATCH 17/31] [sdk-metrics] Use FrozenSet on net8+ for custom tag lookup (#5409) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 11 +++++++++++ src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs | 7 +++++++ src/OpenTelemetry/Metrics/ThreadStaticStorage.cs | 7 +++++++ src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs | 11 +++++++++++ 4 files changed, 36 insertions(+) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 547d7c61964..e11eb9ee781 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -11,7 +14,11 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { +#if NET8_0_OR_GREATER + internal readonly FrozenSet? TagKeysInteresting; +#else internal readonly HashSet? TagKeysInteresting; +#endif internal readonly bool OutputDelta; internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; internal readonly int CardinalityLimit; @@ -85,7 +92,11 @@ internal AggregatorStore( { this.updateLongCallback = this.UpdateLongCustomTags; this.updateDoubleCallback = this.UpdateDoubleCustomTags; +#if NET8_0_OR_GREATER + var hs = FrozenSet.ToFrozenSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); +#else var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); +#endif this.TagKeysInteresting = hs; this.tagsKeysInterestingCount = hs.Count; } diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index 35f6042d4fc..1c2d27b99eb 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; #if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; @@ -27,7 +30,11 @@ namespace OpenTelemetry.Metrics; #endif struct Exemplar { +#if NET8_0_OR_GREATER + internal FrozenSet? ViewDefinedTagKeys; +#else internal HashSet? ViewDefinedTagKeys; +#endif private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, Array.Empty>(), count: 0); private int tagCount; diff --git a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs index 09063a7dd04..35f9be5b5da 100644 --- a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs +++ b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -54,7 +57,11 @@ internal void SplitToKeysAndValues( internal void SplitToKeysAndValues( ReadOnlySpan> tags, int tagLength, +#if NET8_0_OR_GREATER + FrozenSet tagKeysInteresting, +#else HashSet tagKeysInteresting, +#endif out KeyValuePair[]? tagKeysAndValues, out int actualLength) { diff --git a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs index 924b6e36f04..2c41020c767 100644 --- a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; #if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; @@ -25,12 +28,20 @@ namespace OpenTelemetry; #endif readonly struct ReadOnlyFilteredTagCollection { +#if NET8_0_OR_GREATER + private readonly FrozenSet? excludedKeys; +#else private readonly HashSet? excludedKeys; +#endif private readonly KeyValuePair[] tags; private readonly int count; internal ReadOnlyFilteredTagCollection( +#if NET8_0_OR_GREATER + FrozenSet? excludedKeys, +#else HashSet? excludedKeys, +#endif KeyValuePair[] tags, int count) { From 123c0b4b6909ddc0dadbff1dbc9a612ae5555993 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 5 Mar 2024 14:20:55 -0800 Subject: [PATCH 18/31] [sdk-metrics] Support setting exemplar filter from spec envvar key (#5412) --- src/OpenTelemetry/CHANGELOG.md | 7 ++ src/OpenTelemetry/Metrics/MeterProviderSdk.cs | 80 ++++++++++++++++--- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 17 ++-- ...nTelemetry.Extensions.Hosting.Tests.csproj | 1 + .../Metrics/MetricExemplarTests.cs | 41 ++++++++++ .../Metrics/MetricPointReclaimTestsBase.cs | 4 +- .../Shared/SkipUnlessTrueTheoryAttribute.cs | 33 ++++++++ 7 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 2958b18487d..b3e5a11e449 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -70,6 +70,13 @@ Specification](https://github.com/open-telemetry/opentelemetry-specification/pull/3820). ([#5404](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5404)) +* **Experimental (pre-release builds only):** The `ExemplarFilter` used by SDK + `MeterProvider`s can now be controlled via the `OTEL_METRICS_EXEMPLAR_FILTER` + environment variable. The supported values are: `always_off`, `always_on`, and + `trace_based`. For details see: [OpenTelemetry Environment Variable + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#exemplar). + ([#5412](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5412)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 430c7ecc0a0..22f49cb986a 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -13,16 +13,19 @@ namespace OpenTelemetry.Metrics; internal sealed class MeterProviderSdk : MeterProvider { + internal const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + internal const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + internal const string ExemplarFilterConfigKey = "OTEL_METRICS_EXEMPLAR_FILTER"; + internal readonly IServiceProvider ServiceProvider; internal readonly IDisposable? OwnedServiceProvider; internal int ShutdownCount; internal bool Disposed; - internal bool ShouldReclaimUnusedMetricPoints; + internal bool EmitOverflowAttribute; + internal bool ReclaimUnusedMetricPoints; + internal ExemplarFilterType? ExemplarFilter; internal Action? OnCollectObservableInstruments; - private const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; - private const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; - private readonly List instrumentations = new(); private readonly List> viewConfigs; private readonly object collectLock = new(); @@ -40,10 +43,6 @@ internal MeterProviderSdk( var state = serviceProvider!.GetRequiredService(); state.RegisterProvider(this); - var config = serviceProvider!.GetRequiredService(); - _ = config.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out bool isEmitOverflowAttributeKeySet); - _ = config.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ShouldReclaimUnusedMetricPoints); - this.ServiceProvider = serviceProvider!; if (ownsServiceProvider) @@ -54,14 +53,16 @@ internal MeterProviderSdk( OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Building MeterProvider."); - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Metric overflow attribute key set to: {isEmitOverflowAttributeKeySet}"); - var configureProviderBuilders = serviceProvider!.GetServices(); foreach (var configureProviderBuilder in configureProviderBuilders) { configureProviderBuilder.ConfigureBuilder(serviceProvider!, state); } + this.ExemplarFilter = state.ExemplarFilter; + + this.ApplySpecificationConfigurationKeys(serviceProvider!.GetRequiredService()); + StringBuilder exportersAdded = new StringBuilder(); StringBuilder instrumentationFactoriesAdded = new StringBuilder(); @@ -80,8 +81,9 @@ internal MeterProviderSdk( reader.ApplyParentProviderSettings( state.MetricLimit, state.CardinalityLimit, - state.ExemplarFilter, - isEmitOverflowAttributeKeySet); + this.EmitOverflowAttribute, + this.ReclaimUnusedMetricPoints, + this.ExemplarFilter); if (this.reader == null) { @@ -475,4 +477,58 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + + private void ApplySpecificationConfigurationKeys(IConfiguration configuration) + { + if (configuration.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out this.EmitOverflowAttribute)) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Overflow attribute feature enabled via configuration."); + } + + if (configuration.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ReclaimUnusedMetricPoints)) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Reclaim unused metric point feature enabled via configuration."); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue)) + { + if (this.ExemplarFilter.HasValue) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent( + $"Exemplar filter configuration value '{configValue}' has been ignored because a value '{this.ExemplarFilter}' was set programmatically."); + return; + } + + ExemplarFilterType? exemplarFilter; + if (string.Equals("always_off", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.AlwaysOff; + } + else if (string.Equals("always_on", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.AlwaysOn; + } + else if (string.Equals("trace_based", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.TraceBased; + } + else + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter configuration was found but the value '{configValue}' is invalid and will be ignored."); + return; + } + + this.ExemplarFilter = exemplarFilter; + + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter set to '{exemplarFilter}' from configuration."); + } +#else + if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue)) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent( + $"Exemplar filter configuration value '{configValue}' has been ignored because exemplars are an experimental feature not available in stable builds."); + } +#endif + } } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 629ef4c33a6..b64938e4c39 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -23,7 +23,7 @@ public abstract partial class MetricReader private Metric[]? metricsCurrentBatch; private int metricIndex = -1; private bool emitOverflowAttribute; - + private bool reclaimUnusedMetricPoints; private ExemplarFilterType? exemplarFilter; internal static void DeactivateMetric(Metric metric) @@ -72,8 +72,7 @@ internal virtual List AddMetricWithNoViews(Instrument instrument) Metric? metric = null; try { - bool shouldReclaimUnusedMetricPoints = this.parentProvider is MeterProviderSdk meterProviderSdk && meterProviderSdk.ShouldReclaimUnusedMetricPoints; - metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, this.emitOverflowAttribute, shouldReclaimUnusedMetricPoints, this.exemplarFilter); + metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, this.emitOverflowAttribute, this.reclaimUnusedMetricPoints, this.exemplarFilter); } catch (NotSupportedException nse) { @@ -145,14 +144,12 @@ internal virtual List AddMetricWithViews(Instrument instrument, List AddMetricWithViews(Instrument instrument, List 1) diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj index 8ae85542eba..1ea17a421de 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index c0927683f86..4c8a4c4153a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; @@ -13,6 +15,45 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricExemplarTests : MetricTestsBase { private const int MaxTimeToAllowForFlush = 10000; + private static readonly Func IsExemplarApiExposed = () => typeof(ExemplarFilterType).IsVisible; + + [SkipUnlessTrueTheory(typeof(MetricExemplarTests), nameof(IsExemplarApiExposed), "ExemplarFilter config tests skipped for stable builds")] + [InlineData(null, null, null)] + [InlineData(null, "always_off", (int)ExemplarFilterType.AlwaysOff)] + [InlineData(null, "ALWays_ON", (int)ExemplarFilterType.AlwaysOn)] + [InlineData(null, "trace_based", (int)ExemplarFilterType.TraceBased)] + [InlineData(null, "invalid", null)] + [InlineData((int)ExemplarFilterType.AlwaysOn, "trace_based", (int)ExemplarFilterType.AlwaysOn)] + public void TestExemplarFilterSetFromConfiguration( + int? programmaticValue, + string? configValue, + int? expectedValue) + { + var configBuilder = new ConfigurationBuilder(); + if (!string.IsNullOrEmpty(configValue)) + { + configBuilder.AddInMemoryCollection(new Dictionary + { + [MeterProviderSdk.ExemplarFilterConfigKey] = configValue, + }); + } + + using var container = this.BuildMeterProvider(out var meterProvider, b => + { + b.ConfigureServices( + s => s.AddSingleton(configBuilder.Build())); + + if (programmaticValue.HasValue) + { + b.SetExemplarFilter(((ExemplarFilterType?)programmaticValue).Value); + } + }); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + + Assert.NotNull(meterProviderSdk); + Assert.Equal((ExemplarFilterType?)expectedValue, meterProviderSdk.ExemplarFilter); + } [Theory] [InlineData(MetricReaderTemporalityPreference.Cumulative)] diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs index a10849c4057..fa33298643e 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs @@ -57,7 +57,7 @@ public void TestReclaimAttributeConfigWithEnvVar(string value, bool isReclaimAtt .Build(); var meterProviderSdk = meterProvider as MeterProviderSdk; - Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ReclaimUnusedMetricPoints); } [Theory] @@ -87,7 +87,7 @@ public void TestReclaimAttributeConfigWithOtherConfigProvider(string value, bool .Build(); var meterProviderSdk = meterProvider as MeterProviderSdk; - Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ShouldReclaimUnusedMetricPoints); + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ReclaimUnusedMetricPoints); } [Theory] diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs new file mode 100644 index 00000000000..087bff4366f --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Reflection; +using OpenTelemetry.Internal; +using Xunit; + +namespace OpenTelemetry.Tests; + +internal sealed class SkipUnlessTrueTheoryAttribute : TheoryAttribute +{ + public SkipUnlessTrueTheoryAttribute(Type typeContainingTest, string testFieldName, string skipMessage) + { + Guard.ThrowIfNull(typeContainingTest); + Guard.ThrowIfNullOrEmpty(testFieldName); + Guard.ThrowIfNullOrEmpty(skipMessage); + + var field = typeContainingTest.GetField(testFieldName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Static field '{testFieldName}' could not be found on '{typeContainingTest}' type."); + + if (field.FieldType != typeof(Func)) + { + throw new InvalidOperationException($"Field '{testFieldName}' on '{typeContainingTest}' type should be defined as '{typeof(Func)}'."); + } + + var testFunc = (Func)field.GetValue(null); + + if (!testFunc()) + { + this.Skip = skipMessage; + } + } +} From df2abe8c0ad55096ca27ad3d21186ae97a658183 Mon Sep 17 00:00:00 2001 From: Reiley Yang Date: Tue, 5 Mar 2024 15:52:07 -0800 Subject: [PATCH 19/31] Improve log filter example (#5406) --- docs/logs/customizing-the-sdk/Program.cs | 77 ++++++++++++++---------- docs/logs/customizing-the-sdk/README.md | 12 ++-- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/docs/logs/customizing-the-sdk/Program.cs b/docs/logs/customizing-the-sdk/Program.cs index cd631255b36..1b22fdbfda9 100644 --- a/docs/logs/customizing-the-sdk/Program.cs +++ b/docs/logs/customizing-the-sdk/Program.cs @@ -5,37 +5,52 @@ using OpenTelemetry.Logs; using OpenTelemetry.Resources; -namespace CustomizingTheSdk; - -public class Program +var loggerFactory = LoggerFactory.Create(builder => { - public static void Main() + builder.AddOpenTelemetry(logging => { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( - serviceName: "MyService", - serviceVersion: "1.0.0")); - options.AddConsoleExporter(); - }); - }); - - var logger = loggerFactory.CreateLogger(); - - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - logger.LogWarning("Hello from {name} {price}.", "tomato", 2.99); - logger.LogError("Hello from {name} {price}.", "tomato", 2.99); - - // log with scopes - using (logger.BeginScope(new List> - { - new KeyValuePair("store", "Seattle"), - })) - { - logger.LogInformation("Hello from {food} {price}.", "tomato", 2.99); - } - } + logging.IncludeScopes = true; + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( + serviceName: "MyService", + serviceVersion: "1.0.0")); + logging.AddConsoleExporter(); + }); +}); + +var logger = loggerFactory.CreateLogger(); + +logger.FoodPriceChanged("artichoke", 9.99); + +using (logger.BeginScope(new List> +{ + new KeyValuePair("store", "Seattle"), +})) +{ + logger.FoodPriceChanged("truffle", 999.99); +} + +logger.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + +// Dispose logger factory before the application ends. +// This will flush the remaining logs and shutdown the logging pipeline. +loggerFactory.Dispose(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Critical, "A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}).")] + public static partial void FoodRecallNotice( + this ILogger logger, + string brandName, + string productDescription, + string productType, + string recallReasonDescription, + string companyName); } diff --git a/docs/logs/customizing-the-sdk/README.md b/docs/logs/customizing-the-sdk/README.md index 1c2b20b9ed5..cd48fd608ab 100644 --- a/docs/logs/customizing-the-sdk/README.md +++ b/docs/logs/customizing-the-sdk/README.md @@ -43,9 +43,9 @@ It is not supported to add Processors after building the `LoggerFactory`. ```csharp var loggerFactory = LoggerFactory.Create(builder => { - builder.AddOpenTelemetry(options => + builder.AddOpenTelemetry(logging => { - options.AddProcessor(...) + logging.AddProcessor(...); }); }); ``` @@ -72,9 +72,9 @@ The snippet below shows configuring a custom `ResourceBuilder` to the provider. ```csharp var loggerFactory = LoggerFactory.Create(builder => { - builder.AddOpenTelemetry(options => + builder.AddOpenTelemetry(logging => { - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( serviceName: "MyService", serviceVersion: "1.0.0")); }); @@ -107,8 +107,8 @@ and also defines "Warning" as the minimum `LogLevel` for a user defined category These rules as defined only apply to the `OpenTelemetryLoggerProvider`. ```csharp -ILoggingBuilder.AddFilter("*", LogLevel.Error); -ILoggingBuilder.AddFilter("category name", LogLevel.Warning); +builder.AddFilter("*", LogLevel.Error); +builder.AddFilter("MyProduct.MyLibrary.MyClass", LogLevel.Warning); ``` ## Learn more From a7f34009778fae54a8b6623186a9876216634a12 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 5 Mar 2024 16:59:52 -0800 Subject: [PATCH 20/31] [sdk-metrics] Pass all tags supplied at measurement to ExemplarReservoir.Offer methods (#5414) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 4 +- .../Metrics/Exemplar/Exemplar.cs | 5 +- .../Metrics/MetricExemplarTests.cs | 100 +++++++++++++----- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index e11eb9ee781..ca71b6f10d5 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -959,7 +959,7 @@ private void UpdateLong(long value, ReadOnlySpan> { var index = this.FindMetricAggregatorsDefault(tags); - this.UpdateLongMetricPoint(index, value, tags: default); + this.UpdateLongMetricPoint(index, value, tags); } private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) @@ -1014,7 +1014,7 @@ private void UpdateDouble(double value, ReadOnlySpan> tags) diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index 1c2d27b99eb..d9eda128f12 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -131,7 +131,10 @@ internal void Update(in ExemplarMeasurement measurement) this.SpanId = default; } - this.StoreRawTags(measurement.Tags); + if (this.ViewDefinedTagKeys != null) + { + this.StoreRawTags(measurement.Tags); + } } internal void Reset() diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index 4c8a4c4153a..2499c825b6a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -689,49 +689,79 @@ public void TestTraceBasedExemplarFilter(bool enableTracing) } } - [Fact] - public void TestExemplarsFilterTags() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestExemplarsFilterTags(bool enableTagFiltering) { - DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var histogram = meter.CreateHistogram("testHistogram"); + TestExemplarReservoir? testExemplarReservoir = null; + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) - .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - })); + .AddView( + histogram.Name, + new MetricStreamConfiguration() + { + TagKeys = enableTagFiltering ? new string[] { "key1" } : null, + ExemplarReservoirFactory = () => + { + if (testExemplarReservoir != null) + { + throw new InvalidOperationException(); + } + + return testExemplarReservoir = new TestExemplarReservoir(); + }, + }) + .AddInMemoryExporter(exportedItems)); - var measurementValues = GenerateRandomValues(10, false, null); - foreach (var value in measurementValues) - { - histogram.Record( - value.Value, - new("key1", "value1"), - new("key2", "value1"), - new("key3", "value1")); - } + histogram.Record( + 0, + new("key1", "value1"), + new("key2", "value2"), + new("key3", "value3")); + + meterProvider.ForceFlush(); + + Assert.NotNull(testExemplarReservoir); + Assert.NotNull(testExemplarReservoir.MeasurementTags); + Assert.Equal(3, testExemplarReservoir.MeasurementTags.Length); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key1" && (string?)t.Value == "value1"); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key2" && (string?)t.Value == "value2"); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key3" && (string?)t.Value == "value3"); - 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); + Assert.NotNull(exemplars); + foreach (var exemplar in exemplars) { - Assert.NotEqual(0, exemplar.FilteredTags.MaximumCount); + if (!enableTagFiltering) + { + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); + } + else + { + Assert.Equal(3, exemplar.FilteredTags.MaximumCount); - var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); + var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); - Assert.Contains(new("key2", "value1"), filteredTags); - Assert.Contains(new("key3", "value1"), filteredTags); + Assert.Equal(2, filteredTags.Count); + + Assert.Contains(new("key2", "value2"), filteredTags); + Assert.Contains(new("key3", "value3"), filteredTags); + } } } @@ -791,4 +821,26 @@ private static void ValidateExemplars( Assert.Equal(measurementValues.Count(), count); } + + private sealed class TestExemplarReservoir : FixedSizeExemplarReservoir + { + public TestExemplarReservoir() + : base(1) + { + } + + public KeyValuePair[]? MeasurementTags { get; private set; } + + public override void Offer(in ExemplarMeasurement measurement) + { + this.MeasurementTags = measurement.Tags.ToArray(); + + this.UpdateExemplar(0, in measurement); + } + + public override void Offer(in ExemplarMeasurement measurement) + { + throw new NotSupportedException(); + } + } } From bae8a6b6991b5072c1b6d8412e329257cdfca953 Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Tue, 5 Mar 2024 18:27:17 -0800 Subject: [PATCH 21/31] Reduce memory usage by reclaimed MetricPoint (#5416) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 4 +--- src/OpenTelemetry/Metrics/MetricPoint.cs | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index ca71b6f10d5..c74c2ff25b0 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -297,9 +297,7 @@ internal void SnapshotDeltaWithMetricPointReclaim() { var lookupData = metricPoint.LookupData; - // Setting `LookupData` to `null` to denote that this MetricPoint is reclaimed. - // Snapshot method can use this to skip trying to reclaim indices which have already been reclaimed and added to the queue. - metricPoint.LookupData = null; + metricPoint.Reclaim(); Debug.Assert(this.TagsToMetricPointIndexDictionaryDelta != null, "this.tagsToMetricPointIndexDictionaryDelta was null"); diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 2ccba6a307a..e1fe8e1695c 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -19,11 +19,6 @@ public struct MetricPoint // ReferenceCount doesn't matter for MetricPoint with no tags and overflow attribute as they are never reclaimed. internal int ReferenceCount; - // When the AggregatorStore is reclaiming MetricPoints, this serves the purpose of validating the a given thread is using the right - // MetricPoint for update by checking it against what as added in the Dictionary. Also, when a thread finds out that the MetricPoint - // that its using is already reclaimed, this helps avoid sorting of the tags for adding a new Dictionary entry. - internal LookupData? LookupData; - private const int DefaultSimpleReservoirPoolSize = 1; private readonly AggregatorStore aggregatorStore; @@ -155,6 +150,12 @@ internal MetricPointStatus MetricPointStatus private set; } + // When the AggregatorStore is reclaiming MetricPoints, this serves the purpose of validating the a given thread is using the right + // MetricPoint for update by checking it against what as added in the Dictionary. Also, when a thread finds out that the MetricPoint + // that its using is already reclaimed, this helps avoid sorting of the tags for adding a new Dictionary entry. + // Snapshot method can use this to skip trying to reclaim indices which have already been reclaimed and added to the queue. + internal LookupData? LookupData { readonly get; private set; } + internal readonly bool IsInitialized => this.aggregatorStore != null; /// @@ -1195,6 +1196,15 @@ internal void TakeSnapshotWithExemplar(bool outputDelta) } } + /// + /// Denote that this MetricPoint is reclaimed. + /// + internal void Reclaim() + { + this.LookupData = null; + this.mpComponents = null; + } + private void UpdateHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) { Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); From b90c3b2cea5217831a49cbde301adc64dd76f0bd Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Thu, 7 Mar 2024 11:15:17 -0800 Subject: [PATCH 22/31] Adjust Cardinality Limit to Accommodate Internal Reserves (#5382) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 54 +++++++++---------- src/OpenTelemetry/Metrics/MetricReaderExt.cs | 10 +--- .../Metrics/MetricStreamConfiguration.cs | 8 ++- .../Metrics/MetricApiTestsBase.cs | 17 +++++- .../MetricOverflowAttributeTestsBase.cs | 37 +++++++------ .../Metrics/MetricViewTests.cs | 16 +++--- 6 files changed, 74 insertions(+), 68 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index c74c2ff25b0..17e20ad42be 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -21,7 +21,7 @@ internal sealed class AggregatorStore #endif internal readonly bool OutputDelta; internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; - internal readonly int CardinalityLimit; + internal readonly int NumberOfMetricPoints; internal readonly bool EmitOverflowAttribute; internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; internal readonly Func? ExemplarReservoirFactory; @@ -71,11 +71,15 @@ internal AggregatorStore( Func? exemplarReservoirFactory = null) { this.name = metricStreamIdentity.InstrumentName; - this.CardinalityLimit = cardinalityLimit; - this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.CardinalityLimit}"; - this.metricPoints = new MetricPoint[cardinalityLimit]; - this.currentMetricPointBatch = new int[cardinalityLimit]; + // Increase the CardinalityLimit by 2 to reserve additional space. + // This adjustment accounts for overflow attribute and a case where zero tags are provided. + // Previously, these were included within the original cardinalityLimit, but now they are explicitly added to enhance clarity. + this.NumberOfMetricPoints = cardinalityLimit + 2; + + this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {cardinalityLimit}"; + this.metricPoints = new MetricPoint[this.NumberOfMetricPoints]; + this.currentMetricPointBatch = new int[this.NumberOfMetricPoints]; this.aggType = aggType; this.OutputDelta = temporality == AggregationTemporality.Delta; this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity); @@ -110,31 +114,26 @@ internal AggregatorStore( || this.exemplarFilter == ExemplarFilterType.TraceBased, "this.exemplarFilter had an unexpected value"); - var reservedMetricPointsCount = 1; - - if (emitOverflowAttribute) - { - // Setting metricPointIndex to 1 as we would reserve the metricPoints[1] for overflow attribute. - // Newer attributes should be added starting at the index: 2 - this.metricPointIndex = 1; - reservedMetricPointsCount++; - } + // Setting metricPointIndex to 1 as we would reserve the metricPoints[1] for overflow attribute. + // Newer attributes should be added starting at the index: 2 + this.metricPointIndex = 1; this.OutputDeltaWithUnusedMetricPointReclaimEnabled = shouldReclaimUnusedMetricPoints && this.OutputDelta; if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) { - this.availableMetricPoints = new Queue(cardinalityLimit - reservedMetricPointsCount); + this.availableMetricPoints = new Queue(cardinalityLimit); // There is no overload which only takes capacity as the parameter // Using the DefaultConcurrencyLevel defined in the ConcurrentDictionary class: https://github.com/dotnet/runtime/blob/v7.0.5/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L2020 - // We expect at the most (maxMetricPoints - reservedMetricPointsCount) * 2 entries- one for sorted and one for unsorted input + // We expect at the most (user provided cardinality limit) * 2 entries- one for sorted and one for unsorted input this.TagsToMetricPointIndexDictionaryDelta = - new ConcurrentDictionary(concurrencyLevel: Environment.ProcessorCount, capacity: (cardinalityLimit - reservedMetricPointsCount) * 2); + new ConcurrentDictionary(concurrencyLevel: Environment.ProcessorCount, capacity: cardinalityLimit * 2); // Add all the indices except for the reserved ones to the queue so that threads have // readily available access to these MetricPoints for their use. - for (int i = reservedMetricPointsCount; i < this.CardinalityLimit; i++) + // Index 0 and 1 are reserved for no tags and overflow + for (int i = 2; i < this.NumberOfMetricPoints; i++) { this.availableMetricPoints.Enqueue(i); } @@ -199,12 +198,12 @@ internal int Snapshot() } else if (this.OutputDelta) { - var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1); + var indexSnapshot = Math.Min(this.metricPointIndex, this.NumberOfMetricPoints - 1); this.SnapshotDelta(indexSnapshot); } else { - var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1); + var indexSnapshot = Math.Min(this.metricPointIndex, this.NumberOfMetricPoints - 1); this.SnapshotCumulative(indexSnapshot); } @@ -260,12 +259,8 @@ internal void SnapshotDeltaWithMetricPointReclaim() this.batchSize++; } - int startIndexForReclaimableMetricPoints = 1; - if (this.EmitOverflowAttribute) { - startIndexForReclaimableMetricPoints = 2; // Index 0 and 1 are reserved for no tags and overflow - // TakeSnapshot for the MetricPoint for overflow ref var metricPointForOverflow = ref this.metricPoints[1]; if (metricPointForOverflow.MetricPointStatus != MetricPointStatus.NoCollectPending) @@ -284,7 +279,8 @@ internal void SnapshotDeltaWithMetricPointReclaim() } } - for (int i = startIndexForReclaimableMetricPoints; i < this.CardinalityLimit; i++) + // Index 0 and 1 are reserved for no tags and overflow + for (int i = 2; i < this.NumberOfMetricPoints; i++) { ref var metricPoint = ref this.metricPoints[i]; @@ -473,7 +469,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) { aggregatorIndex = this.metricPointIndex; - if (aggregatorIndex >= this.CardinalityLimit) + if (aggregatorIndex >= this.NumberOfMetricPoints) { // sorry! out of data points. // TODO: Once we support cleanup of @@ -502,7 +498,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) { aggregatorIndex = ++this.metricPointIndex; - if (aggregatorIndex >= this.CardinalityLimit) + if (aggregatorIndex >= this.NumberOfMetricPoints) { // sorry! out of data points. // TODO: Once we support cleanup of @@ -529,7 +525,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu { // This else block is for tag length = 1 aggregatorIndex = this.metricPointIndex; - if (aggregatorIndex >= this.CardinalityLimit) + if (aggregatorIndex >= this.NumberOfMetricPoints) { // sorry! out of data points. // TODO: Once we support cleanup of @@ -551,7 +547,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValu if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex)) { aggregatorIndex = ++this.metricPointIndex; - if (aggregatorIndex >= this.CardinalityLimit) + if (aggregatorIndex >= this.NumberOfMetricPoints) { // sorry! out of data points. // TODO: Once we support cleanup of diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index b64938e4c39..dfb95fab2b9 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -176,17 +176,9 @@ internal void ApplyParentProviderSettings( this.metrics = new Metric[metricLimit]; this.metricsCurrentBatch = new Metric[metricLimit]; this.cardinalityLimit = cardinalityLimit; + this.emitOverflowAttribute = emitOverflowAttribute; this.reclaimUnusedMetricPoints = reclaimUnusedMetricPoints; this.exemplarFilter = exemplarFilter; - - if (emitOverflowAttribute) - { - // We need at least two metric points. One is reserved for zero tags and the other one for overflow attribute - if (cardinalityLimit > 1) - { - this.emitOverflowAttribute = true; - } - } } private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric) diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs index cb7fb92a598..084912385fa 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs @@ -109,8 +109,12 @@ public string[]? TagKeys /// Spec reference: Cardinality /// limits. - /// Note: If not set the default MeterProvider cardinality limit of 2000 - /// will apply. + /// Note: The cardinality limit determines the maximum number of unique + /// dimension combinations for metrics. + /// Metrics with zero dimensions and overflow metrics are treated specially + /// and do not count against this limit. + /// If not set the default + /// MeterProvider cardinality limit of 2000 will apply. /// #if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.CardinalityLimitExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index 00336d03663..cc5a34d7fd1 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -1399,7 +1399,22 @@ int MetricPointCount() foreach (var metric in exportedItems) { - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + var enumerator = metric.GetMetricPoints().GetEnumerator(); + + // A case with zero tags and overflow attribute and are not a part of cardinality limit. Avoid counting them. + enumerator.MoveNext(); // First element reserved for zero tags. + enumerator.MoveNext(); // Second element reserved for overflow attribute. + + // Validate second element is overflow attribute. + // Overflow attribute is behind experimental flag. So, it is not guaranteed to be present. + var tagEnumerator = enumerator.Current.Tags.GetEnumerator(); + tagEnumerator.MoveNext(); + if (!tagEnumerator.Current.Key.Contains("otel.metric.overflow")) + { + count++; + } + + while (enumerator.MoveNext()) { count++; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs index baff3b86d76..6e929d1468a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs @@ -104,10 +104,10 @@ public void TestEmitOverflowAttributeConfigWithOtherConfigProvider(string value, } [Theory] - [InlineData(1, false)] - [InlineData(2, true)] - [InlineData(10, true)] - public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(int maxMetricPoints, bool isEmitOverflowAttributeKeySet) + [InlineData(1)] + [InlineData(2)] + [InlineData(10)] + public void EmitOverflowAttributeIsNotDependentOnMaxMetricPoints(int maxMetricPoints) { var exportedItems = new List(); @@ -129,7 +129,7 @@ public void EmitOverflowAttributeIsOnlySetWhenMaxMetricPointsIsGreaterThanOne(in meterProvider.ForceFlush(); Assert.Single(exportedItems); - Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.EmitOverflowAttribute); + Assert.True(exportedItems[0].AggregatorStore.EmitOverflowAttribute); } [Theory] @@ -158,7 +158,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem counter.Add(10); // Record measurement for zero tags // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit - 2; + int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit; for (int i = 0; i < maxMetricPointsForUse; i++) { @@ -186,7 +186,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem exportedItems.Clear(); metricPoints.Clear(); - counter.Add(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + counter.Add(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit meterProvider.ForceFlush(); metric = exportedItems[0]; @@ -215,7 +215,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem counter.Add(15); // Record another measurement for zero tags // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) + for (int i = 2001; i < 4501; i++) { counter.Add(5, new KeyValuePair("Key", i)); } @@ -236,11 +236,11 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem int expectedSum; - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) if (this.shouldReclaimUnusedMetricPoints) { - // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 - expectedSum = 2510; // 502 * 5 + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 2000 = 500 + expectedSum = 2500; // 500 * 5 } else { @@ -309,7 +309,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT histogram.Record(10); // Record measurement for zero tags // Max number for MetricPoints available for use when emitted with tags - int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit - 2; + int maxMetricPointsForUse = MeterProviderBuilderSdk.DefaultCardinalityLimit; for (int i = 0; i < maxMetricPointsForUse; i++) { @@ -337,7 +337,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT exportedItems.Clear(); metricPoints.Clear(); - histogram.Record(5, new KeyValuePair("Key", 1998)); // Emit a metric to exceed the max MetricPoint limit + histogram.Record(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit meterProvider.ForceFlush(); metric = exportedItems[0]; @@ -366,7 +366,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT histogram.Record(15); // Record another measurement for zero tags // Emit 2500 more newer MetricPoints with distinct dimension combinations - for (int i = 2000; i < 4500; i++) + for (int i = 2001; i < 4501; i++) { histogram.Record(5, new KeyValuePair("Key", i)); } @@ -388,12 +388,12 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT int expectedCount; int expectedSum; - // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - 2 (reserved for zero tags and overflow) = 1998 + // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) if (this.shouldReclaimUnusedMetricPoints) { - // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 1998 = 502 - expectedCount = 502; - expectedSum = 2510; // 502 * 5 + // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 2000 = 500 + expectedCount = 500; + expectedSum = 2500; // 500 * 5 } else { @@ -407,7 +407,6 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT else { Assert.Equal(25, zeroTagsMetricPoint.GetHistogramSum()); - Assert.Equal(2501, overflowMetricPoint.GetHistogramCount()); Assert.Equal(12505, overflowMetricPoint.GetHistogramSum()); // 5 + (2500 * 5) } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index 70a26753ef0..c1a0fca281b 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -963,16 +963,16 @@ public void CardinalityLimitofMatchingViewTakesPrecedenceOverMeterProvider(bool Assert.Equal(3, exportedItems.Count); - Assert.Equal(10000, exportedItems[1].AggregatorStore.CardinalityLimit); + Assert.Equal(10002, exportedItems[1].AggregatorStore.NumberOfMetricPoints); if (setDefault) { - Assert.Equal(3, exportedItems[0].AggregatorStore.CardinalityLimit); - Assert.Equal(3, exportedItems[2].AggregatorStore.CardinalityLimit); + Assert.Equal(5, exportedItems[0].AggregatorStore.NumberOfMetricPoints); + Assert.Equal(5, exportedItems[2].AggregatorStore.NumberOfMetricPoints); } else { - Assert.Equal(2000, exportedItems[0].AggregatorStore.CardinalityLimit); - Assert.Equal(2000, exportedItems[2].AggregatorStore.CardinalityLimit); + Assert.Equal(2002, exportedItems[0].AggregatorStore.NumberOfMetricPoints); + Assert.Equal(2002, exportedItems[2].AggregatorStore.NumberOfMetricPoints); } } @@ -1015,15 +1015,15 @@ public void ViewConflict_TwoDistinctInstruments_ThreeStreams() var metricB = exportedItems[1]; var metricC = exportedItems[2]; - Assert.Equal(256, metricA.AggregatorStore.CardinalityLimit); + Assert.Equal(258, metricA.AggregatorStore.NumberOfMetricPoints); Assert.Equal("MetricStreamA", metricA.Name); Assert.Equal(20, GetAggregatedValue(metricA)); - Assert.Equal(3, metricB.AggregatorStore.CardinalityLimit); + Assert.Equal(5, metricB.AggregatorStore.NumberOfMetricPoints); Assert.Equal("MetricStreamB", metricB.Name); Assert.Equal(10, GetAggregatedValue(metricB)); - Assert.Equal(200000, metricC.AggregatorStore.CardinalityLimit); + Assert.Equal(200002, metricC.AggregatorStore.NumberOfMetricPoints); Assert.Equal("MetricStreamC", metricC.Name); Assert.Equal(10, GetAggregatedValue(metricC)); From f34fce56207a9ab7ec49b7c5657f2bc7bd53954b Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Thu, 7 Mar 2024 13:44:34 -0800 Subject: [PATCH 23/31] [repo-shared] Remove the new() constraint from Options API helpers (#5422) --- src/Shared/Options/ConfigurationExtensions.cs | 4 ++-- src/Shared/Options/DelegatingOptionsFactory.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Shared/Options/ConfigurationExtensions.cs b/src/Shared/Options/ConfigurationExtensions.cs index 82e06158810..1d867e7167d 100644 --- a/src/Shared/Options/ConfigurationExtensions.cs +++ b/src/Shared/Options/ConfigurationExtensions.cs @@ -129,7 +129,7 @@ public static bool TryGetValue( public static IServiceCollection RegisterOptionsFactory( this IServiceCollection services, Func optionsFactoryFunc) - where T : class, new() + where T : class { Debug.Assert(services != null, "services was null"); Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); @@ -150,7 +150,7 @@ public static IServiceCollection RegisterOptionsFactory( public static IServiceCollection RegisterOptionsFactory( this IServiceCollection services, Func optionsFactoryFunc) - where T : class, new() + where T : class { Debug.Assert(services != null, "services was null"); Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); diff --git a/src/Shared/Options/DelegatingOptionsFactory.cs b/src/Shared/Options/DelegatingOptionsFactory.cs index 6d987c3dd4d..1abd015dcad 100644 --- a/src/Shared/Options/DelegatingOptionsFactory.cs +++ b/src/Shared/Options/DelegatingOptionsFactory.cs @@ -29,7 +29,7 @@ namespace Microsoft.Extensions.Options /// The type of options being requested. internal sealed class DelegatingOptionsFactory : IOptionsFactory - where TOptions : class, new() + where TOptions : class { private readonly Func optionsFactoryFunc; private readonly IConfiguration configuration; From 08201013967ba5b8aefd3a49a06546cf263754f9 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Thu, 7 Mar 2024 14:33:49 -0800 Subject: [PATCH 24/31] [sdk] Add ActivityExportProcessorOptions and clean up MetricReaderOptions (#5423) --- .../.publicApi/Stable/PublicAPI.Unshipped.txt | 6 +++ ...viderBuilderServiceCollectionExtensions.cs | 15 +++--- .../Metrics/MetricReaderOptions.cs | 11 +++-- .../Trace/ActivityExportProcessorOptions.cs | 49 +++++++++++++++++++ ...eriodicExportingMetricReaderHelperTests.cs | 6 +-- 5 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs diff --git a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt index d58a0903257..66cf734c848 100644 --- a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -1,4 +1,10 @@ OpenTelemetry.OpenTelemetryBuilderSdkExtensions +OpenTelemetry.Trace.ActivityExportProcessorOptions +OpenTelemetry.Trace.ActivityExportProcessorOptions.ActivityExportProcessorOptions() -> void +OpenTelemetry.Trace.ActivityExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Trace.BatchExportActivityProcessorOptions! +OpenTelemetry.Trace.ActivityExportProcessorOptions.BatchExportProcessorOptions.set -> void +OpenTelemetry.Trace.ActivityExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType +OpenTelemetry.Trace.ActivityExportProcessorOptions.ExportProcessorType.set -> void static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.ConfigureResource(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! diff --git a/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs index 1666291bb52..0e6091a3f65 100644 --- a/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs +++ b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs @@ -21,13 +21,6 @@ public static IServiceCollection AddOpenTelemetryLoggerProviderBuilderServices(t services!.TryAddSingleton(); services!.RegisterOptionsFactory(configuration => new BatchExportLogRecordProcessorOptions(configuration)); - - // Note: This registers a factory so that when - // sp.GetRequiredService>().Get(name))) - // is executed the SDK internal - // BatchExportLogRecordProcessorOptions(IConfiguration) ctor is used - // correctly which allows users to control the OTEL_BLRP_* keys using - // IConfiguration (envvars, appSettings, cli, etc.). services!.RegisterOptionsFactory( (sp, configuration, name) => new LogRecordExportProcessorOptions( sp.GetRequiredService>().Get(name))); @@ -40,7 +33,10 @@ public static IServiceCollection AddOpenTelemetryMeterProviderBuilderServices(th Debug.Assert(services != null, "services was null"); services!.TryAddSingleton(); - services!.RegisterOptionsFactory(configuration => new MetricReaderOptions(configuration)); + services!.RegisterOptionsFactory(configuration => new PeriodicExportingMetricReaderOptions(configuration)); + services!.RegisterOptionsFactory( + (sp, configuration, name) => new MetricReaderOptions( + sp.GetRequiredService>().Get(name))); return services!; } @@ -51,6 +47,9 @@ public static IServiceCollection AddOpenTelemetryTracerProviderBuilderServices(t services!.TryAddSingleton(); services!.RegisterOptionsFactory(configuration => new BatchExportActivityProcessorOptions(configuration)); + services!.RegisterOptionsFactory( + (sp, configuration, name) => new ActivityExportProcessorOptions( + sp.GetRequiredService>().Get(name))); return services!; } diff --git a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs b/src/OpenTelemetry/Metrics/MetricReaderOptions.cs index 6123dc2c2cd..59b850e88aa 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderOptions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Microsoft.Extensions.Configuration; +using System.Diagnostics; using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics; @@ -17,13 +17,16 @@ public class MetricReaderOptions /// Initializes a new instance of the class. /// public MetricReaderOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + : this(new()) { } - internal MetricReaderOptions(IConfiguration configuration) + internal MetricReaderOptions( + PeriodicExportingMetricReaderOptions defaultPeriodicExportingMetricReaderOptions) { - this.periodicExportingMetricReaderOptions = new PeriodicExportingMetricReaderOptions(configuration); + Debug.Assert(defaultPeriodicExportingMetricReaderOptions != null, "defaultPeriodicExportingMetricReaderOptions was null"); + + this.periodicExportingMetricReaderOptions = defaultPeriodicExportingMetricReaderOptions ?? new(); } /// diff --git a/src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs b/src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs new file mode 100644 index 00000000000..86acb043015 --- /dev/null +++ b/src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Options for configuring either a or . +/// +public class ActivityExportProcessorOptions +{ + private BatchExportActivityProcessorOptions batchExportProcessorOptions; + + /// + /// Initializes a new instance of the class. + /// + public ActivityExportProcessorOptions() + : this(new()) + { + } + + internal ActivityExportProcessorOptions( + BatchExportActivityProcessorOptions defaultBatchExportActivityProcessorOptions) + { + Debug.Assert(defaultBatchExportActivityProcessorOptions != null, "defaultBatchExportActivityProcessorOptions was null"); + + this.batchExportProcessorOptions = defaultBatchExportActivityProcessorOptions ?? new(); + } + + /// + /// Gets or sets the export processor type to be used. The default value is . + /// + public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; + + /// + /// Gets or sets the batch export options. Ignored unless is . + /// + public BatchExportActivityProcessorOptions BatchExportProcessorOptions + { + get => this.batchExportProcessorOptions; + set + { + Guard.ThrowIfNull(value); + this.batchExportProcessorOptions = value; + } + } +} diff --git a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs index 4b8847a17c3..d0da994e018 100644 --- a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs @@ -109,10 +109,10 @@ public void CreatePeriodicExportingMetricReader_FromIConfiguration() .AddInMemoryCollection(values) .Build(); - var options = new MetricReaderOptions(configuration); + var options = new PeriodicExportingMetricReaderOptions(configuration); - Assert.Equal(18, options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); - Assert.Equal(19, options.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds); + Assert.Equal(18, options.ExportIntervalMilliseconds); + Assert.Equal(19, options.ExportTimeoutMilliseconds); } [Fact] From eb2f99856cdf38f187f1020fde57b079d09d0142 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 8 Mar 2024 11:06:15 -0800 Subject: [PATCH 25/31] [sdk-logs+traces] Add internal processor weight api (#5424) --- src/OpenTelemetry/BaseProcessor.cs | 12 ++ src/OpenTelemetry/Logs/LoggerProviderSdk.cs | 3 +- .../Trace/Builder/TracerProviderBuilderSdk.cs | 4 +- src/OpenTelemetry/Trace/TracerProviderSdk.cs | 9 +- .../LoggerProviderBuilderExtensionsTests.cs | 158 +++++++++++++++- .../TracerProviderBuilderExtensionsTest.cs | 175 ++++++++++++++++++ 6 files changed, 346 insertions(+), 15 deletions(-) diff --git a/src/OpenTelemetry/BaseProcessor.cs b/src/OpenTelemetry/BaseProcessor.cs index 88e1aca35e0..daa7468c2d7 100644 --- a/src/OpenTelemetry/BaseProcessor.cs +++ b/src/OpenTelemetry/BaseProcessor.cs @@ -27,6 +27,18 @@ public BaseProcessor() /// public BaseProvider? ParentProvider { get; private set; } + /// + /// Gets or sets the weight of the processor when added to the provider + /// pipeline. Default value: 0. + /// + /// + /// Note: Weight is used to order processors when building a provider + /// pipeline. Lower weighted processors come before higher weighted + /// processors. Changing the weight after a pipeline has been constructed + /// has no effect. + /// + internal int PipelineWeight { get; set; } + /// /// Called synchronously when a telemetry object is started. /// diff --git a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs index f23308680e7..f3fc00fe8a1 100644 --- a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs @@ -54,7 +54,8 @@ public LoggerProviderSdk( resourceBuilder.ServiceProvider = serviceProvider; this.Resource = resourceBuilder.Build(); - foreach (var processor in state.Processors) + // Note: Linq OrderBy performs a stable sort, which is a requirement here + foreach (var processor in state.Processors.OrderBy(p => p.PipelineWeight)) { this.AddProcessor(processor); } diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs index 3e26b5fa27c..7b0af22d720 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs @@ -162,13 +162,13 @@ public TracerProviderBuilder ConfigureServices(Action config throw new NotSupportedException("Services cannot be configured after ServiceProvider has been created."); } - public void AddExceptionProcessorIfEnabled() + public void AddExceptionProcessorIfEnabled(ref IEnumerable> processors) { if (this.ExceptionProcessorEnabled) { try { - this.Processors.Insert(0, new ExceptionProcessor()); + processors = new BaseProcessor[] { new ExceptionProcessor() }.Concat(processors); } catch (Exception ex) { diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index 55507e87ff3..927285a4ee0 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -53,8 +53,6 @@ internal TracerProviderSdk( StringBuilder processorsAdded = new StringBuilder(); StringBuilder instrumentationFactoriesAdded = new StringBuilder(); - state.AddExceptionProcessorIfEnabled(); - var resourceBuilder = state.ResourceBuilder ?? ResourceBuilder.CreateDefault(); resourceBuilder.ServiceProvider = serviceProvider; this.Resource = resourceBuilder.Build(); @@ -74,7 +72,12 @@ internal TracerProviderSdk( } } - foreach (var processor in state.Processors) + // Note: Linq OrderBy performs a stable sort, which is a requirement here + IEnumerable> processors = state.Processors.OrderBy(p => p.PipelineWeight); + + state.AddExceptionProcessorIfEnabled(ref processors); + + foreach (var processor in processors) { this.AddProcessor(processor); processorsAdded.Append(processor.GetType()); diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs index e8a40af4f1b..79907a3d2c7 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs @@ -127,16 +127,65 @@ public void LoggerProviderBuilderConfigureResourceBuilderTests() Assert.Contains(provider.Resource.Attributes, value => value.Key == "key1" && (string)value.Value == "value1"); } + [Fact] + public void LoggerProviderBuilderUsingDependencyInjectionTest() + { + using var provider = Sdk.CreateLoggerProviderBuilder() + .AddProcessor() + .AddProcessor() + .Build() as LoggerProviderSdk; + + Assert.NotNull(provider); + + var processors = ((IServiceProvider)provider.OwnedServiceProvider!).GetServices(); + + // Note: Two "Add" calls but it is a singleton so only a single registration is produced + Assert.Single(processors); + + var processor = provider.Processor as CompositeProcessor; + + Assert.NotNull(processor); + + // Note: Two "Add" calls due yield two processors added to provider, even though they are the same + Assert.True(processor.Head.Value is CustomProcessor); + Assert.True(processor.Head.Next?.Value is CustomProcessor); + } + [Fact] public void LoggerProviderBuilderAddProcessorTest() { - List processors = new(); + List processorsToAdd = new() + { + new CustomProcessor() + { + Name = "A", + }, + new CustomProcessor() + { + Name = "B", + }, + new CustomProcessor() + { + Name = "C", + }, + }; - using (var provider = Sdk.CreateLoggerProviderBuilder() - .AddProcessor() - .AddProcessor(sp => new CustomProcessor()) - .AddProcessor(new CustomProcessor()) - .Build() as LoggerProviderSdk) + var builder = Sdk.CreateLoggerProviderBuilder(); + foreach (var processor in processorsToAdd) + { + builder.AddProcessor(processor); + } + + List expectedProcessors = new() + { + processorsToAdd.First(p => p.Name == "A"), + processorsToAdd.First(p => p.Name == "B"), + processorsToAdd.First(p => p.Name == "C"), + }; + + List actualProcessors = new(); + + using (var provider = builder.Build() as LoggerProviderSdk) { Assert.NotNull(provider); Assert.NotNull(provider.Processor); @@ -151,16 +200,106 @@ public void LoggerProviderBuilderAddProcessorTest() var processor = current.Value as CustomProcessor; Assert.NotNull(processor); - processors.Add(processor); + actualProcessors.Add(processor); Assert.False(processor.Disposed); current = current.Next; } + + Assert.Equal(expectedProcessors, actualProcessors); + } + + foreach (var processor in actualProcessors) + { + Assert.True(processor.Disposed); + } + } + + [Fact] + public void LoggerProviderBuilderAddProcessorWithWeightTest() + { + List processorsToAdd = new() + { + new CustomProcessor() + { + Name = "C", + PipelineWeight = 0, + }, + new CustomProcessor() + { + Name = "E", + PipelineWeight = 10_000, + }, + new CustomProcessor() + { + Name = "B", + PipelineWeight = -10_000, + }, + new CustomProcessor() + { + Name = "F", + PipelineWeight = int.MaxValue, + }, + new CustomProcessor() + { + Name = "A", + PipelineWeight = int.MinValue, + }, + new CustomProcessor() + { + Name = "D", + PipelineWeight = 0, + }, + }; + + var builder = Sdk.CreateLoggerProviderBuilder(); + foreach (var processor in processorsToAdd) + { + builder.AddProcessor(processor); } - Assert.Equal(3, processors.Count); + List expectedProcessors = new() + { + processorsToAdd.First(p => p.Name == "A"), + processorsToAdd.First(p => p.Name == "B"), + processorsToAdd.First(p => p.Name == "C"), + processorsToAdd.First(p => p.Name == "D"), + processorsToAdd.First(p => p.Name == "E"), + processorsToAdd.First(p => p.Name == "F"), + }; + + List actualProcessors = new(); + + using (var provider = builder.Build() as LoggerProviderSdk) + { + Assert.NotNull(provider); + Assert.NotNull(provider.Processor); + + var compositeProcessor = provider.Processor as CompositeProcessor; + + Assert.NotNull(compositeProcessor); + + var lastWeight = int.MinValue; + var current = compositeProcessor.Head; + while (current != null) + { + var processor = current.Value as CustomProcessor; + Assert.NotNull(processor); + + actualProcessors.Add(processor); + Assert.False(processor.Disposed); + + Assert.True(processor.PipelineWeight >= lastWeight); + + lastWeight = processor.PipelineWeight; + + current = current.Next; + } + + Assert.Equal(expectedProcessors, actualProcessors); + } - foreach (var processor in processors) + foreach (var processor in actualProcessors) { Assert.True(processor.Disposed); } @@ -187,6 +326,7 @@ public void Dispose() private sealed class CustomProcessor : BaseProcessor { + public string? Name; public bool Disposed; protected override void Dispose(bool disposing) diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs index 2d582402f62..e7b2e7bfc45 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs @@ -133,6 +133,172 @@ public void ServiceLifecycleAvailableToSDKBuilderTest() Assert.True(myInstrumentation.Disposed); } + [Fact] + public void AddProcessorTest() + { + List processorsToAdd = new() + { + new MyProcessor() + { + Name = "A", + }, + new MyProcessor() + { + Name = "B", + }, + new MyProcessor() + { + Name = "C", + }, + }; + + var builder = Sdk.CreateTracerProviderBuilder(); + foreach (var processor in processorsToAdd) + { + builder.AddProcessor(processor); + } + + List expectedProcessors = new() + { + processorsToAdd.First(p => p.Name == "A"), + processorsToAdd.First(p => p.Name == "B"), + processorsToAdd.First(p => p.Name == "C"), + }; + + List actualProcessors = new(); + + using (var provider = builder.Build() as TracerProviderSdk) + { + Assert.NotNull(provider); + Assert.NotNull(provider.Processor); + + var compositeProcessor = provider.Processor as CompositeProcessor; + + Assert.NotNull(compositeProcessor); + + var current = compositeProcessor.Head; + while (current != null) + { + var processor = current.Value as MyProcessor; + Assert.NotNull(processor); + + actualProcessors.Add(processor); + Assert.False(processor.Disposed); + + current = current.Next; + } + + Assert.Equal(expectedProcessors, actualProcessors); + } + + foreach (var processor in actualProcessors) + { + Assert.True(processor.Disposed); + } + } + + [Fact] + public void AddProcessorWithWeightTest() + { + List processorsToAdd = new() + { + new MyProcessor() + { + Name = "C", + PipelineWeight = 0, + }, + new MyProcessor() + { + Name = "E", + PipelineWeight = 10_000, + }, + new MyProcessor() + { + Name = "B", + PipelineWeight = -10_000, + }, + new MyProcessor() + { + Name = "F", + PipelineWeight = int.MaxValue, + }, + new MyProcessor() + { + Name = "A", + PipelineWeight = int.MinValue, + }, + new MyProcessor() + { + Name = "D", + PipelineWeight = 0, + }, + }; + + var builder = Sdk.CreateTracerProviderBuilder(); + foreach (var processor in processorsToAdd) + { + builder.AddProcessor(processor); + } + + List expectedProcessors = new() + { + processorsToAdd.First(p => p.Name == "A"), + processorsToAdd.First(p => p.Name == "B"), + processorsToAdd.First(p => p.Name == "C"), + processorsToAdd.First(p => p.Name == "D"), + processorsToAdd.First(p => p.Name == "E"), + processorsToAdd.First(p => p.Name == "F"), + }; + + List actualProcessors = new(); + + using (var provider = builder + .SetErrorStatusOnException() // Forced to be first processor + .Build() as TracerProviderSdk) + { + Assert.NotNull(provider); + Assert.NotNull(provider.Processor); + + var compositeProcessor = provider.Processor as CompositeProcessor; + + Assert.NotNull(compositeProcessor); + + bool isFirstProcessor = true; + var lastWeight = int.MinValue; + var current = compositeProcessor.Head; + while (current != null) + { + if (isFirstProcessor) + { + Assert.True(current.Value is ExceptionProcessor); + Assert.Equal(0, current.Value.PipelineWeight); + isFirstProcessor = false; + } + else + { + var processor = current.Value as MyProcessor; + Assert.NotNull(processor); + + actualProcessors.Add(processor); + Assert.False(processor.Disposed); + + Assert.True(processor.PipelineWeight >= lastWeight); + + lastWeight = processor.PipelineWeight; + } + + current = current.Next; + } + + Assert.Equal(expectedProcessors, actualProcessors); + } + + foreach (var processor in actualProcessors) + { + Assert.True(processor.Disposed); + } + } + [Fact] public void AddProcessorUsingDependencyInjectionTest() { @@ -497,6 +663,15 @@ public void Dispose() private sealed class MyProcessor : BaseProcessor { + public string Name; + public bool Disposed; + + protected override void Dispose(bool disposing) + { + this.Disposed = true; + + base.Dispose(disposing); + } } private sealed class MyExporter : BaseExporter From 81d29cf87e198e55a10712bcb0c55d25fb3421ae Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 8 Mar 2024 11:35:01 -0800 Subject: [PATCH 26/31] [otlp] Rename ProgrammaticallyModifiedEndpoint -> AppendSignalPathToEndpoint (#5427) --- .../ExportClient/BaseOtlpHttpExportClient.cs | 2 +- .../OtlpExporterOptions.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs index 4aad820b1e2..213316cf8ce 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs @@ -21,7 +21,7 @@ protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpC Guard.ThrowIfNull(signalPath); Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); - Uri exporterEndpoint = !options.ProgrammaticallyModifiedEndpoint + Uri exporterEndpoint = options.AppendSignalPathToEndpoint ? options.Endpoint.AppendPathIfNotPresent(signalPath) : options.Endpoint; this.Endpoint = new UriBuilder(exporterEndpoint).Uri; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index e4c820cf58a..bf86c2d817e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -114,7 +114,7 @@ public Uri Endpoint set { this.endpoint = value; - this.ProgrammaticallyModifiedEndpoint = true; + this.AppendSignalPathToEndpoint = false; } } @@ -180,9 +180,14 @@ public Uri Endpoint public Func HttpClientFactory { get; set; } /// - /// Gets a value indicating whether was modified via its setter. + /// Gets a value indicating whether or not the signal-specific path should + /// be appended to . /// - internal bool ProgrammaticallyModifiedEndpoint { get; private set; } + /// + /// Note: Only applicable when + /// is used. + /// + internal bool AppendSignalPathToEndpoint { get; private set; } = true; internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection services) { From 9adb088e0653d9a93bbfcf7b9a348fe81a17b11d Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Fri, 8 Mar 2024 12:01:18 -0800 Subject: [PATCH 27/31] [otlp] Move spec envvar key definitions to a single spot (#5428) --- .../OtlpExporterSpecEnvVarKeyDefinitions.cs | 20 ++++++++ .../OtlpExporterOptions.cs | 13 ++--- .../OtlpMetricExporterExtensions.cs | 6 +-- .../BaseOtlpHttpExportClientTests.cs | 4 +- .../OtlpExporterOptionsTests.cs | 48 +++++++++---------- .../OtlpLogExporterTests.cs | 2 +- 6 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs new file mode 100644 index 00000000000..c7e4dad9181 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter; + +/// +/// Contains spec environment variable key definitions for OpenTelemetry Protocol (OTLP) exporter. +/// +/// +/// Specification: . +/// +internal static class OtlpExporterSpecEnvVarKeyDefinitions +{ + public const string DefaultEndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; + public const string DefaultHeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; + public const string DefaultTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; + public const string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + public const string MetricsTemporalityPreferenceEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index bf86c2d817e..05382efd6ce 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -22,11 +22,6 @@ namespace OpenTelemetry.Exporter; /// public class OtlpExporterOptions { - internal const string EndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; - internal const string HeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; - internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; - internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; - internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] { new KeyValuePair("User-Agent", GetUserAgentString()), @@ -56,23 +51,23 @@ internal OtlpExporterOptions( Debug.Assert(configuration != null, "configuration was null"); Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - if (configuration.TryGetUriValue(EndpointEnvVarName, out var endpoint)) + if (configuration.TryGetUriValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, out var endpoint)) { this.endpoint = endpoint; } - if (configuration.TryGetStringValue(HeadersEnvVarName, out var headers)) + if (configuration.TryGetStringValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, out var headers)) { this.Headers = headers; } - if (configuration.TryGetIntValue(TimeoutEnvVarName, out var timeout)) + if (configuration.TryGetIntValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, out var timeout)) { this.TimeoutMilliseconds = timeout; } if (configuration.TryGetValue( - ProtocolEnvVarName, + OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, OtlpExportProtocolParser.TryParse, out var protocol)) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index f6e3be715d7..747cf46f949 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -16,8 +16,6 @@ namespace OpenTelemetry.Metrics; /// public static class OtlpMetricExporterExtensions { - internal const string OtlpMetricExporterTemporalityPreferenceEnvVarKey = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; - /// /// Adds to the using default options. /// @@ -65,7 +63,7 @@ public static MeterProviderBuilder AddOtlpExporter( services.AddOptions(finalOptionsName).Configure( (readerOptions, config) => { - var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey]; + var otlpTemporalityPreference = config[OtlpExporterSpecEnvVarKeyDefinitions.MetricsTemporalityPreferenceEnvVarName]; if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) { @@ -142,7 +140,7 @@ public static MeterProviderBuilder AddOtlpExporter( services.AddOptions(finalOptionsName).Configure( (readerOptions, config) => { - var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey]; + var otlpTemporalityPreference = config[OtlpExporterSpecEnvVarKeyDefinitions.MetricsTemporalityPreferenceEnvVarName]; if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs index 68011107b7f..4a419463b6d 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs @@ -21,7 +21,7 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e { try { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, endpointEnvVar); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, endpointEnvVar); OtlpExporterOptions options = new() { Protocol = OtlpExportProtocol.HttpProtobuf }; @@ -35,7 +35,7 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e } finally { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, null); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 619dd7999e9..f567ed118e1 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -46,10 +46,10 @@ public void OtlpExporterOptions_DefaultsForHttpProtobuf() [Fact] public void OtlpExporterOptions_EnvironmentVariableOverride() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, "A=2,B=3"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "http/protobuf"); var options = new OtlpExporterOptions(); @@ -64,10 +64,10 @@ public void OtlpExporterOptions_UsingIConfiguration() { var values = new Dictionary() { - [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", - [OtlpExporterOptions.HeadersEnvVarName] = "A=2,B=3", - [OtlpExporterOptions.TimeoutEnvVarName] = "2000", - [OtlpExporterOptions.ProtocolEnvVarName] = "http/protobuf", + [OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", + [OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName] = "A=2,B=3", + [OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName] = "2000", + [OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName] = "http/protobuf", }; var configuration = new ConfigurationBuilder() @@ -85,9 +85,9 @@ public void OtlpExporterOptions_UsingIConfiguration() [Fact] public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "invalid"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "invalid"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "invalid"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "invalid"); var options = new OtlpExporterOptions(); @@ -99,10 +99,10 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() [Fact] public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "grpc"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, "A=2,B=3"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "grpc"); var options = new OtlpExporterOptions { @@ -121,7 +121,7 @@ public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() [Fact] public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromEnvVariables() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.Grpc }; @@ -141,17 +141,17 @@ public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromS [Fact] public void OtlpExporterOptions_EnvironmentVariableNames() { - Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterOptions.EndpointEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterOptions.HeadersEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterOptions.TimeoutEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterOptions.ProtocolEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName); } private static void ClearEnvVars() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, null); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index a01d992040b..6ad2ff04686 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -1517,7 +1517,7 @@ private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( { var values = new Dictionary() { - [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", + [OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", }; var configuration = new ConfigurationBuilder() From 3cd4c622ae72130ddb3f7c76cd687393f5ceed6b Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Mon, 11 Mar 2024 10:51:15 -0700 Subject: [PATCH 28/31] [Otlp] Fix Http Retry to cover network failure and add tests (#5394) --- .../Implementation/ExportClient/OtlpRetry.cs | 42 +++-- .../OtlpRetryTests.cs | 157 +++++++++++++++++- 2 files changed, 186 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs index e1db5e5007b..4d214fcfb7f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + using System.Diagnostics; using System.Net; using System.Net.Http.Headers; @@ -52,9 +54,33 @@ internal static class OtlpRetry private static readonly Random Random = new Random(); #endif - public static bool TryGetHttpRetryResult(HttpStatusCode statusCode, DateTime? deadline, HttpResponseHeaders responseHeaders, int retryDelayMilliseconds, out RetryResult retryResult) + public static bool TryGetHttpRetryResult(ExportClientHttpResponse response, int retryDelayInMilliSeconds, out RetryResult retryResult) { - return TryGetRetryResult(statusCode, IsHttpStatusCodeRetryable, deadline, responseHeaders, TryGetHttpRetryDelay, retryDelayMilliseconds, out retryResult); + retryResult = default; + if (response.StatusCode.HasValue) + { + return TryGetRetryResult(response.StatusCode.Value, IsHttpStatusCodeRetryable, response.DeadlineUtc, response.Headers, TryGetHttpRetryDelay, retryDelayInMilliSeconds, out retryResult); + } + else + { + if (ShouldHandleHttpRequestException(response.Exception)) + { + var delay = TimeSpan.FromMilliseconds(GetRandomNumber(0, retryDelayInMilliSeconds)); + if (!IsDeadlineExceeded(response.DeadlineUtc + delay)) + { + retryResult = new RetryResult(false, delay, CalculateNextRetryDelay(retryDelayInMilliSeconds)); + return true; + } + } + + return false; + } + } + + public static bool ShouldHandleHttpRequestException(Exception? exception) + { + // TODO: Handle specific exceptions. + return true; } public static bool TryGetGrpcRetryResult(StatusCode statusCode, DateTime? deadline, Metadata trailers, int retryDelayMilliseconds, out RetryResult retryResult) @@ -140,7 +166,7 @@ private static int CalculateNextRetryDelay(int nextRetryDelayMilliseconds) return null; } - var statusDetails = trailers.Get(GrpcStatusDetailsHeader); + var statusDetails = trailers!.Get(GrpcStatusDetailsHeader); if (statusDetails != null && statusDetails.IsBinary) { var status = Status.Parser.ParseFrom(statusDetails.ValueBytes); @@ -157,16 +183,14 @@ private static int CalculateNextRetryDelay(int nextRetryDelayMilliseconds) return null; } - private static TimeSpan? TryGetHttpRetryDelay(HttpStatusCode statusCode, HttpResponseHeaders headers) + private static TimeSpan? TryGetHttpRetryDelay(HttpStatusCode statusCode, HttpResponseHeaders? responseHeaders) { - Debug.Assert(headers != null, "headers was null"); - #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER return statusCode == HttpStatusCode.TooManyRequests || statusCode == HttpStatusCode.ServiceUnavailable #else return statusCode == (HttpStatusCode)429 || statusCode == HttpStatusCode.ServiceUnavailable #endif - ? headers.RetryAfter?.Delta + ? responseHeaders?.RetryAfter?.Delta : null; } @@ -188,9 +212,7 @@ private static bool IsGrpcStatusCodeRetryable(StatusCode statusCode, bool hasRet } } -#pragma warning disable SA1313 // Parameter should begin with lower-case letter - private static bool IsHttpStatusCodeRetryable(HttpStatusCode statusCode, bool _) -#pragma warning restore SA1313 // Parameter should begin with lower-case letter + private static bool IsHttpStatusCodeRetryable(HttpStatusCode statusCode, bool hasRetryDelay) { switch (statusCode) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs index f8815ed5665..9b45b441317 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs @@ -1,6 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + +using System.Net; +using System.Net.Http.Headers; +#if NETFRAMEWORK +using System.Net.Http; +#endif using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -10,7 +17,9 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie public class OtlpRetryTests { - public static IEnumerable GrpcRetryTestData => GrpcRetryTestCase.GetTestCases(); + public static IEnumerable GrpcRetryTestData => GrpcRetryTestCase.GetGrpcTestCases(); + + public static IEnumerable HttpRetryTestData => HttpRetryTestCase.GetHttpTestCases(); [Theory] [MemberData(nameof(GrpcRetryTestData))] @@ -53,6 +62,47 @@ public void TryGetGrpcRetryResultTest(GrpcRetryTestCase testCase) Assert.Equal(testCase.ExpectedRetryAttempts, attempts); } + [Theory] + [MemberData(nameof(HttpRetryTestData))] + public void TryGetHttpRetryResultTest(HttpRetryTestCase testCase) + { + var attempts = 0; + var nextRetryDelayMilliseconds = OtlpRetry.InitialBackoffMilliseconds; + + foreach (var retryAttempt in testCase.RetryAttempts) + { + ++attempts; + var statusCode = retryAttempt.Response.StatusCode; + var deadline = retryAttempt.Response.DeadlineUtc; + var headers = retryAttempt.Response.Headers; + var success = OtlpRetry.TryGetHttpRetryResult(retryAttempt.Response, nextRetryDelayMilliseconds, out var retryResult); + + Assert.Equal(retryAttempt.ExpectedSuccess, success); + + if (!success) + { + Assert.Equal(testCase.ExpectedRetryAttempts, attempts); + break; + } + + if (retryResult.Throttled) + { + Assert.Equal(retryAttempt.ThrottleDelay, retryResult.RetryDelay); + } + else + { + Assert.True(retryResult.RetryDelay >= TimeSpan.Zero); + Assert.True(retryResult.RetryDelay < TimeSpan.FromMilliseconds(nextRetryDelayMilliseconds)); + } + + Assert.Equal(retryAttempt.ExpectedNextRetryDelayMilliseconds, retryResult.NextRetryDelayMilliseconds); + + nextRetryDelayMilliseconds = retryResult.NextRetryDelayMilliseconds; + } + + Assert.Equal(testCase.ExpectedRetryAttempts, attempts); + } + public class GrpcRetryTestCase { public int ExpectedRetryAttempts; @@ -67,7 +117,7 @@ private GrpcRetryTestCase(string testRunnerName, GrpcRetryAttempt[] retryAttempt this.testRunnerName = testRunnerName; } - public static IEnumerable GetTestCases() + public static IEnumerable GetGrpcTestCases() { yield return new[] { new GrpcRetryTestCase("Cancelled", new GrpcRetryAttempt[] { new(StatusCode.Cancelled) }) }; yield return new[] { new GrpcRetryTestCase("DeadlineExceeded", new GrpcRetryAttempt[] { new(StatusCode.DeadlineExceeded) }) }; @@ -181,7 +231,7 @@ public struct GrpcRetryAttempt public GrpcRetryAttempt( StatusCode statusCode, bool deadlineExceeded = false, - Duration throttleDelay = null, + Duration? throttleDelay = null, int expectedNextRetryDelayMilliseconds = 1500, bool expectedSuccess = true) { @@ -200,4 +250,105 @@ public GrpcRetryAttempt( } } } + + public class HttpRetryTestCase + { + public int ExpectedRetryAttempts; + internal HttpRetryAttempt[] RetryAttempts; + + private string testRunnerName; + + private HttpRetryTestCase(string testRunnerName, HttpRetryAttempt[] retryAttempts, int expectedRetryAttempts = 1) + { + this.ExpectedRetryAttempts = expectedRetryAttempts; + this.RetryAttempts = retryAttempts; + this.testRunnerName = testRunnerName; + } + + public static IEnumerable GetHttpTestCases() + { + yield return new[] { new HttpRetryTestCase("NetworkError", [new(statusCode: null)]) }; + yield return new[] { new HttpRetryTestCase("GatewayTimeout", [new(statusCode: HttpStatusCode.GatewayTimeout, throttleDelay: TimeSpan.FromSeconds(1))]) }; +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + yield return new[] { new HttpRetryTestCase("ServiceUnavailable", [new(statusCode: HttpStatusCode.TooManyRequests, throttleDelay: TimeSpan.FromSeconds(1))]) }; +#endif + + yield return new[] + { + new HttpRetryTestCase( + "Exponential Backoff", + new HttpRetryAttempt[] + { + new(statusCode: null, expectedNextRetryDelayMilliseconds: 1500), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 2250), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 3375), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000), + }, + expectedRetryAttempts: 5), + }; + + yield return new[] + { + new HttpRetryTestCase( + "Retry until non-retryable status code encountered", + new HttpRetryAttempt[] + { + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 1500), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 2250), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 3375), + new(statusCode: HttpStatusCode.BadRequest, expectedSuccess: false), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 5000), + }, + expectedRetryAttempts: 4), + }; + + yield return new[] { new HttpRetryTestCase("Expired deadline", new HttpRetryAttempt[] { new(statusCode: HttpStatusCode.ServiceUnavailable, isDeadlineExceeded: true, expectedSuccess: false) }) }; + + // TODO: Add more cases. + } + + public override string ToString() + { + return this.testRunnerName; + } + + internal class HttpRetryAttempt + { + public ExportClientHttpResponse Response; + public DateTime? Deadline; + public TimeSpan? ThrottleDelay; + public int? ExpectedNextRetryDelayMilliseconds; + public bool ExpectedSuccess; + + internal HttpRetryAttempt( + HttpStatusCode? statusCode, + TimeSpan? throttleDelay = null, + bool isDeadlineExceeded = false, + int expectedNextRetryDelayMilliseconds = 1500, + bool expectedSuccess = true) + { + this.ThrottleDelay = throttleDelay; + + HttpResponseMessage? responseMessage = null; + if (statusCode != null) + { + responseMessage = new HttpResponseMessage(); + + if (throttleDelay != null) + { + responseMessage.Headers.RetryAfter = new RetryConditionHeaderValue(throttleDelay.Value); + } + + responseMessage.StatusCode = (HttpStatusCode)statusCode; + } + + this.Response = new ExportClientHttpResponse(expectedSuccess, isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : null, responseMessage, new HttpRequestException()); + + this.Deadline = isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : null; + this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; + this.ExpectedSuccess = expectedSuccess; + } + } + } } From 85933659abde74d161e8d488e6ef6e242fcbaa2a Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 11 Mar 2024 11:37:44 -0700 Subject: [PATCH 29/31] [otlp] Add foundation for signal-specific envvars (#5429) --- .../OtlpExporterOptionsConfigurationType.cs | 17 ++ .../OtlpExporterSpecEnvVarKeyDefinitions.cs | 20 -- .../OtlpSpecConfigDefinitions.cs | 34 ++++ .../OtlpExporterOptions.cs | 142 ++++++++++--- .../OtlpMetricExporterExtensions.cs | 4 +- .../BaseOtlpHttpExportClientTests.cs | 4 +- .../OtlpExporterOptionsTests.cs | 191 +++++++++++++----- .../OtlpLogExporterTests.cs | 2 +- 8 files changed, 313 insertions(+), 101 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs delete mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs new file mode 100644 index 00000000000..d3cedd6915c --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Exporter; + +[Flags] +internal enum OtlpExporterOptionsConfigurationType +{ +#pragma warning disable SA1602 // Enumeration items should be documented + Default, + Logs, + Metrics, + Traces, +#pragma warning restore SA1602 // Enumeration items should be documented +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs deleted file mode 100644 index c7e4dad9181..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterSpecEnvVarKeyDefinitions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Exporter; - -/// -/// Contains spec environment variable key definitions for OpenTelemetry Protocol (OTLP) exporter. -/// -/// -/// Specification: . -/// -internal static class OtlpExporterSpecEnvVarKeyDefinitions -{ - public const string DefaultEndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; - public const string DefaultHeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; - public const string DefaultTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; - public const string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; - - public const string MetricsTemporalityPreferenceEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs new file mode 100644 index 00000000000..3bc62218b3f --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter; + +/// +/// Contains spec environment variable key definitions for OpenTelemetry Protocol (OTLP) exporter. +/// +/// +/// Specification: . +/// +internal static class OtlpSpecConfigDefinitions +{ + public const string DefaultEndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; + public const string DefaultHeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; + public const string DefaultTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; + public const string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + public const string LogsEndpointEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"; + public const string LogsHeadersEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; + public const string LogsTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"; + public const string LogsProtocolEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"; + + public const string MetricsEndpointEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"; + public const string MetricsHeadersEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"; + public const string MetricsTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT"; + public const string MetricsProtocolEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"; + public const string MetricsTemporalityPreferenceEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; + + public const string TracesEndpointEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; + public const string TracesHeadersEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"; + public const string TracesTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"; + public const string TracesProtocolEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 05382efd6ce..0cffd50806c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -17,11 +17,18 @@ namespace OpenTelemetry.Exporter; /// /// OpenTelemetry Protocol (OTLP) exporter options. -/// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_PROTOCOL -/// environment variables are parsed during object construction. /// +/// +/// Note: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, +/// OTEL_EXPORTER_OTLP_TIMEOUT, and OTEL_EXPORTER_OTLP_PROTOCOL environment +/// variables are parsed during object construction. +/// public class OtlpExporterOptions { + internal const string DefaultGrpcEndpoint = "http://localhost:4317"; + internal const string DefaultHttpEndpoint = "http://localhost:4318"; + internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; + internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] { new KeyValuePair("User-Agent", GetUserAgentString()), @@ -29,9 +36,6 @@ public class OtlpExporterOptions internal readonly Func DefaultHttpClientFactory; - private const string DefaultGrpcEndpoint = "http://localhost:4317"; - private const string DefaultHttpEndpoint = "http://localhost:4318"; - private const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; private Uri endpoint; @@ -40,39 +44,27 @@ public class OtlpExporterOptions /// Initializes a new instance of the class. /// public OtlpExporterOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) + : this(OtlpExporterOptionsConfigurationType.Default) + { + } + + internal OtlpExporterOptions( + OtlpExporterOptionsConfigurationType configurationType) + : this( + configuration: new ConfigurationBuilder().AddEnvironmentVariables().Build(), + configurationType, + defaultBatchOptions: new()) { } internal OtlpExporterOptions( IConfiguration configuration, + OtlpExporterOptionsConfigurationType configurationType, BatchExportActivityProcessorOptions defaultBatchOptions) { - Debug.Assert(configuration != null, "configuration was null"); Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - if (configuration.TryGetUriValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, out var endpoint)) - { - this.endpoint = endpoint; - } - - if (configuration.TryGetStringValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, out var headers)) - { - this.Headers = headers; - } - - if (configuration.TryGetIntValue(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, out var timeout)) - { - this.TimeoutMilliseconds = timeout; - } - - if (configuration.TryGetValue( - OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, - OtlpExportProtocolParser.TryParse, - out var protocol)) - { - this.Protocol = protocol; - } + this.ApplyConfiguration(configuration, configurationType); this.HttpClientFactory = this.DefaultHttpClientFactory = () => { @@ -98,7 +90,7 @@ public Uri Endpoint { if (this.endpoint == null) { - this.endpoint = this.Protocol == OtlpExportProtocol.Grpc + return this.Protocol == OtlpExportProtocol.Grpc ? new Uri(DefaultGrpcEndpoint) : new Uri(DefaultHttpEndpoint); } @@ -195,8 +187,42 @@ internal static OtlpExporterOptions CreateOtlpExporterOptions( string name) => new( configuration, + OtlpExporterOptionsConfigurationType.Default, serviceProvider.GetRequiredService>().Get(name)); + internal void ApplyConfigurationUsingSpecificationEnvVars( + IConfiguration configuration, + string endpointEnvVarKey, + bool appendSignalPathToEndpoint, + string protocolEnvVarKey, + string headersEnvVarKey, + string timeoutEnvVarKey) + { + if (configuration.TryGetUriValue(endpointEnvVarKey, out var endpoint)) + { + this.endpoint = endpoint; + this.AppendSignalPathToEndpoint = appendSignalPathToEndpoint; + } + + if (configuration.TryGetValue( + protocolEnvVarKey, + OtlpExportProtocolParser.TryParse, + out var protocol)) + { + this.Protocol = protocol; + } + + if (configuration.TryGetStringValue(headersEnvVarKey, out var headers)) + { + this.Headers = headers; + } + + if (configuration.TryGetIntValue(timeoutEnvVarKey, out var timeout)) + { + this.TimeoutMilliseconds = timeout; + } + } + private static string GetUserAgentString() { try @@ -210,4 +236,60 @@ private static string GetUserAgentString() return UserAgentProduct; } } + + private void ApplyConfiguration( + IConfiguration configuration, + OtlpExporterOptionsConfigurationType configurationType) + { + Debug.Assert(configuration != null, "configuration was null"); + + // Note: When using the "AddOtlpExporter" extensions configurationType + // never has a value other than "Default" because OtlpExporterOptions is + // shared by all signals and there is no way to differentiate which + // signal is being constructed. + if (configurationType == OtlpExporterOptionsConfigurationType.Default) + { + this.ApplyConfigurationUsingSpecificationEnvVars( + configuration!, + OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, + appendSignalPathToEndpoint: true, + OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName, + OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName, + OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName); + } + else if (configurationType == OtlpExporterOptionsConfigurationType.Logs) + { + this.ApplyConfigurationUsingSpecificationEnvVars( + configuration!, + OtlpSpecConfigDefinitions.LogsEndpointEnvVarName, + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.LogsProtocolEnvVarName, + OtlpSpecConfigDefinitions.LogsHeadersEnvVarName, + OtlpSpecConfigDefinitions.LogsTimeoutEnvVarName); + } + else if (configurationType == OtlpExporterOptionsConfigurationType.Metrics) + { + this.ApplyConfigurationUsingSpecificationEnvVars( + configuration!, + OtlpSpecConfigDefinitions.MetricsEndpointEnvVarName, + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.MetricsProtocolEnvVarName, + OtlpSpecConfigDefinitions.MetricsHeadersEnvVarName, + OtlpSpecConfigDefinitions.MetricsTimeoutEnvVarName); + } + else if (configurationType == OtlpExporterOptionsConfigurationType.Traces) + { + this.ApplyConfigurationUsingSpecificationEnvVars( + configuration!, + OtlpSpecConfigDefinitions.TracesEndpointEnvVarName, + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.TracesProtocolEnvVarName, + OtlpSpecConfigDefinitions.TracesHeadersEnvVarName, + OtlpSpecConfigDefinitions.TracesTimeoutEnvVarName); + } + else + { + throw new NotSupportedException($"OtlpExporterOptionsConfigurationType '{configurationType}' is not supported."); + } + } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index 747cf46f949..37a66dc2f10 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -63,7 +63,7 @@ public static MeterProviderBuilder AddOtlpExporter( services.AddOptions(finalOptionsName).Configure( (readerOptions, config) => { - var otlpTemporalityPreference = config[OtlpExporterSpecEnvVarKeyDefinitions.MetricsTemporalityPreferenceEnvVarName]; + var otlpTemporalityPreference = config[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) { @@ -140,7 +140,7 @@ public static MeterProviderBuilder AddOtlpExporter( services.AddOptions(finalOptionsName).Configure( (readerOptions, config) => { - var otlpTemporalityPreference = config[OtlpExporterSpecEnvVarKeyDefinitions.MetricsTemporalityPreferenceEnvVarName]; + var otlpTemporalityPreference = config[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs index 4a419463b6d..a846af33a4e 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs @@ -21,7 +21,7 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e { try { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, endpointEnvVar); + Environment.SetEnvironmentVariable(OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, endpointEnvVar); OtlpExporterOptions options = new() { Protocol = OtlpExportProtocol.HttpProtobuf }; @@ -35,7 +35,7 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e } finally { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, null); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index f567ed118e1..4ba49ec760a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -13,6 +13,49 @@ public OtlpExporterOptionsTests() ClearEnvVars(); } + public static IEnumerable GetOtlpExporterOptionsTestCases() + { + yield return new object[] + { + OtlpExporterOptionsConfigurationType.Default, + OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, + OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName, + OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName, + OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName, + true, + }; + + yield return new object[] + { + OtlpExporterOptionsConfigurationType.Logs, + OtlpSpecConfigDefinitions.LogsEndpointEnvVarName, + OtlpSpecConfigDefinitions.LogsHeadersEnvVarName, + OtlpSpecConfigDefinitions.LogsTimeoutEnvVarName, + OtlpSpecConfigDefinitions.LogsProtocolEnvVarName, + false, + }; + + yield return new object[] + { + OtlpExporterOptionsConfigurationType.Metrics, + OtlpSpecConfigDefinitions.MetricsEndpointEnvVarName, + OtlpSpecConfigDefinitions.MetricsHeadersEnvVarName, + OtlpSpecConfigDefinitions.MetricsTimeoutEnvVarName, + OtlpSpecConfigDefinitions.MetricsProtocolEnvVarName, + false, + }; + + yield return new object[] + { + OtlpExporterOptionsConfigurationType.Traces, + OtlpSpecConfigDefinitions.TracesEndpointEnvVarName, + OtlpSpecConfigDefinitions.TracesHeadersEnvVarName, + OtlpSpecConfigDefinitions.TracesTimeoutEnvVarName, + OtlpSpecConfigDefinitions.TracesProtocolEnvVarName, + false, + }; + } + public void Dispose() { ClearEnvVars(); @@ -24,10 +67,10 @@ public void OtlpExporterOptions_Defaults() { var options = new OtlpExporterOptions(); - Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); + Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); Assert.Null(options.Headers); Assert.Equal(10000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); } [Fact] @@ -37,100 +80,153 @@ public void OtlpExporterOptions_DefaultsForHttpProtobuf() { Protocol = OtlpExportProtocol.HttpProtobuf, }; - Assert.Equal(new Uri("http://localhost:4318"), options.Endpoint); + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); Assert.Null(options.Headers); Assert.Equal(10000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); } - [Fact] - public void OtlpExporterOptions_EnvironmentVariableOverride() + [Theory] + [MemberData(nameof(GetOtlpExporterOptionsTestCases))] + public void OtlpExporterOptions_EnvironmentVariableOverride( + int configurationType, + string endpointEnvVarKeyName, + string headersEnvVarKeyName, + string timeoutEnvVarKeyName, + string protocolEnvVarKeyName, + bool appendSignalPathToEndpoint) { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "http/protobuf"); + Environment.SetEnvironmentVariable(endpointEnvVarKeyName, "http://test:8888"); + Environment.SetEnvironmentVariable(headersEnvVarKeyName, "A=2,B=3"); + Environment.SetEnvironmentVariable(timeoutEnvVarKeyName, "2000"); + Environment.SetEnvironmentVariable(protocolEnvVarKeyName, "http/protobuf"); - var options = new OtlpExporterOptions(); + var options = new OtlpExporterOptions((OtlpExporterOptionsConfigurationType)configurationType); Assert.Equal(new Uri("http://test:8888"), options.Endpoint); Assert.Equal("A=2,B=3", options.Headers); Assert.Equal(2000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + Assert.Equal(appendSignalPathToEndpoint, options.AppendSignalPathToEndpoint); } - [Fact] - public void OtlpExporterOptions_UsingIConfiguration() + [Theory] + [MemberData(nameof(GetOtlpExporterOptionsTestCases))] + public void OtlpExporterOptions_UsingIConfiguration( + int configurationType, + string endpointEnvVarKeyName, + string headersEnvVarKeyName, + string timeoutEnvVarKeyName, + string protocolEnvVarKeyName, + bool appendSignalPathToEndpoint) { var values = new Dictionary() { - [OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", - [OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName] = "A=2,B=3", - [OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName] = "2000", - [OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName] = "http/protobuf", + [endpointEnvVarKeyName] = "http://test:8888", + [headersEnvVarKeyName] = "A=2,B=3", + [timeoutEnvVarKeyName] = "2000", + [protocolEnvVarKeyName] = "http/protobuf", }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(values) .Build(); - var options = new OtlpExporterOptions(configuration, new()); + var options = new OtlpExporterOptions(configuration, (OtlpExporterOptionsConfigurationType)configurationType, new()); Assert.Equal(new Uri("http://test:8888"), options.Endpoint); Assert.Equal("A=2,B=3", options.Headers); Assert.Equal(2000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + Assert.Equal(appendSignalPathToEndpoint, options.AppendSignalPathToEndpoint); } [Fact] public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "invalid"); + var values = new Dictionary() + { + ["EndpointWithInvalidValue"] = "invalid", + ["TimeoutWithInvalidValue"] = "invalid", + ["ProtocolWithInvalidValue"] = "invalid", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); var options = new OtlpExporterOptions(); - Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); + options.ApplyConfigurationUsingSpecificationEnvVars( + configuration, + "EndpointWithInvalidValue", + appendSignalPathToEndpoint: true, + "ProtocolWithInvalidValue", + "NoopHeaders", + "TimeoutWithInvalidValue"); + + Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); Assert.Equal(10000, options.TimeoutMilliseconds); - Assert.Equal(default, options.Protocol); + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); + Assert.Null(options.Headers); } [Fact] public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, "grpc"); - - var options = new OtlpExporterOptions + var values = new Dictionary() { - Endpoint = new Uri("http://localhost:200"), - Headers = "C=3", - TimeoutMilliseconds = 40000, - Protocol = OtlpExportProtocol.HttpProtobuf, + ["Endpoint"] = "http://test:8888", + ["Timeout"] = "2000", + ["Protocol"] = "grpc", + ["Headers"] = "A=2,B=3", }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new OtlpExporterOptions(); + + options.ApplyConfigurationUsingSpecificationEnvVars( + configuration, + "Endpoint", + appendSignalPathToEndpoint: true, + "Protocol", + "Headers", + "Timeout"); + + options.Endpoint = new Uri("http://localhost:200"); + options.Headers = "C=3"; + options.TimeoutMilliseconds = 40000; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); Assert.Equal("C=3", options.Headers); Assert.Equal(40000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + Assert.False(options.AppendSignalPathToEndpoint); } [Fact] - public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromEnvVariables() + public void OtlpExporterOptions_EndpointGetterUsesProtocolWhenNull() { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, "http://test:8888"); + var options = new OtlpExporterOptions(); - var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.Grpc }; + Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); - Assert.Equal(new Uri("http://test:8888"), options.Endpoint); - Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); + + options.Protocol = OtlpExportProtocol.Grpc; + + Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); } [Fact] - public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromSetter() + public void OtlpExporterOptions_EndpointGetterIgnoresProtocolWhenNotNull() { var options = new OtlpExporterOptions { Endpoint = new Uri("http://test:8888"), Protocol = OtlpExportProtocol.Grpc }; @@ -141,17 +237,20 @@ public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromS [Fact] public void OtlpExporterOptions_EnvironmentVariableNames() { - Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName); } private static void ClearEnvVars() { - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultHeadersEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultTimeoutEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterSpecEnvVarKeyDefinitions.DefaultProtocolEnvVarName, null); + foreach (var item in GetOtlpExporterOptionsTestCases()) + { + Environment.SetEnvironmentVariable((string)item[1], null); + Environment.SetEnvironmentVariable((string)item[2], null); + Environment.SetEnvironmentVariable((string)item[3], null); + Environment.SetEnvironmentVariable((string)item[4], null); + } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 6ad2ff04686..87a6011f2f5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -1517,7 +1517,7 @@ private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( { var values = new Dictionary() { - [OtlpExporterSpecEnvVarKeyDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", + [OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", }; var configuration = new ConfigurationBuilder() From 043d8a31c768d05544c06629e072492238a0069b Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Mon, 11 Mar 2024 11:57:57 -0700 Subject: [PATCH 30/31] [Otlp] Refactor ExportClient (#5431) --- .../ExportClient/BaseOtlpGrpcExportClient.cs | 4 +- .../ExportClient/BaseOtlpHttpExportClient.cs | 12 ++--- .../ExportClient/ExportClientGrpcResponse.cs | 2 +- .../ExportClient/ExportClientHttpResponse.cs | 2 +- .../ExportClient/ExportClientResponse.cs | 4 +- .../ExportClient/IExportClient.cs | 3 +- .../ExportClient/OtlpGrpcLogExportClient.cs | 8 ++- .../OtlpGrpcMetricsExportClient.cs | 8 ++- .../ExportClient/OtlpGrpcTraceExportClient.cs | 8 ++- .../OtlpExporterTransmissionHandler.cs | 15 ++++-- .../OtlpExporterOptionsExtensions.cs | 39 ++++++++++++-- .../Exporter/OtlpGrpcExporterBenchmarks.cs | 2 +- .../Exporter/OtlpHttpExporterBenchmarks.cs | 2 +- .../OtlpHttpTraceExportClientTests.cs | 7 ++- .../OtlpExporterOptionsExtensionsTests.cs | 52 +++++++++++++++++++ .../OtlpLogExporterTests.cs | 15 +++--- .../OtlpRetryTests.cs | 4 +- .../OtlpTraceExporterTests.cs | 5 +- .../TestExportClient.cs | 6 +-- 19 files changed, 144 insertions(+), 54 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs index 4bb48cb8fe6..5565c9ca8ca 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs @@ -13,7 +13,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie /// Type of export request. internal abstract class BaseOtlpGrpcExportClient : IExportClient { - protected static readonly ExportClientGrpcResponse SuccessExportResponse = new ExportClientGrpcResponse(success: true, deadlineUtc: null, exception: null); + protected static readonly ExportClientGrpcResponse SuccessExportResponse = new ExportClientGrpcResponse(success: true, deadlineUtc: default, exception: null); protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) { @@ -40,7 +40,7 @@ protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) internal int TimeoutMilliseconds { get; } /// - public abstract ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default); + public abstract ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default); /// public virtual bool Shutdown(int timeoutMilliseconds) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs index 213316cf8ce..4fedc6b6176 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs @@ -12,7 +12,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie /// Type of export request. internal abstract class BaseOtlpHttpExportClient : IExportClient { - private static readonly ExportClientHttpResponse SuccessExportResponse = new ExportClientHttpResponse(success: true, deadlineUtc: null, response: null, exception: null); + private static readonly ExportClientHttpResponse SuccessExportResponse = new ExportClientHttpResponse(success: true, deadlineUtc: default, response: null, exception: null); protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) { @@ -36,12 +36,8 @@ protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpC internal IReadOnlyDictionary Headers { get; } /// - public ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default) + public ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, 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 { using var httpRequest = this.CreateHttpRequest(request); @@ -54,7 +50,7 @@ public ExportClientResponse SendExportRequest(TRequest request, CancellationToke } catch (HttpRequestException ex) { - return new ExportClientHttpResponse(success: false, deadlineUtc: deadline, response: httpResponse, ex); + return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: httpResponse, ex); } // We do not need to return back response and deadline for successful response so using cached value. @@ -64,7 +60,7 @@ public ExportClientResponse SendExportRequest(TRequest request, CancellationToke { OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return new ExportClientHttpResponse(success: false, deadlineUtc: deadline, response: null, exception: ex); + return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: null, exception: ex); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs index cadecc5b3c8..f7c95107a7a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs @@ -9,7 +9,7 @@ internal sealed class ExportClientGrpcResponse : ExportClientResponse { public ExportClientGrpcResponse( bool success, - DateTime? deadlineUtc, + DateTime deadlineUtc, Exception? exception) : base(success, deadlineUtc, exception) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs index a3c6e581b93..9d274b0ffed 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs @@ -15,7 +15,7 @@ internal sealed class ExportClientHttpResponse : ExportClientResponse { public ExportClientHttpResponse( bool success, - DateTime? deadlineUtc, + DateTime deadlineUtc, HttpResponseMessage? response, Exception? exception) : base(success, deadlineUtc, exception) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs index 2113e96d870..49f8c0eb209 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie internal abstract class ExportClientResponse { - protected ExportClientResponse(bool success, DateTime? deadlineUtc, Exception? exception) + protected ExportClientResponse(bool success, DateTime deadlineUtc, Exception? exception) { this.Success = success; this.Exception = exception; @@ -18,5 +18,5 @@ protected ExportClientResponse(bool success, DateTime? deadlineUtc, Exception? e public Exception? Exception { get; } - public DateTime? DeadlineUtc { get; } + public DateTime DeadlineUtc { get; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs index a13a63e743c..0c44da6ef30 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs @@ -13,9 +13,10 @@ internal interface IExportClient /// Method for sending export request to the server. /// /// The request to send to the server. + /// The deadline time in utc for export request to finish. /// An optional token for canceling the call. /// . - ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default); + ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default); /// /// Method for shutting down the export client. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs index 4caf8a7d6ae..b45837cd820 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs @@ -26,13 +26,11 @@ public OtlpGrpcLogExportClient(OtlpExporterOptions options, OtlpCollector.LogsSe } /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, CancellationToken cancellationToken = default) + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); - try { - this.logsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); + this.logsClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); // We do not need to return back response and deadline for successful response so using cached value. return SuccessExportResponse; @@ -41,7 +39,7 @@ public override ExportClientResponse SendExportRequest(OtlpCollector.ExportLogsS { OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs index 666413c874f..b156f6c6d02 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs @@ -26,13 +26,11 @@ public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.Me } /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, CancellationToken cancellationToken = default) + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); - try { - this.metricsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); + this.metricsClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); // We do not need to return back response and deadline for successful response so using cached value. return SuccessExportResponse; @@ -41,7 +39,7 @@ public override ExportClientResponse SendExportRequest(OtlpCollector.ExportMetri { OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs index 6918189f5c7..b38fbe2e471 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs @@ -26,13 +26,11 @@ public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.Trac } /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, CancellationToken cancellationToken = default) + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); - try { - this.traceClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); + this.traceClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); // We do not need to return back response and deadline for successful response so using cached value. return SuccessExportResponse; @@ -41,7 +39,7 @@ public override ExportClientResponse SendExportRequest(OtlpCollector.ExportTrace { OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs index 71f4d5ebac1..3300c8f6352 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs @@ -11,14 +11,17 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmissi internal class OtlpExporterTransmissionHandler { - public OtlpExporterTransmissionHandler(IExportClient exportClient) + public OtlpExporterTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds) { Guard.ThrowIfNull(exportClient); this.ExportClient = exportClient; + this.TimeoutMilliseconds = timeoutMilliseconds; } - protected IExportClient ExportClient { get; } + internal IExportClient ExportClient { get; } + + internal double TimeoutMilliseconds { get; } /// /// Attempts to send an export request to the server. @@ -31,7 +34,8 @@ public bool TrySubmitRequest(TRequest request) { try { - var response = this.ExportClient.SendExportRequest(request); + var deadlineUtc = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + var response = this.ExportClient.SendExportRequest(request, deadlineUtc); if (response.Success) { return true; @@ -103,12 +107,13 @@ protected virtual bool OnSubmitRequestFailure(TRequest request, ExportClientResp /// Fired when resending a request to the server. /// /// The request to be resent to the server. + /// The deadline time in utc for export request to finish. /// . /// If the retry succeeds; otherwise, . - protected bool TryRetryRequest(TRequest request, out ExportClientResponse response) + protected bool TryRetryRequest(TRequest request, DateTime deadlineUtc, out ExportClientResponse response) { - response = this.ExportClient.SendExportRequest(request); + response = this.ExportClient.SendExportRequest(request, deadlineUtc); if (!response.Success) { OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(response.Exception, isRetry: true); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index 44133af1f84..0ee3ee06a44 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -89,13 +89,46 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac } public static OtlpExporterTransmissionHandler GetTraceExportTransmissionHandler(this OtlpExporterOptions options) - => new(GetTraceExportClient(options)); + { + var exportClient = GetTraceExportClient(options); + + // `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. + double timeoutMilliseconds = exportClient is OtlpHttpTraceExportClient httpTraceExportClient + ? httpTraceExportClient.HttpClient.Timeout.TotalMilliseconds + : options.TimeoutMilliseconds; + + return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); + } public static OtlpExporterTransmissionHandler GetMetricsExportTransmissionHandler(this OtlpExporterOptions options) - => new(GetMetricsExportClient(options)); + { + var exportClient = GetMetricsExportClient(options); + + // `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. + double timeoutMilliseconds = exportClient is OtlpHttpMetricsExportClient httpMetricsExportClient + ? httpMetricsExportClient.HttpClient.Timeout.TotalMilliseconds + : options.TimeoutMilliseconds; + + return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); + } public static OtlpExporterTransmissionHandler GetLogsExportTransmissionHandler(this OtlpExporterOptions options) - => new(GetLogExportClient(options)); + { + var exportClient = GetLogExportClient(options); + + // `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. + double timeoutMilliseconds = exportClient is OtlpHttpLogExportClient httpLogExportClient + ? httpLogExportClient.HttpClient.Timeout.TotalMilliseconds + : options.TimeoutMilliseconds; + + return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); + } public static IExportClient GetTraceExportClient(this OtlpExporterOptions options) => options.Protocol switch diff --git a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index f80d59d2a14..86f3573e21e 100644 --- a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs @@ -35,7 +35,7 @@ public void GlobalSetup() this.exporter = new OtlpTraceExporter( options, new SdkLimitOptions(), - new OtlpExporterTransmissionHandler(new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient()))); + new OtlpExporterTransmissionHandler(new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient()), options.TimeoutMilliseconds)); 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 86e79812be0..65259aabd2f 100644 --- a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs @@ -63,7 +63,7 @@ public void GlobalSetup() this.exporter = new OtlpTraceExporter( options, new SdkLimitOptions(), - new OtlpExporterTransmissionHandler(new OtlpHttpTraceExportClient(options, options.HttpClientFactory()))); + new OtlpExporterTransmissionHandler(new OtlpHttpTraceExportClient(options, options.HttpClientFactory()), options.TimeoutMilliseconds)); this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index ad216916c1f..7593a672873 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -83,7 +83,9 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( var httpRequestContent = Array.Empty(); - var exportClient = new OtlpHttpTraceExportClient(options, new HttpClient(testHttpHandler)); + var httpClient = new HttpClient(testHttpHandler); + + var exportClient = new OtlpHttpTraceExportClient(options, httpClient); var resourceBuilder = ResourceBuilder.CreateEmpty(); if (includeServiceNameInResource) @@ -125,12 +127,13 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( void RunTest(Batch batch) { + var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); var request = new OtlpCollector.ExportTraceServiceRequest(); request.AddBatch(DefaultSdkLimitOptions, resourceBuilder.Build().ToOtlpResource(), batch); // Act - var result = exportClient.SendExportRequest(request); + var result = exportClient.SendExportRequest(request, deadlineUtc); var httpRequest = testHttpHandler.HttpRequestMessage; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index 37a41697f26..dd8464d9145 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -1,7 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK +using System.Net.Http; +#endif using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using Xunit; using Xunit.Sdk; @@ -157,4 +161,52 @@ public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri, Assert.Equal(expectedUri, resultUri.AbsoluteUri); } + + [Theory] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), true, 8000)] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcMetricsExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), true, 8000)] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcLogExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), false, 10000)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), true, 8000)] + public void GetTransmissionHandler_InitializesCorrectExportClientAndTimeoutValue(OtlpExportProtocol protocol, Type exportClientType, bool customHttpClient, int expectedTimeoutMilliseconds) + { + var exporterOptions = new OtlpExporterOptions() { Protocol = protocol }; + if (customHttpClient) + { + exporterOptions.HttpClientFactory = () => + { + return new HttpClient() { Timeout = TimeSpan.FromMilliseconds(expectedTimeoutMilliseconds) }; + }; + } + + if (exportClientType == typeof(OtlpGrpcTraceExportClient) || exportClientType == typeof(OtlpHttpTraceExportClient)) + { + var transmissionHandler = exporterOptions.GetTraceExportTransmissionHandler(); + + AssertTransmissionHandlerProperties(transmissionHandler, exportClientType, expectedTimeoutMilliseconds); + } + else if (exportClientType == typeof(OtlpGrpcMetricsExportClient) || exportClientType == typeof(OtlpHttpMetricsExportClient)) + { + var transmissionHandler = exporterOptions.GetMetricsExportTransmissionHandler(); + + AssertTransmissionHandlerProperties(transmissionHandler, exportClientType, expectedTimeoutMilliseconds); + } + else + { + var transmissionHandler = exporterOptions.GetLogsExportTransmissionHandler(); + + AssertTransmissionHandlerProperties(transmissionHandler, exportClientType, expectedTimeoutMilliseconds); + } + } + + private static void AssertTransmissionHandlerProperties(OtlpExporterTransmissionHandler transmissionHandler, Type exportClientType, int expectedTimeoutMilliseconds) + { + Assert.Equal(exportClientType, transmissionHandler.ExportClient.GetType()); + + Assert.Equal(expectedTimeoutMilliseconds, transmissionHandler.TimeoutMilliseconds); + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 87a6011f2f5..511e2f55876 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -732,11 +732,12 @@ public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() { // Arrange. var testExportClient = new TestExportClient(); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var exporterOptions = new OtlpExporterOptions(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( - new OtlpExporterOptions(), + exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), transmissionHandler); @@ -753,11 +754,12 @@ public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() { // Arrange. var testExportClient = new TestExportClient(throwException: true); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var exporterOptions = new OtlpExporterOptions(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( - new OtlpExporterOptions(), + exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), transmissionHandler); @@ -774,11 +776,12 @@ public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() { // Arrange. var testExportClient = new TestExportClient(); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var exporterOptions = new OtlpExporterOptions(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); var sut = new OtlpLogExporter( - new OtlpExporterOptions(), + exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), transmissionHandler); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs index 9b45b441317..4f7a0bef5f6 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs @@ -343,7 +343,9 @@ internal HttpRetryAttempt( responseMessage.StatusCode = (HttpStatusCode)statusCode; } - this.Response = new ExportClientHttpResponse(expectedSuccess, isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : null, responseMessage, new HttpRequestException()); + // Using arbitrary +1 hr for deadline for test purposes. + var deadlineUtc = isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : DateTime.UtcNow.AddHours(1); + this.Response = new ExportClientHttpResponse(expectedSuccess, deadlineUtc, responseMessage, new HttpRequestException()); this.Deadline = isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : null; this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 0c7a5db76e2..e3a5ad52b86 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -630,9 +630,10 @@ public void Shutdown_ClientShutdownIsCalled() { var exportClientMock = new TestExportClient(); - var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock); + var exporterOptions = new OtlpExporterOptions(); + var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock, exporterOptions.TimeoutMilliseconds); - var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, transmissionHandler); + var exporter = new OtlpTraceExporter(exporterOptions, DefaultSdkLimitOptions, transmissionHandler); exporter.Shutdown(); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs index c16a4f1665a..eab9178db49 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs @@ -13,7 +13,7 @@ internal class TestExportClient(bool throwException = false) : IExportClient< public bool ThrowException { get; set; } = throwException; - public ExportClientResponse SendExportRequest(T request, CancellationToken cancellationToken = default) + public ExportClientResponse SendExportRequest(T request, DateTime deadlineUtc, CancellationToken cancellationToken = default) { if (this.ThrowException) { @@ -21,7 +21,7 @@ public ExportClientResponse SendExportRequest(T request, CancellationToken cance } this.SendExportRequestCalled = true; - return new TestExportClientResponse(true, null, null); + return new TestExportClientResponse(true, deadlineUtc, null); } public bool Shutdown(int timeoutMilliseconds) @@ -32,7 +32,7 @@ public bool Shutdown(int timeoutMilliseconds) private class TestExportClientResponse : ExportClientResponse { - public TestExportClientResponse(bool success, DateTime? deadline, Exception exception) + public TestExportClientResponse(bool success, DateTime deadline, Exception exception) : base(success, deadline, exception) { } From adc89d94249446cbd153b99330f274a76a33ca09 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 11 Mar 2024 14:05:53 -0700 Subject: [PATCH 31/31] [otlp] Nullable annotations for the OtlpExporterOptions class and some xml doc improvement (#5434) --- .../.publicApi/Stable/PublicAPI.Shipped.txt | 16 +-- .../CHANGELOG.md | 4 + .../OtlpExporterOptions.cs | 102 +++++++++++++----- .../OtlpLogExporterHelperExtensions.cs | 4 +- .../OtlpExporterOptionsTests.cs | 18 +++- .../OtlpLogExporterTests.cs | 2 +- .../OtlpMetricsExporterTests.cs | 6 -- .../OtlpTraceExporterTests.cs | 6 -- 8 files changed, 106 insertions(+), 52 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt index 4b7af046438..30b70382df5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,12 +1,12 @@ #nullable enable -~OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -~OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -~OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -~OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -~OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -~OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -~OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func -~OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions! +OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri! +OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string? +OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func! +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void ~OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void ~OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void ~override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index d5d71920bd0..e88f15d58dd 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -35,6 +35,10 @@ ExponentialHistograms. ([#5397](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5397)) +* Setting `Endpoint` or `HttpClientFactory` properties on `OtlpExporterOptions` + to `null` will now result in an `ArgumentNullException` being thrown. + ([#5434](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5434)) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 0cffd50806c..0523b12d99a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + using System.Diagnostics; using System.Reflection; #if NETFRAMEWORK @@ -10,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Internal; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter; @@ -38,7 +39,8 @@ public class OtlpExporterOptions private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; - private Uri endpoint; + private Uri? endpoint; + private Func? httpClientFactory; /// /// Initializes a new instance of the class. @@ -66,7 +68,7 @@ internal OtlpExporterOptions( this.ApplyConfiguration(configuration, configurationType); - this.HttpClientFactory = this.DefaultHttpClientFactory = () => + this.DefaultHttpClientFactory = () => { return new HttpClient { @@ -74,16 +76,41 @@ internal OtlpExporterOptions( }; }; - this.BatchExportProcessorOptions = defaultBatchOptions; + this.BatchExportProcessorOptions = defaultBatchOptions!; } /// - /// Gets or sets the target to which the exporter is going to send telemetry. - /// Must be a valid Uri with scheme (http or https) and host, and - /// may contain a port and path. The default value is - /// * http://localhost:4317 for - /// * http://localhost:4318 for . + /// Gets or sets the target to which the exporter is going to send + /// telemetry. /// + /// + /// Notes: + /// + /// When setting the value must be a valid with scheme (http or https) and host, and may contain a + /// port and path. + /// The default value when not set is based on the property: + /// + /// http://localhost:4317 for . + /// http://localhost:4318 for . + /// + /// When is set to and has + /// not been set the default value (http://localhost:4318) will have + /// a signal-specific path appended. The final default endpoint values will + /// be constructed as: + /// + /// Logging: http://localhost:4318/v1/logs + /// Metrics: http://localhost:4318/v1/metrics + /// Tracing: http://localhost:4318/v1/traces + /// + /// + /// + /// + /// public Uri Endpoint { get @@ -100,24 +127,32 @@ public Uri Endpoint set { + Guard.ThrowIfNull(value); + this.endpoint = value; this.AppendSignalPathToEndpoint = false; } } /// - /// Gets or sets optional headers for the connection. Refer to the - /// specification for information on the expected format for Headers. + /// Gets or sets optional headers for the connection. /// - public string Headers { get; set; } + /// + /// Note: Refer to the + /// OpenTelemetry Specification for details on the format of . + /// + public string? Headers { get; set; } /// - /// Gets or sets the max waiting time (in milliseconds) for the backend to process each batch. The default value is 10000. + /// Gets or sets the max waiting time (in milliseconds) for the backend to + /// process each batch. Default value: 10000. /// public int TimeoutMilliseconds { get; set; } = 10000; /// - /// Gets or sets the the OTLP transport protocol. Supported values: Grpc and HttpProtobuf. + /// Gets or sets the the OTLP transport protocol. /// public OtlpExportProtocol Protocol { get; set; } = DefaultOtlpExportProtocol; @@ -144,27 +179,38 @@ public Uri Endpoint /// /// This is only invoked for the protocol. - /// The default behavior when using the extension is if an The default behavior when using tracing registration extensions is + /// if an IHttpClientFactory /// instance can be resolved through the application then an will be - /// created through the factory with the name "OtlpTraceExporter" - /// otherwise an will be instantiated - /// directly. - /// The default behavior when using the extension is if an will be instantiated directly. + /// The default behavior when using metrics registration extensions is + /// if an IHttpClientFactory /// instance can be resolved through the application then an will be - /// created through the factory with the name "OtlpMetricExporter" - /// otherwise an will be instantiated - /// directly. + /// created through the factory with the name "OtlpMetricExporter" otherwise + /// an will be instantiated directly. + /// + /// The default behavior when using logging registration extensions is an + /// will be instantiated directly. IHttpClientFactory + /// is not currently supported for logging. + /// /// /// - public Func HttpClientFactory { get; set; } + public Func HttpClientFactory + { + get => this.httpClientFactory ?? this.DefaultHttpClientFactory; + set + { + Guard.ThrowIfNull(value); + + this.httpClientFactory = value; + } + } /// /// Gets a value indicating whether or not the signal-specific path should @@ -228,7 +274,7 @@ private static string GetUserAgentString() try { var assemblyVersion = typeof(OtlpExporterOptions).Assembly.GetCustomAttribute(); - var informationalVersion = assemblyVersion.InformationalVersion; + var informationalVersion = assemblyVersion?.InformationalVersion; return string.IsNullOrEmpty(informationalVersion) ? UserAgentProduct : $"{UserAgentProduct}/{informationalVersion}"; } catch (Exception) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs index 85298517f95..1f99374c324 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs @@ -60,7 +60,7 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( return loggerOptions.AddProcessor(sp => { - var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); @@ -104,7 +104,7 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( return loggerOptions.AddProcessor(sp => { - var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 4ba49ec760a..bf55abb4795 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -226,7 +226,7 @@ public void OtlpExporterOptions_EndpointGetterUsesProtocolWhenNull() } [Fact] - public void OtlpExporterOptions_EndpointGetterIgnoresProtocolWhenNotNull() + public void OtlpExporterOptions_EndpointThrowsWhenSetToNull() { var options = new OtlpExporterOptions { Endpoint = new Uri("http://test:8888"), Protocol = OtlpExportProtocol.Grpc }; @@ -243,6 +243,22 @@ public void OtlpExporterOptions_EnvironmentVariableNames() Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName); } + [Fact] + public void OtlpExporterOptions_SettingEndpointToNullResetsAppendSignalPathToEndpoint() + { + var options = new OtlpExporterOptions(OtlpExporterOptionsConfigurationType.Default); + + Assert.Throws(() => options.Endpoint = null); + } + + [Fact] + public void OtlpExporterOptions_HttpClientFactoryThrowsWhenSetToNull() + { + var options = new OtlpExporterOptions(OtlpExporterOptionsConfigurationType.Default); + + Assert.Throws(() => options.HttpClientFactory = null); + } + private static void ClearEnvVars() { foreach (var item in GetOtlpExporterOptionsTestCases()) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 511e2f55876..92720ad0302 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -101,7 +101,7 @@ public void UserHttpFactoryCalledWhenUsingHttpProtobuf() Assert.Equal(2, invocations); } - options.HttpClientFactory = null; + options.HttpClientFactory = () => null; Assert.Throws(() => { using var exporter = new OtlpLogExporter(options); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 7322c4d90e5..451362ed927 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -131,12 +131,6 @@ public void UserHttpFactoryCalled() Assert.Equal(2, invocations); } - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new OtlpMetricExporter(options); - }); - options.HttpClientFactory = () => null; Assert.Throws(() => { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index e3a5ad52b86..527fb5de72b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -96,12 +96,6 @@ public void UserHttpFactoryCalled() Assert.Equal(2, invocations); } - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new OtlpTraceExporter(options); - }); - options.HttpClientFactory = () => null; Assert.Throws(() => {