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: 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 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 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.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/.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 f862133e31f..e88f15d58dd 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -30,6 +30,15 @@ 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)) + +* 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/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 56f0118aa87..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) { @@ -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; @@ -36,9 +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) { - DateTime deadline = DateTime.UtcNow.AddMilliseconds(this.HttpClient.Timeout.TotalMilliseconds); try { using var httpRequest = this.CreateHttpRequest(request); @@ -51,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. @@ -61,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/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/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index d93105e3dfb..fb9266cb613 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; @@ -157,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); } @@ -184,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); } @@ -205,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); } @@ -226,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); } @@ -267,37 +308,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)); } } @@ -344,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); } @@ -357,73 +380,70 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) return otlpMetric; } - private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + internal static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + where T : struct { - foreach (var tag in tags) + var otlpExemplar = new OtlpMetrics.Exemplar { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } + TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(), + }; - private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) - { - foreach (var tag in meterTags) + if (exemplar.TraceId != default) { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } + byte[] traceIdBytes = new byte[16]; + exemplar.TraceId.CopyTo(traceIdBytes); - /* - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) - { - var otlpExemplar = new OtlpMetrics.Exemplar(); + byte[] spanIdBytes = new byte[8]; + exemplar.SpanId.CopyTo(spanIdBytes); + + otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + } - if (exemplar.Value is double doubleValue) + if (typeof(T) == typeof(long)) { - otlpExemplar.AsDouble = doubleValue; + otlpExemplar.AsInt = (long)(object)value; } - else if (exemplar.Value is long longValue) + else if (typeof(T) == typeof(double)) { - otlpExemplar.AsInt = longValue; + otlpExemplar.AsDouble = (double)(object)value; } else { - // 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(); + Debug.Fail("Unexpected type"); + otlpExemplar.AsDouble = Convert.ToDouble(value); } - otlpExemplar.TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(); - - // TODO: Do the TagEnumerationState thing. foreach (var tag in exemplar.FilteredTags) { - otlpExemplar.FilteredAttributes.Add(tag.ToOtlpAttribute()); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + otlpExemplar.FilteredAttributes.Add(result); + } } - if (exemplar.TraceId != default) + return otlpExemplar; + } + + private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + { + foreach (var tag in tags) { - byte[] traceIdBytes = new byte[16]; - exemplar.TraceId.CopyTo(traceIdBytes); - otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } } + } - if (exemplar.SpanId != default) + private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) + { + foreach (var tag in meterTags) { - byte[] spanIdBytes = new byte[8]; - exemplar.SpanId.CopyTo(spanIdBytes); - otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } } - - return otlpExemplar; } - */ } 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/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/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/Implementation/Transmission/OtlpExporterTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs new file mode 100644 index 00000000000..3300c8f6352 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs @@ -0,0 +1,125 @@ +// 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, double timeoutMilliseconds) + { + Guard.ThrowIfNull(exportClient); + + this.ExportClient = exportClient; + this.TimeoutMilliseconds = timeoutMilliseconds; + } + + internal IExportClient ExportClient { get; } + + internal double TimeoutMilliseconds { 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 deadlineUtc = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + var response = this.ExportClient.SendExportRequest(request, deadlineUtc); + 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. + /// The deadline time in utc for export request to finish. + /// . + /// If the retry succeeds; otherwise, . + protected bool TryRetryRequest(TRequest request, DateTime deadlineUtc, out ExportClientResponse response) + { + response = this.ExportClient.SendExportRequest(request, deadlineUtc); + if (!response.Success) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(response.Exception, isRetry: true); + return false; + } + + return true; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index e0a685a9873..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,22 +12,23 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Internal; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; 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 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 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[] { @@ -34,52 +37,38 @@ 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; + private Uri? endpoint; + private Func? httpClientFactory; /// /// 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(EndpointEnvVarName, out var endpoint)) - { - this.endpoint = endpoint; - } + this.ApplyConfiguration(configuration, configurationType); - if (configuration.TryGetStringValue(HeadersEnvVarName, out var headers)) - { - this.Headers = headers; - } - - if (configuration.TryGetIntValue(TimeoutEnvVarName, out var timeout)) - { - this.TimeoutMilliseconds = timeout; - } - - if (configuration.TryGetValue( - ProtocolEnvVarName, - OtlpExportProtocolParser.TryParse, - out var protocol)) - { - this.Protocol = protocol; - } - - this.HttpClientFactory = this.DefaultHttpClientFactory = () => + this.DefaultHttpClientFactory = () => { return new HttpClient { @@ -87,19 +76,40 @@ 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. /// /// - /// When using , the full URL MUST be provided, including the signal-specific path v1/{signal}. - /// For example, for traces, the full URL will look like http://your-custom-endpoint/v1/traces. + /// 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 { @@ -107,7 +117,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); } @@ -117,24 +127,32 @@ public Uri Endpoint set { + Guard.ThrowIfNull(value); + this.endpoint = value; - this.ProgrammaticallyModifiedEndpoint = true; + 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; @@ -161,32 +179,48 @@ 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 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) { @@ -199,14 +233,48 @@ 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 { var assemblyVersion = typeof(OtlpExporterOptions).Assembly.GetCustomAttribute(); - var informationalVersion = assemblyVersion.InformationalVersion; + var informationalVersion = assemblyVersion?.InformationalVersion; return string.IsNullOrEmpty(informationalVersion) ? UserAgentProduct : $"{UserAgentProduct}/{informationalVersion}"; } catch (Exception) @@ -214,4 +282,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/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index 91a23749804..0ee3ee06a44 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,48 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac return headers; } + public static OtlpExporterTransmissionHandler GetTraceExportTransmissionHandler(this OtlpExporterOptions 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) + { + 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) + { + 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/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/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/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/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index f6e3be715d7..37a66dc2f10 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[OtlpSpecConfigDefinitions.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[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) { 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/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index 16832495101..8c1a746f041 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -5,23 +5,39 @@ 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 -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.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! +OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId +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> +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.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> 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.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! @@ -31,20 +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 -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 -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/.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/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/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/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 6260596c78c..b3e5a11e449 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. @@ -42,6 +47,36 @@ [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)) + +* **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)) + +* **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)) + +* **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/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/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/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index a5d603589ad..17e20ad42be 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,20 +14,25 @@ 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; + internal readonly int NumberOfMetricPoints; internal readonly bool EmitOverflowAttribute; internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; + 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(); - private readonly HashSet? tagKeysInteresting; private readonly int tagsKeysInterestingCount; // This holds the reclaimed MetricPoints that are available for reuse. @@ -43,7 +51,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; @@ -59,21 +67,26 @@ internal AggregatorStore( int cardinalityLimit, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints, - ExemplarFilter? exemplarFilter = null) + ExemplarFilterType? exemplarFilter = null, + 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); this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize; this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; this.StartTimeExclusive = DateTimeOffset.UtcNow; - this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + this.ExemplarReservoirFactory = exemplarReservoirFactory; if (metricStreamIdentity.TagKeys == null) { this.updateLongCallback = this.UpdateLong; @@ -83,38 +96,44 @@ 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); - this.tagKeysInteresting = hs; +#endif + this.TagKeysInteresting = hs; this.tagsKeysInterestingCount = hs.Count; } this.EmitOverflowAttribute = emitOverflowAttribute; - var reservedMetricPointsCount = 1; + this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + Debug.Assert( + this.exemplarFilter == ExemplarFilterType.AlwaysOff + || this.exemplarFilter == ExemplarFilterType.AlwaysOn + || this.exemplarFilter == ExemplarFilterType.TraceBased, + "this.exemplarFilter had an unexpected value"); - 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); } @@ -141,17 +160,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() @@ -163,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); } @@ -224,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) @@ -248,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]; @@ -261,9 +293,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"); @@ -439,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 @@ -468,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 @@ -495,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 @@ -517,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 @@ -921,177 +951,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); - } + var index = this.FindMetricAggregatorsDefault(tags); - 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.UpdateLongMetricPoint(index, value, tags); } 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: 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); - - 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.UpdateDoubleMetricPoint(index, value, tags); } 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: 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); } } @@ -1122,9 +1086,9 @@ private int FindMetricAggregatorsCustomTag(ReadOnlySpan - /// 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: + case ExemplarFilterType.AlwaysOff: + case ExemplarFilterType.TraceBased: + 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/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs index 4f10f3e2527..ce99dd85f74 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -6,94 +6,30 @@ namespace OpenTelemetry.Metrics; /// -/// The AlignedHistogramBucketExemplarReservoir implementation. +/// AlignedHistogramBucketExemplarReservoir implementation. /// -internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir +/// +/// Specification: . +/// +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/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs deleted file mode 100644 index b6c7973b9d3..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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 ExemplarFilter which makes no measurements eligible for being an Exemplar. -/// Using this ExemplarFilter is as good as disabling Exemplar feature. -/// -/// -#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 -{ - /// - 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 7be5d04db0f..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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 ExemplarFilter which makes all measurements eligible for being an Exemplar. -/// -/// -#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 -{ - /// - 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/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index d635a1a4ad4..d9eda128f12 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,58 +1,182 @@ // 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; using OpenTelemetry.Internal; #endif -using System.Diagnostics; - 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 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 { +#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; + 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; + } + + if (this.ViewDefinedTagKeys != null) + { + 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/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs deleted file mode 100644 index 3af62b66c65..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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 -/// -/// The base class for defining Exemplar Filter. -/// -/// -#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 -{ - /// - /// 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/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/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs new file mode 100644 index 00000000000..8c86753e7a2 --- /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..9fd1fc1b9f8 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -4,37 +4,47 @@ namespace OpenTelemetry.Metrics; /// -/// The base class for defining Exemplar Reservoir. +/// ExemplarReservoir base implementation and contract. /// +/// +/// Specification: . +/// 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..4a43bf3ebc6 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -0,0 +1,139 @@ +// 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 +{ + internal static readonly ReadOnlyExemplarCollection Empty = new(Array.Empty()); + 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 maximumCount = this.MaximumCount; + + if (maximumCount > 0) + { + 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 Empty; + } + + internal IReadOnlyList ToReadOnlyList() + { + var list = new List(this.MaximumCount); + + foreach (var exemplar in this) + { + // 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; + } + + /// + /// 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..34dc945fabb 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -1,140 +1,60 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; - namespace OpenTelemetry.Metrics; /// -/// The SimpleFixedSizeExemplarReservoir implementation. +/// SimpleFixedSizeExemplarReservoir implementation. /// -internal sealed class SimpleFixedSizeExemplarReservoir : ExemplarReservoir +/// +/// Specification: . +/// +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/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs deleted file mode 100644 index 6d176f8a169..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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 ExemplarFilter which makes those measurements eligible for being an Exemplar, -/// which are recorded in the context of a sampled parent activity (span). -/// -/// -#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 -{ - /// - 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/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/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index cfd6b4e3463..32b107c5b2f 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) + ExemplarFilterType? 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 38ee0b18c86..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; @@ -62,13 +57,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); } @@ -84,13 +91,17 @@ 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 { this.mpComponents = null; } - if (aggregatorStore!.IsExemplarEnabled() && reservoir == null) + if (isExemplarEnabled && reservoir == null) { reservoir = new SimpleFixedSizeExemplarReservoir(DefaultSimpleReservoirPoolSize); } @@ -102,6 +113,8 @@ internal MetricPoint( this.mpComponents = new MetricPointOptionalComponents(); } + reservoir.Initialize(aggregatorStore); + this.mpComponents.ExemplarReservoir = reservoir; } @@ -137,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; /// @@ -345,22 +364,19 @@ 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(out ReadOnlyExemplarCollection exemplars) { - // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents?.Exemplars ?? Array.Empty(); + exemplars = this.mpComponents?.Exemplars ?? ReadOnlyExemplarCollection.Empty; + return exemplars.MaximumCount > 0; } internal readonly MetricPoint Copy() @@ -429,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) @@ -463,14 +463,7 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) @@ -666,14 +613,7 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan> tags = default, bool reportExemplar = false, bool isSampled = false) + /// + /// 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"); @@ -1300,19 +1219,12 @@ private void UpdateHistogram(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"); @@ -1328,25 +1240,18 @@ private void UpdateHistogramWithMinMax(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(); @@ -1354,28 +1259,21 @@ private void UpdateHistogramWithBuckets(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(); @@ -1383,27 +1281,18 @@ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) { if (number < 0) { @@ -1423,12 +1312,12 @@ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) { if (number < 0) { @@ -1451,9 +1340,80 @@ private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySp histogram.RunningMax = Math.Max(histogram.RunningMax, number); } + 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"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + 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)); + } + } + } + + [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)] + private void CompleteUpdate() + { + // 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); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private readonly void ThrowNotSupportedMetricTypeException(string methodName) { diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index f028b2add56..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 Exemplar[]? Exemplars; + public ReadOnlyExemplarCollection Exemplars = ReadOnlyExemplarCollection.Empty; 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/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 9f3b6fa10d2..dfb95fab2b9 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -23,8 +23,8 @@ public abstract partial class MetricReader private Metric[]? metricsCurrentBatch; private int metricIndex = -1; private bool emitOverflowAttribute; - - private ExemplarFilter? exemplarFilter; + 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,16 +144,14 @@ internal virtual List AddMetricWithViews(Instrument instrument, List AddMetricWithViews(Instrument instrument, List 1) - { - this.emitOverflowAttribute = true; - } - } } private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric) 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/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs index c38379bb718..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)] @@ -133,6 +137,10 @@ public string[]? TagKeys } } + // TODO: Expose this to be complaint with the spec: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#stream-configuration + internal Func? ExemplarReservoirFactory { get; set; } + internal string[]? CopiedTagKeys { get; private set; } internal int? ViewId { get; set; } 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 new file mode 100644 index 00000000000..2c41020c767 --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -0,0 +1,131 @@ +// 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; +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 +{ +#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) + { + 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/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/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/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/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 } /// 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/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; diff --git a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index b8cffdbdb71..86f3573e21e 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()), 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 d4560579a92..65259aabd2f 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()), options.TimeoutMilliseconds)); this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); 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/BaseOtlpHttpExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs index 68011107b7f..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(OtlpExporterOptions.EndpointEnvVarName, 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(OtlpExporterOptions.EndpointEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, null); } } 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/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 619dd7999e9..bf55abb4795 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(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(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() { - [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", - [OtlpExporterOptions.HeadersEnvVarName] = "A=2,B=3", - [OtlpExporterOptions.TimeoutEnvVarName] = "2000", - [OtlpExporterOptions.ProtocolEnvVarName] = "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(OtlpExporterOptions.EndpointEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "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(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "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(OtlpExporterOptions.EndpointEnvVarName, "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_EndpointThrowsWhenSetToNull() { var options = new OtlpExporterOptions { Endpoint = new Uri("http://test:8888"), Protocol = OtlpExportProtocol.Grpc }; @@ -141,17 +237,36 @@ 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", 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); + } + + [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() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, 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 7181557ae5a..92720ad0302 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; @@ -100,7 +101,7 @@ public void UserHttpFactoryCalledWhenUsingHttpProtobuf() Assert.Equal(2, invocations); } - options.HttpClientFactory = null; + options.HttpClientFactory = () => null; Assert.Throws(() => { using var exporter = new OtlpLogExporter(options); @@ -731,13 +732,15 @@ public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() { // Arrange. var testExportClient = new 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(), - testExportClient); + transmissionHandler); // Act. sut.Export(emptyBatch); @@ -751,13 +754,15 @@ public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() { // Arrange. var testExportClient = new TestExportClient(throwException: true); + 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(), - testExportClient); + transmissionHandler); // Act. var result = sut.Export(emptyBatch); @@ -771,13 +776,15 @@ public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() { // Arrange. var testExportClient = new 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(), - testExportClient); + transmissionHandler); // Act. var result = sut.Export(emptyBatch); @@ -1513,7 +1520,7 @@ private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( { var values = new Dictionary() { - [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", + [OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", }; var configuration = new ConfigurationBuilder() diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 85713222ea6..451362ed927 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() { @@ -123,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(() => { @@ -216,14 +218,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 ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics) .Build(); @@ -277,29 +282,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 ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .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 +377,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 ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .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 +474,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 ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .AddInMemoryExporter(metrics, metricReaderOptions => { metricReaderOptions.TemporalityPreference = aggregationTemporality; @@ -485,7 +508,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 +610,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 ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) .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 +723,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 +770,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) + { + 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(ExemplarFilterType.AlwaysOn) + .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); + + 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.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 keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray(); - var values = keysValues?.Where((_, index) => index % 2 != 0).ToArray(); + var exemplar = getExemplarFunc(state); - for (var i = 0; keys != null && i < keys.Length; ++i) + 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); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs index f8815ed5665..4f7a0bef5f6 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,107 @@ 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; + } + + // 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; + this.ExpectedSuccess = expectedSuccess; + } + } + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 95f61d41297..527fb5de72b 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; @@ -95,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(() => { @@ -629,7 +624,10 @@ public void Shutdown_ClientShutdownIsCalled() { var exportClientMock = new TestExportClient(); - var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, exportClientMock); + var exporterOptions = new OtlpExporterOptions(); + var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock, exporterOptions.TimeoutMilliseconds); + + 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) { } 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.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.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/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..17360444c8e 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(ExemplarFilterType.AlwaysOn); + } + + 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.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/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.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/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..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. -Use the `SPACE` key to toggle the console output, which is off by default. +While a test is running... -Use the `ENTER` key to print the latest performance statistics. +* Use the `SPACE` key to toggle the console output, which is on by default. -Use the `ESC` key to exit the stress test. +* Use the `ENTER` key to print the latest performance statistics. + +* 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 @@ -76,52 +95,88 @@ process_runtime_dotnet_gc_allocations_size_bytes 5485192 1658950184752 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`](./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 @@ -130,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. 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; } +} 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] 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/Metrics/AggregatorTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs index 61d739d6a18..6bd96046d3f 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs @@ -335,7 +335,7 @@ internal void ExponentialHistogramTests(AggregationType aggregationType, Aggrega cardinalityLimit: 1024, this.emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints, - exemplarsEnabled ? new AlwaysOnExemplarFilter() : null); + exemplarsEnabled ? ExemplarFilterType.AlwaysOn : null); var expectedHistogram = new Base2ExponentialBucketHistogram(); 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/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index b5fd844f43c..2499c825b6a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -1,22 +1,58 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; 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; + private static readonly Func IsExemplarApiExposed = () => typeof(ExemplarFilterType).IsVisible; - public MetricExemplarTests(ITestOutputHelper output) + [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) { - this.output = output; + 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] @@ -28,35 +64,40 @@ 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()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("testCounter")) + { + return new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + + return null; + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(2, false, null); foreach (var value in measurementValues) { - counter.Add(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); - // 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); + ValidateFirstPhase("testCounterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testCounterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); exportedItems.Clear(); @@ -64,53 +105,222 @@ 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(); + 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); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + ValidateSecondPhase("testCounterDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.DoubleValue); + ValidateSecondPhase("testCounterLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, 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, getExemplarValueFunc); + } + + void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues, + 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); + + 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); + } } - [Fact] - public void TestExemplarsHistogram() + [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 histogram = meter.CreateHistogram("testHistogram"); + 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(ExemplarFilterType.AlwaysOn) + .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); + } + + 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 TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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()) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + RecordMinMax = false, + }; + } + }) .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); + 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, false); + + ValidateFirstPhase("histogramWithBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsLong", testStartTime, exportedItems, measurementValues); exportedItems.Clear(); @@ -118,82 +328,484 @@ public void TestExemplarsHistogram() Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); + var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray(); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + 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); + + 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 ValidateScondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + 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); + } + } + + [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(ExemplarFilterType.AlwaysOn) + .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(); - histogram.Record(value); + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.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); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + 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); + } } - [Fact] - public void TestExemplarsFilterTags() + [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 histogram = meter.CreateHistogram("testHistogram"); + 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(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("exponentialHistogramWithMinMax")) + { + return new Base2ExponentialBucketHistogramConfiguration(); + } + else + { + return new Base2ExponentialBucketHistogramConfiguration() + { + RecordMinMax = false, + }; + } + }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + metricReaderOptions.TemporalityPreference = temporality; })); - var measurementValues = GenerateRandomValues(10); + var measurementValues = GenerateRandomValues(20, false, null); foreach (var value in measurementValues) { - histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1")); + 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); + } + } + + [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); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); + var exemplars = GetExemplars(metricPoint.Value); + + if (enableTracing) + { + Assert.Single(exemplars); + } + else + { + Assert.Empty(exemplars); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestExemplarsFilterTags(bool enableTagFiltering) + { + 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 = enableTagFiltering ? new string[] { "key1" } : null, + ExemplarReservoirFactory = () => + { + if (testExemplarReservoir != null) + { + throw new InvalidOperationException(); + } + + return testExemplarReservoir = new TestExemplarReservoir(); + }, + }) + .AddInMemoryExporter(exportedItems)); + + 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"); + + var metricPoint = GetFirstMetricPoint(exportedItems); + + Assert.NotNull(metricPoint); + + var exemplars = GetExemplars(metricPoint.Value); + 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); + if (!enableTagFiltering) + { + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); + } + else + { + Assert.Equal(3, exemplar.FilteredTags.MaximumCount); + + var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); + + Assert.Equal(2, filteredTags.Count); + + Assert.Contains(new("key2", "value2"), filteredTags); + Assert.Contains(new("key3", "value3"), filteredTags); + } } } - 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() * 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; + } + + values[i] = (nextValue, expectTraceId); } 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, + IEnumerable<(double Value, bool ExpectTraceId)> measurementValues, + Func getExemplarValueFunc) { - 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.Null(exemplar.FilteredTags); - if (traceContextExists) + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); + + var measurement = measurementValues.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar) + || (long)v.Value == getExemplarValueFunc(exemplar)); + Assert.NotEqual(default, measurement); + if (measurement.ExpectTraceId) { Assert.NotEqual(default, exemplar.TraceId); Assert.NotEqual(default, exemplar.SpanId); @@ -203,6 +815,32 @@ private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset start Assert.Equal(default, exemplar.TraceId); Assert.Equal(default, exemplar.SpanId); } + + count++; + } + + 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(); } } } 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/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/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 0e5c1e1e53f..7d72b773ea6 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) { @@ -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.ToReadOnlyList(); + } + + return Array.Empty(); } #if BUILDING_HOSTING_TESTS 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)); 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; + } + } +} 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