diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..b0659b798bb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +OpenTelemetry.OpenTelemetryBuilderOtlpExporterExtensions +static OpenTelemetry.OpenTelemetryBuilderOtlpExporterExtensions.UseOtlpExporter(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderOtlpExporterExtensions.UseOtlpExporter(this OpenTelemetry.IOpenTelemetryBuilder! builder, OpenTelemetry.Exporter.OtlpExportProtocol protocol, System.Uri! baseEndpoint) -> OpenTelemetry.IOpenTelemetryBuilder! diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs new file mode 100644 index 00000000000..c0368cd4ae1 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs @@ -0,0 +1,149 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; + +namespace OpenTelemetry; + +/// +/// Contains extension methods to facilitate registration of the OpenTelemetry +/// Protocol (OTLP) exporter into an +/// instance. +/// +public static class OpenTelemetryBuilderOtlpExporterExtensions +{ + /// + /// Uses OpenTelemetry Protocol (OTLP) exporter for all signals. + /// + /// + /// Notes: + /// + /// Calling this method automatically enables logging, metrics, and + /// tracing. + /// The exporter registered by this method will be added as the last + /// processor in the pipeline established for logging and tracing. + /// This method can only be called once. Subsequent calls will results + /// in a being thrown. + /// This method cannot be called in addition to signal-specific + /// AddOtlpExporter methods. If this method is called signal-specific + /// AddOtlpExporter calls will result in a being thrown. + /// + /// + /// . + /// Supplied for chaining calls. + public static IOpenTelemetryBuilder UseOtlpExporter( + this IOpenTelemetryBuilder builder) + => UseOtlpExporter(builder, name: null, configuration: null, configure: null); + + /// + /// + /// + /// . + /// . + /// + /// Base endpoint to use. + /// Note: A signal-specific path will be appended to the base endpoint for + /// each signal automatically if the protocol is set to . + /// + public static IOpenTelemetryBuilder UseOtlpExporter( + this IOpenTelemetryBuilder builder, + OtlpExportProtocol protocol, + Uri baseEndpoint) + { + Guard.ThrowIfNull(baseEndpoint); + + return UseOtlpExporter(builder, name: null, configuration: null, configure: otlpBuilder => + { + otlpBuilder.ConfigureDefaultExporterOptions(o => + { + o.Protocol = protocol; + o.Endpoint = baseEndpoint; + }); + }); + } + + /// + /// + /// + /// . + /// Callback action for configuring . + internal static IOpenTelemetryBuilder UseOtlpExporter( + this IOpenTelemetryBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + return UseOtlpExporter(builder, name: null, configuration: null, configure); + } + + /// + /// + /// + /// . + /// + /// to bind onto . + /// Notes: + /// + /// See [TODO:Add doc link] for details on the configuration + /// schema. + /// The instance will be + /// named "otlp" by default when calling this method. + /// + /// + /// + internal static IOpenTelemetryBuilder UseOtlpExporter( + this IOpenTelemetryBuilder builder, + IConfiguration configuration) + { + Guard.ThrowIfNull(configuration); + + return UseOtlpExporter(builder, name: null, configuration, configure: null); + } + + /// + /// + /// + /// . + /// Optional name which is used when retrieving options. + /// + /// Optional to bind onto . + /// Notes: + /// + /// + /// If is not set the instance will be named "otlp" by + /// default when is used. + /// + /// + /// + /// Optional callback action for configuring . + internal static IOpenTelemetryBuilder UseOtlpExporter( + this IOpenTelemetryBuilder builder, + string? name, + IConfiguration? configuration, + Action? configure) + { + Guard.ThrowIfNull(builder); + + // Note: We automatically turn on signals for "UseOtlpExporter" + builder + .WithLogging() + .WithMetrics() + .WithTracing(); + + var otlpBuilder = new OtlpExporterBuilder(builder.Services, name, configuration); + + configure?.Invoke(otlpBuilder); + + return builder; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderServiceProviderExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderServiceProviderExtensions.cs new file mode 100644 index 00000000000..afa534f4160 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderServiceProviderExtensions.cs @@ -0,0 +1,27 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; + +namespace OpenTelemetry.Exporter; + +internal static class OpenTelemetryBuilderServiceProviderExtensions +{ + public static void EnsureSingleUseOtlpExporterRegistration(this IServiceProvider serviceProvider) + { + var registrations = serviceProvider.GetServices(); + if (registrations.Count() > 1) + { + throw new NotSupportedException("Multiple calls to UseOtlpExporter on the same IServiceCollection are not supported."); + } + } + + public static void EnsureNoUseOtlpExporterRegistrations(this IServiceProvider serviceProvider) + { + var registrations = serviceProvider.GetServices(); + if (registrations.Any()) + { + throw new NotSupportedException("Signal-specific AddOtlpExporter methods and the cross-cutting UseOtlpExporter method being invoked on the same IServiceCollection is not supported."); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs new file mode 100644 index 00000000000..4c3d37cf282 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs @@ -0,0 +1,260 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter; + +internal sealed class OtlpExporterBuilder +{ + private const int DefaultProcessorPipelineWeight = 10_000; + + private readonly string name; + + internal OtlpExporterBuilder( + IServiceCollection services, + string? name, + IConfiguration? configuration) + { + Debug.Assert(services != null, "services was null"); + + if (configuration != null) + { + if (string.IsNullOrEmpty(name)) + { + name = "otlp"; + } + + BindConfigurationToOptions(services!, name!, configuration); + } + + name ??= Options.DefaultName; + + RegisterOtlpExporterServices(services!, name!); + + this.name = name; + this.Services = services!; + } + + public IServiceCollection Services { get; } + + public OtlpExporterBuilder ConfigureDefaultExporterOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure( + this.name, + o => configure(o.DefaultOptions)); + return this; + } + + public OtlpExporterBuilder ConfigureLoggingExporterOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure( + this.name, + o => configure(o.LoggingOptions)); + return this; + } + + public OtlpExporterBuilder ConfigureLoggingProcessorOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure(this.name, configure); + return this; + } + + public OtlpExporterBuilder ConfigureMetricsExporterOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure( + this.name, + o => configure(o.MetricsOptions)); + return this; + } + + public OtlpExporterBuilder ConfigureMetricsReaderOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure(this.name, configure); + return this; + } + + public OtlpExporterBuilder ConfigureTracingExporterOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure( + this.name, + o => configure(o.TracingOptions)); + return this; + } + + public OtlpExporterBuilder ConfigureTracingProcessorOptions( + Action configure) + { + Guard.ThrowIfNull(configure); + + this.Services.Configure(this.name, configure); + return this; + } + + private static void BindConfigurationToOptions(IServiceCollection services, string name, IConfiguration configuration) + { + Debug.Assert(services != null, "services was null"); + Debug.Assert(!string.IsNullOrEmpty(name), "name was null or empty"); + Debug.Assert(configuration != null, "configuration was null"); + + /* Config JSON structure is expected to be something like this: + { + "DefaultOptions": { + "Endpoint": "http://default_endpoint/" + }, + "LoggingOptions": { + "Endpoint": "http://logs_endpoint/" + "ExportProcessorType": Batch, + "BatchExportProcessorOptions": { + "ScheduledDelayMilliseconds": 5000 + } + }, + "MetricsOptions": { + "Endpoint": "http://metrics_endpoint/", + "TemporalityPreference": "Delta", + "PeriodicExportingMetricReaderOptions": { + "ExportIntervalMilliseconds": 5000 + } + }, + "TracingOptions": { + "Endpoint": "http://trcing_endpoint/" + "ExportProcessorType": Batch, + "BatchExportProcessorOptions": { + "ScheduledDelayMilliseconds": 5000 + } + } + } + */ + + services!.Configure(name, configuration!); + + services!.Configure( + name, configuration!.GetSection(nameof(OtlpExporterBuilderOptions.LoggingOptions))); + + services!.Configure( + name, configuration.GetSection(nameof(OtlpExporterBuilderOptions.MetricsOptions))); + + services!.Configure( + name, configuration.GetSection(nameof(OtlpExporterBuilderOptions.TracingOptions))); + } + + private static void RegisterOtlpExporterServices(IServiceCollection services, string name) + { + Debug.Assert(services != null, "services was null"); + Debug.Assert(name != null, "name was null"); + + services!.AddOtlpExporterLoggingServices(); + services!.AddOtlpExporterMetricsServices(name!); + services!.AddOtlpExporterTracingServices(); + + // Note: UseOtlpExporterRegistration is added to the service collection + // to detect repeated calls to "UseOtlpExporter" and to throw if + // "AddOtlpExporter" extensions are called + services!.AddSingleton(); + + services!.RegisterOptionsFactory((sp, configuration, name) => new OtlpExporterBuilderOptions( + configuration, + /* Note: We don't use name for SdkLimitOptions. There should only be + one provider for a given service collection so SdkLimitOptions is + treated as a single default instance. */ + sp.GetRequiredService>().CurrentValue, + sp.GetRequiredService>().Get(name), + /* Note: We allow LogRecordExportProcessorOptions, + MetricReaderOptions, & ActivityExportProcessorOptions to be null + because those only exist if the corresponding signal is turned on. + Currently this extension turns on all signals so they will always be + there but that may change in the future so it is handled + defensively. */ + sp.GetService>()?.Get(name), + sp.GetService>()?.Get(name), + sp.GetService>()?.Get(name))); + + services!.ConfigureOpenTelemetryLoggerProvider( + (sp, logging) => + { + var builderOptions = GetBuilderOptionsAndValidateRegistrations(sp, name!); + + var processor = OtlpLogExporterHelperExtensions.BuildOtlpLogExporter( + sp, + builderOptions.LoggingOptionsInstance.ApplyDefaults(builderOptions.DefaultOptionsInstance), + builderOptions.LogRecordExportProcessorOptions ?? throw new InvalidOperationException("LogRecordExportProcessorOptions were missing with logging enabled"), + builderOptions.SdkLimitOptions, + builderOptions.ExperimentalOptions, + skipUseOtlpExporterRegistrationCheck: true); + + processor.PipelineWeight = DefaultProcessorPipelineWeight; + + logging.AddProcessor(processor); + }); + + services!.ConfigureOpenTelemetryMeterProvider( + (sp, metrics) => + { + var builderOptions = GetBuilderOptionsAndValidateRegistrations(sp, name!); + + metrics.AddReader( + OtlpMetricExporterExtensions.BuildOtlpExporterMetricReader( + sp, + builderOptions.MetricsOptionsInstance.ApplyDefaults(builderOptions.DefaultOptionsInstance), + builderOptions.MetricReaderOptions ?? throw new InvalidOperationException("MetricReaderOptions were missing with metrics enabled"), + builderOptions.ExperimentalOptions, + skipUseOtlpExporterRegistrationCheck: true)); + }); + + services!.ConfigureOpenTelemetryTracerProvider( + (sp, tracing) => + { + var builderOptions = GetBuilderOptionsAndValidateRegistrations(sp, name!); + + var processorOptions = builderOptions.ActivityExportProcessorOptions ?? throw new InvalidOperationException("ActivityExportProcessorOptions were missing with tracing enabled"); + + var processor = OtlpTraceExporterHelperExtensions.BuildOtlpExporterProcessor( + sp, + builderOptions.TracingOptionsInstance.ApplyDefaults(builderOptions.DefaultOptionsInstance), + builderOptions.SdkLimitOptions, + builderOptions.ExperimentalOptions, + processorOptions.ExportProcessorType, + processorOptions.BatchExportProcessorOptions, + skipUseOtlpExporterRegistrationCheck: true); + + processor.PipelineWeight = DefaultProcessorPipelineWeight; + + tracing.AddProcessor(processor); + }); + + static OtlpExporterBuilderOptions GetBuilderOptionsAndValidateRegistrations(IServiceProvider sp, string name) + { + sp.EnsureSingleUseOtlpExporterRegistration(); + + return sp.GetRequiredService>().Get(name); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs new file mode 100644 index 00000000000..e3ba3541ffb --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs @@ -0,0 +1,64 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter; + +internal sealed class OtlpExporterBuilderOptions +{ + internal readonly SdkLimitOptions SdkLimitOptions; + internal readonly ExperimentalOptions ExperimentalOptions; + internal readonly LogRecordExportProcessorOptions? LogRecordExportProcessorOptions; + internal readonly MetricReaderOptions? MetricReaderOptions; + internal readonly ActivityExportProcessorOptions? ActivityExportProcessorOptions; + + internal readonly OtlpExporterOptions DefaultOptionsInstance; + internal readonly OtlpExporterOptions LoggingOptionsInstance; + internal readonly OtlpExporterOptions MetricsOptionsInstance; + internal readonly OtlpExporterOptions TracingOptionsInstance; + + internal OtlpExporterBuilderOptions( + IConfiguration configuration, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, + LogRecordExportProcessorOptions? logRecordExportProcessorOptions, + MetricReaderOptions? metricReaderOptions, + ActivityExportProcessorOptions? activityExportProcessorOptions) + { + Debug.Assert(configuration != null, "configuration was null"); + Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); + + this.SdkLimitOptions = sdkLimitOptions!; + this.ExperimentalOptions = experimentalOptions!; + this.LogRecordExportProcessorOptions = logRecordExportProcessorOptions; + this.MetricReaderOptions = metricReaderOptions; + this.ActivityExportProcessorOptions = activityExportProcessorOptions; + + var defaultBatchOptions = this.ActivityExportProcessorOptions!.BatchExportProcessorOptions; + + this.DefaultOptionsInstance = new OtlpExporterOptions(configuration!, OtlpExporterOptionsConfigurationType.Default, defaultBatchOptions); + + this.LoggingOptionsInstance = new OtlpExporterOptions(configuration!, OtlpExporterOptionsConfigurationType.Logs, defaultBatchOptions); + + this.MetricsOptionsInstance = new OtlpExporterOptions(configuration!, OtlpExporterOptionsConfigurationType.Metrics, defaultBatchOptions); + + this.TracingOptionsInstance = new OtlpExporterOptions(configuration!, OtlpExporterOptionsConfigurationType.Traces, defaultBatchOptions); + } + + public IOtlpExporterOptions DefaultOptions => this.DefaultOptionsInstance; + + public IOtlpExporterOptions LoggingOptions => this.LoggingOptionsInstance; + + public IOtlpExporterOptions MetricsOptions => this.MetricsOptionsInstance; + + public IOtlpExporterOptions TracingOptions => this.TracingOptionsInstance; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs new file mode 100644 index 00000000000..ad0ad9fbc90 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs @@ -0,0 +1,13 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Exporter; + +// Note: This class is added to the IServiceCollection when UseOtlpExporter is +// called. Its purpose is to detect registrations so that subsequent calls and +// calls to signal-specific AddOtlpExporter can throw. +internal sealed class UseOtlpExporterRegistration +{ +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index bbd390269d7..d698acf9a6f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -45,6 +45,30 @@ variable to true. ([#5435](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5435)) +* Added `IOpenTelemetryBuilder.UseOtlpExporter` extension to simplify setup of + the OTLP Exporter when all three signals are used (logs, metrics, and traces). + The new extension has the following behaviors: + + * Calling `UseOtlpExporter` will automatically enable logging, tracing, and + metrics. Additional calls to `WithLogging`, `WithMetrics`, and `WithTracing` + are NOT required however for metrics and tracing sources/meters still need + to be enabled. + + * `UseOtlpExporter` can only be called once and cannot be used with the + existing `AddOtlpExporter` extensions. Extra calls will result in + `NotSupportedException`s being thrown. + + * `UseOtlpExporter` will register the OTLP Exporter at the end of the + processor pipeline for logging and tracing. + + * The OTLP Exporters added for logging, tracing, and metrics can be configured + using environment variables or `IConfiguration`. + + For details see: [README > Enable OTLP Exporter for all + signals](./README.md#enable-otlp-exporter-for-all-signals). + + PR: [#5400](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5400) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs new file mode 100644 index 00000000000..4c394759a13 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +#if NETFRAMEWORK +using System.Net.Http; +#endif + +namespace OpenTelemetry.Exporter; + +/// +/// Describes the OpenTelemetry Protocol (OTLP) exporter options shared by all +/// signals. +/// +internal interface IOtlpExporterOptions +{ + /// + /// Gets or sets the the OTLP transport protocol. + /// + OtlpExportProtocol Protocol { get; set; } + + /// + /// Gets or sets the target to which the exporter is going to send + /// telemetry. + /// + /// + /// Notes: + /// + /// When setting the value must be a valid with scheme (http or https) and host, and may contain a + /// port and path. + /// The default value when not set is based on the property: + /// + /// http://localhost:4317 for . + /// http://localhost:4318 for . + /// + /// When is set to and has + /// not been set the default value (http://localhost:4318) will have + /// a signal-specific path appended. The final default endpoint values will + /// be constructed as: + /// + /// Logging: http://localhost:4318/v1/logs + /// Metrics: http://localhost:4318/v1/metrics + /// Tracing: http://localhost:4318/v1/traces + /// + /// + /// + /// + /// + Uri Endpoint { get; set; } + + /// + /// Gets or sets optional headers for the connection. + /// + /// + /// Note: Refer to the + /// OpenTelemetry Specification for details on the format of . + /// + string? Headers { get; set; } + + /// + /// Gets or sets the max waiting time (in milliseconds) for the backend to + /// process each batch. Default value: 10000. + /// + int TimeoutMilliseconds { get; set; } + + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit telemetry over HTTP. The returned instance will be reused + /// for all export invocations. + /// + /// + /// Notes: + /// + /// This is only invoked for the protocol. + /// 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 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. + /// + /// The default behavior when using logging registration extensions is an + /// will be instantiated directly. IHttpClientFactory + /// is not currently supported for logging. + /// + /// + /// + Func HttpClientFactory { get; set; } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs new file mode 100644 index 00000000000..9aeaa7bf7df --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter; + +internal static class OtlpServiceCollectionExtensions +{ + public static void AddOtlpExporterLoggingServices(this IServiceCollection services) + { + Debug.Assert(services != null, "services was null"); + + AddOtlpExporterSharedServices(services!, registerSdkLimitOptions: true); + } + + public static void AddOtlpExporterMetricsServices(this IServiceCollection services, string name) + { + Debug.Assert(services != null, "services was null"); + Debug.Assert(name != null, "name was null"); + + AddOtlpExporterSharedServices(services!, registerSdkLimitOptions: false); + + services!.AddOptions(name).Configure( + (readerOptions, config) => + { + var otlpTemporalityPreference = config[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; + if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) + && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) + { + readerOptions.TemporalityPreference = enumValue; + } + }); + } + + public static void AddOtlpExporterTracingServices(this IServiceCollection services) + { + Debug.Assert(services != null, "services was null"); + + AddOtlpExporterSharedServices(services!, registerSdkLimitOptions: true); + } + + private static void AddOtlpExporterSharedServices( + IServiceCollection services, + bool registerSdkLimitOptions) + { + services.RegisterOptionsFactory(OtlpExporterOptions.CreateOtlpExporterOptions); + services.RegisterOptionsFactory(configuration => new ExperimentalOptions(configuration)); + + if (registerSdkLimitOptions) + { + services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index bed60120451..f4de3db506a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -35,14 +35,10 @@ NOT required because this project sees API + SDK internals --> - - - - diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 9c912b6b731..2a49efb1db7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -4,14 +4,13 @@ #nullable enable using System.Diagnostics; -using System.Reflection; #if NETFRAMEWORK using System.Net.Http; #endif +using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Internal; using OpenTelemetry.Trace; @@ -25,7 +24,7 @@ namespace OpenTelemetry.Exporter; /// OTEL_EXPORTER_OTLP_TIMEOUT, and OTEL_EXPORTER_OTLP_PROTOCOL environment /// variables are parsed during object construction. /// -public class OtlpExporterOptions +public class OtlpExporterOptions : IOtlpExporterOptions { internal const string DefaultGrpcEndpoint = "http://localhost:4317"; internal const string DefaultHttpEndpoint = "http://localhost:4318"; @@ -40,7 +39,9 @@ public class OtlpExporterOptions private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; + private OtlpExportProtocol? protocol; private Uri? endpoint; + private int? timeoutMilliseconds; private Func? httpClientFactory; /// @@ -80,38 +81,7 @@ internal OtlpExporterOptions( this.BatchExportProcessorOptions = defaultBatchOptions!; } - /// - /// Gets or sets the target to which the exporter is going to send - /// telemetry. - /// - /// - /// Notes: - /// - /// When setting the value must be a valid with scheme (http or https) and host, and may contain a - /// port and path. - /// The default value when not set is based on the property: - /// - /// http://localhost:4317 for . - /// http://localhost:4318 for . - /// - /// When is set to and has - /// not been set the default value (http://localhost:4318) will have - /// a signal-specific path appended. The final default endpoint values will - /// be constructed as: - /// - /// Logging: http://localhost:4318/v1/logs - /// Metrics: http://localhost:4318/v1/metrics - /// Tracing: http://localhost:4318/v1/traces - /// - /// - /// - /// - /// + /// public Uri Endpoint { get @@ -135,27 +105,22 @@ public Uri Endpoint } } - /// - /// Gets or sets optional headers for the connection. - /// - /// - /// 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. Default value: 10000. - /// - public int TimeoutMilliseconds { get; set; } = 10000; + /// + public int TimeoutMilliseconds + { + get => this.timeoutMilliseconds ?? 10000; + set => this.timeoutMilliseconds = value; + } - /// - /// Gets or sets the the OTLP transport protocol. - /// - public OtlpExportProtocol Protocol { get; set; } = DefaultOtlpExportProtocol; + /// + public OtlpExportProtocol Protocol + { + get => this.protocol ?? DefaultOtlpExportProtocol; + set => this.protocol = value; + } /// /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . @@ -169,39 +134,7 @@ public Uri Endpoint /// Note: This only applies when exporting traces. public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } - /// - /// Gets or sets the factory function called to create the instance that will be used at runtime to - /// transmit telemetry over HTTP. The returned instance will be reused - /// for all export invocations. - /// - /// - /// Notes: - /// - /// This is only invoked for the protocol. - /// 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 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. - /// - /// 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 => this.httpClientFactory ?? this.DefaultHttpClientFactory; @@ -223,11 +156,11 @@ public Func HttpClientFactory /// internal bool AppendSignalPathToEndpoint { get; private set; } = true; - internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection services) - { - services.RegisterOptionsFactory(CreateOtlpExporterOptions); - services.RegisterOptionsFactory(configuration => new ExperimentalOptions(configuration)); - } + internal bool HasData + => this.protocol.HasValue + || this.endpoint != null + || this.timeoutMilliseconds.HasValue + || this.httpClientFactory != null; internal static OtlpExporterOptions CreateOtlpExporterOptions( IServiceProvider serviceProvider, @@ -271,6 +204,25 @@ internal void ApplyConfigurationUsingSpecificationEnvVars( } } + internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOptions) + { + this.protocol ??= defaultExporterOptions.protocol; + + this.endpoint ??= defaultExporterOptions.endpoint; + + // Note: We leave AppendSignalPathToEndpoint set to true here because we + // want to append the signal if the endpoint came from the default + // endpoint. + + this.Headers ??= defaultExporterOptions.Headers; + + this.timeoutMilliseconds ??= defaultExporterOptions.timeoutMilliseconds; + + this.httpClientFactory ??= defaultExporterOptions.httpClientFactory; + + return this; + } + private static string GetUserAgentString() { try diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs index dbf156c1a0b..ca5a759caf4 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs @@ -224,7 +224,7 @@ static LoggerProviderBuilder AddOtlpExporter( services.Configure(finalOptionsName, configureExporter); } - RegisterOptions(services); + services.AddOtlpExporterLoggingServices(); }); return builder.AddProcessor(sp => @@ -299,7 +299,7 @@ static LoggerProviderBuilder AddOtlpExporter( { var finalOptionsName = name ?? Options.DefaultName; - builder.ConfigureServices(RegisterOptions); + builder.ConfigureServices(services => services.AddOtlpExporterLoggingServices()); return builder.AddProcessor(sp => { @@ -342,21 +342,25 @@ static LoggerProviderBuilder AddOtlpExporter( } internal static BaseProcessor BuildOtlpLogExporter( - IServiceProvider sp, + IServiceProvider serviceProvider, OtlpExporterOptions exporterOptions, LogRecordExportProcessorOptions processorOptions, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, + bool skipUseOtlpExporterRegistrationCheck = false, Func, BaseExporter>? configureExporterInstance = null) { - // Note: sp is not currently used by this method but it should be used - // at some point for IHttpClientFactory integration. - Debug.Assert(sp != null, "sp was null"); + Debug.Assert(serviceProvider != null, "serviceProvider was null"); Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(processorOptions != null, "processorOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); + if (!skipUseOtlpExporterRegistrationCheck) + { + serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + } + /* * Note: * @@ -400,12 +404,6 @@ internal static BaseProcessor BuildOtlpLogExporter( } } - private static void RegisterOptions(IServiceCollection services) - { - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); - } - private static T GetOptions( IServiceProvider sp, string? name, diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index 38cb1a44a66..57ad3dd47ec 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -3,7 +3,7 @@ #nullable enable -using Microsoft.Extensions.Configuration; +using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; @@ -59,18 +59,7 @@ public static MeterProviderBuilder AddOtlpExporter( services.Configure(finalOptionsName, configure); } - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - - services.AddOptions(finalOptionsName).Configure( - (readerOptions, config) => - { - var otlpTemporalityPreference = config[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; - if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) - && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) - { - readerOptions.TemporalityPreference = enumValue; - } - }); + services.AddOtlpExporterMetricsServices(finalOptionsName); }); return builder.AddReader(sp => @@ -97,10 +86,10 @@ public static MeterProviderBuilder AddOtlpExporter( } return BuildOtlpExporterMetricReader( + sp, exporterOptions, sp.GetRequiredService>().Get(finalOptionsName), - sp.GetRequiredService>().Get(finalOptionsName), - sp); + sp.GetRequiredService>().Get(finalOptionsName)); }); } @@ -137,18 +126,7 @@ public static MeterProviderBuilder AddOtlpExporter( builder.ConfigureServices(services => { - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - - services.AddOptions(finalOptionsName).Configure( - (readerOptions, config) => - { - var otlpTemporalityPreference = config[OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName]; - if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) - && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) - { - readerOptions.TemporalityPreference = enumValue; - } - }); + services.AddOtlpExporterMetricsServices(finalOptionsName); }); return builder.AddReader(sp => @@ -172,20 +150,31 @@ public static MeterProviderBuilder AddOtlpExporter( configureExporterAndMetricReader?.Invoke(exporterOptions, metricReaderOptions); return BuildOtlpExporterMetricReader( + sp, exporterOptions, metricReaderOptions, - sp.GetRequiredService>().Get(finalOptionsName), - sp); + sp.GetRequiredService>().Get(finalOptionsName)); }); } internal static MetricReader BuildOtlpExporterMetricReader( + IServiceProvider serviceProvider, OtlpExporterOptions exporterOptions, MetricReaderOptions metricReaderOptions, ExperimentalOptions experimentalOptions, - IServiceProvider serviceProvider, + bool skipUseOtlpExporterRegistrationCheck = false, Func, BaseExporter>? configureExporterInstance = null) { + Debug.Assert(serviceProvider != null, "serviceProvider was null"); + Debug.Assert(exporterOptions != null, "exporterOptions was null"); + Debug.Assert(metricReaderOptions != null, "metricReaderOptions was null"); + Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); + + if (!skipUseOtlpExporterRegistrationCheck) + { + serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + } + exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter"); BaseExporter metricExporter = new OtlpMetricExporter(exporterOptions, experimentalOptions); @@ -197,6 +186,6 @@ internal static MetricReader BuildOtlpExporterMetricReader( return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( metricExporter, - metricReaderOptions); + metricReaderOptions!); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 4e2ee7cebcb..1497276feac 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -38,10 +38,10 @@ public OtlpTraceExporter(OtlpExporterOptions options) /// . /// . internal OtlpTraceExporter( - OtlpExporterOptions exporterOptions, - SdkLimitOptions sdkLimitOptions, - ExperimentalOptions experimentalOptions, - OtlpExporterTransmissionHandler transmissionHandler = null) + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, + OtlpExporterTransmissionHandler transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs index 7ccbdd33472..036e9a90dd3 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs @@ -59,8 +59,7 @@ public static TracerProviderBuilder AddOtlpExporter( services.Configure(finalOptionsName, configure); } - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); + services.AddOtlpExporterTracingServices(); }); return builder.AddProcessor(sp => @@ -90,23 +89,53 @@ public static TracerProviderBuilder AddOtlpExporter( // There should only be one provider for a given service // collection so SdkLimitOptions is treated as a single default // instance. - var sdkOptionsManager = sp.GetRequiredService>().CurrentValue; + var sdkLimitOptions = sp.GetRequiredService>().CurrentValue; return BuildOtlpExporterProcessor( + sp, exporterOptions, - sdkOptionsManager, - sp.GetRequiredService>().Get(finalOptionsName), - sp); + sdkLimitOptions, + sp.GetRequiredService>().Get(finalOptionsName)); }); } internal static BaseProcessor BuildOtlpExporterProcessor( + IServiceProvider serviceProvider, OtlpExporterOptions exporterOptions, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, + Func, BaseExporter>? configureExporterInstance = null) + => BuildOtlpExporterProcessor( + serviceProvider, + exporterOptions, + sdkLimitOptions, + experimentalOptions, + exporterOptions.ExportProcessorType, + exporterOptions.BatchExportProcessorOptions ?? new BatchExportActivityProcessorOptions(), + skipUseOtlpExporterRegistrationCheck: false, + configureExporterInstance: configureExporterInstance); + + internal static BaseProcessor BuildOtlpExporterProcessor( IServiceProvider serviceProvider, + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, + ExportProcessorType exportProcessorType, + BatchExportProcessorOptions batchExportProcessorOptions, + bool skipUseOtlpExporterRegistrationCheck = false, Func, BaseExporter>? configureExporterInstance = null) { + Debug.Assert(serviceProvider != null, "serviceProvider was null"); + Debug.Assert(exporterOptions != null, "exporterOptions was null"); + Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); + Debug.Assert(batchExportProcessorOptions != null, "batchExportProcessorOptions was null"); + + if (!skipUseOtlpExporterRegistrationCheck) + { + serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + } + exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter"); BaseExporter otlpExporter = new OtlpTraceExporter(exporterOptions, sdkLimitOptions, experimentalOptions); @@ -116,20 +145,18 @@ internal static BaseProcessor BuildOtlpExporterProcessor( otlpExporter = configureExporterInstance(otlpExporter); } - if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) + if (exportProcessorType == ExportProcessorType.Simple) { return new SimpleActivityExportProcessor(otlpExporter); } else { - var batchOptions = exporterOptions.BatchExportProcessorOptions ?? new BatchExportActivityProcessorOptions(); - return new BatchActivityExportProcessor( otlpExporter, - batchOptions.MaxQueueSize, - batchOptions.ScheduledDelayMilliseconds, - batchOptions.ExporterTimeoutMilliseconds, - batchOptions.MaxExportBatchSize); + batchExportProcessorOptions!.MaxQueueSize, + batchExportProcessorOptions.ScheduledDelayMilliseconds, + batchExportProcessorOptions.ExporterTimeoutMilliseconds, + batchExportProcessorOptions.MaxExportBatchSize); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 763c2d54deb..a670487e5f7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -14,6 +14,7 @@ implementation. * [Enable Log Exporter](#enable-log-exporter) * [Enable Metric Exporter](#enable-metric-exporter) * [Enable Trace Exporter](#enable-trace-exporter) +* [Enable OTLP Exporter for all signals](#enable-otlp-exporter-for-all-signals) * [Configuration](#configuration) * [OtlpExporterOptions](#otlpexporteroptions) * [LogRecordExportProcessorOptions](#logrecordexportprocessoroptions) @@ -109,6 +110,10 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for runnable example. +## Enable OTLP Exporter for all signals + +Content coming soon. + ## Configuration You can configure the `OtlpExporter` through `OtlpExporterOptions` diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs index 90823131448..45c008955ad 100644 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ b/src/OpenTelemetry/AssemblyInfo.cs @@ -5,6 +5,8 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.InMemory" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.AspNetCore" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.HttpListener.Tests" + AssemblyInfo.PublicKey)] @@ -14,8 +16,6 @@ #if !EXPOSE_EXPERIMENTAL_FEATURES [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 diff --git a/src/OpenTelemetry/CompositeProcessor.cs b/src/OpenTelemetry/CompositeProcessor.cs index 614e0dd3b13..d59936ee87e 100644 --- a/src/OpenTelemetry/CompositeProcessor.cs +++ b/src/OpenTelemetry/CompositeProcessor.cs @@ -86,6 +86,18 @@ internal override void SetParentProvider(BaseProvider parentProvider) } } + internal IReadOnlyList> ToReadOnlyList() + { + var list = new List>(); + + for (var cur = this.Head; cur != null; cur = cur.Next) + { + list.Add(cur.Value); + } + + return list; + } + /// protected override bool OnForceFlush(int timeoutMilliseconds) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs index 673260f281d..7ef14595f06 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs @@ -14,10 +14,15 @@ public Http2UnencryptedSupportTests() public void Dispose() { - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); + this.Dispose(true); GC.SuppressFinalize(this); } + protected virtual void Dispose(bool disposing) + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); + } + private static bool DetermineInitialFlagStatus() { if (AppContext.TryGetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var flag)) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index e7bb7822fba..49c0e054237 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -21,6 +21,7 @@ public sealed class IntegrationTests : IDisposable private const string CollectorHostnameEnvVarName = "OTEL_COLLECTOR_HOSTNAME"; private const int ExportIntervalMilliseconds = 10000; private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + private static readonly ExperimentalOptions DefaultExperimentalOptions = new(); private static readonly string CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); private readonly OpenTelemetryEventListener openTelemetryEventListener; @@ -69,11 +70,11 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo var builder = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName); - builder.AddProcessor(OtlpTraceExporterHelperExtensions.BuildOtlpExporterProcessor( - exporterOptions, - DefaultSdkLimitOptions, - experimentalOptions: new(), - serviceProvider: null, + builder.AddProcessor(sp => OtlpTraceExporterHelperExtensions.BuildOtlpExporterProcessor( + serviceProvider: sp, + exporterOptions: exporterOptions, + sdkLimitOptions: DefaultSdkLimitOptions, + experimentalOptions: DefaultExperimentalOptions, configureExporterInstance: otlpExporter => { delegatingExporter = new DelegatingExporter @@ -151,11 +152,11 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp var readerOptions = new MetricReaderOptions(); readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = useManualExport ? Timeout.Infinite : ExportIntervalMilliseconds; - builder.AddReader(OtlpMetricExporterExtensions.BuildOtlpExporterMetricReader( - exporterOptions, - readerOptions, - experimentalOptions: new(), - serviceProvider: null, + builder.AddReader(sp => OtlpMetricExporterExtensions.BuildOtlpExporterMetricReader( + serviceProvider: sp, + exporterOptions: exporterOptions, + metricReaderOptions: readerOptions, + experimentalOptions: DefaultExperimentalOptions, configureExporterInstance: otlpExporter => { delegatingExporter = new DelegatingExporter @@ -240,8 +241,8 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin sp, exporterOptions, processorOptions, - new SdkLimitOptions(), - new ExperimentalOptions(), + DefaultSdkLimitOptions, + DefaultExperimentalOptions, configureExporterInstance: otlpExporter => { delegatingExporter = new DelegatingExporter diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index bf55abb4795..dc79c95c07b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -6,60 +6,17 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; +[Collection("EnvVars")] public class OtlpExporterOptionsTests : IDisposable { 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, - }; + OtlpSpecConfigDefinitionTests.ClearEnvVars(); } public void Dispose() { - ClearEnvVars(); - GC.SuppressFinalize(this); + OtlpSpecConfigDefinitionTests.ClearEnvVars(); } [Fact] @@ -87,58 +44,31 @@ public void OtlpExporterOptions_DefaultsForHttpProtobuf() } [Theory] - [MemberData(nameof(GetOtlpExporterOptionsTestCases))] - public void OtlpExporterOptions_EnvironmentVariableOverride( - int configurationType, - string endpointEnvVarKeyName, - string headersEnvVarKeyName, - string timeoutEnvVarKeyName, - string protocolEnvVarKeyName, - bool appendSignalPathToEndpoint) + [ClassData(typeof(OtlpSpecConfigDefinitionTests))] + public void OtlpExporterOptions_EnvironmentVariableOverride(object testDataObject) { - Environment.SetEnvironmentVariable(endpointEnvVarKeyName, "http://test:8888"); - Environment.SetEnvironmentVariable(headersEnvVarKeyName, "A=2,B=3"); - Environment.SetEnvironmentVariable(timeoutEnvVarKeyName, "2000"); - Environment.SetEnvironmentVariable(protocolEnvVarKeyName, "http/protobuf"); + var testData = testDataObject as OtlpSpecConfigDefinitionTests.TestData; + Assert.NotNull(testData); - var options = new OtlpExporterOptions((OtlpExporterOptionsConfigurationType)configurationType); + testData.SetEnvVars(); - 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); + var options = new OtlpExporterOptions(testData.ConfigurationType); + + testData.AssertMatches(options); } [Theory] - [MemberData(nameof(GetOtlpExporterOptionsTestCases))] - public void OtlpExporterOptions_UsingIConfiguration( - int configurationType, - string endpointEnvVarKeyName, - string headersEnvVarKeyName, - string timeoutEnvVarKeyName, - string protocolEnvVarKeyName, - bool appendSignalPathToEndpoint) + [ClassData(typeof(OtlpSpecConfigDefinitionTests))] + public void OtlpExporterOptions_UsingIConfiguration(object testDataObject) { - var values = new Dictionary() - { - [endpointEnvVarKeyName] = "http://test:8888", - [headersEnvVarKeyName] = "A=2,B=3", - [timeoutEnvVarKeyName] = "2000", - [protocolEnvVarKeyName] = "http/protobuf", - }; + var testData = testDataObject as OtlpSpecConfigDefinitionTests.TestData; + Assert.NotNull(testData); - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); + var configuration = testData.ToConfiguration(); - var options = new OtlpExporterOptions(configuration, (OtlpExporterOptionsConfigurationType)configurationType, new()); + var options = new OtlpExporterOptions(configuration, testData.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); + testData.AssertMatches(options); } [Fact] @@ -234,15 +164,6 @@ public void OtlpExporterOptions_EndpointThrowsWhenSetToNull() Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); } - [Fact] - public void OtlpExporterOptions_EnvironmentVariableNames() - { - 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() { @@ -259,14 +180,51 @@ public void OtlpExporterOptions_HttpClientFactoryThrowsWhenSetToNull() Assert.Throws(() => options.HttpClientFactory = null); } - private static void ClearEnvVars() + [Fact] + public void OtlpExporterOptions_ApplyDefaultsTest() { - foreach (var item in GetOtlpExporterOptionsTestCases()) + var defaultOptionsWithData = new OtlpExporterOptions { - Environment.SetEnvironmentVariable((string)item[1], null); - Environment.SetEnvironmentVariable((string)item[2], null); - Environment.SetEnvironmentVariable((string)item[3], null); - Environment.SetEnvironmentVariable((string)item[4], null); - } + Endpoint = new Uri("http://default_endpoint/"), + Protocol = OtlpExportProtocol.HttpProtobuf, + Headers = "key1=value1", + TimeoutMilliseconds = 18, + HttpClientFactory = () => null!, + }; + + Assert.True(defaultOptionsWithData.HasData); + + var targetOptionsWithoutData = new OtlpExporterOptions(); + + Assert.False(targetOptionsWithoutData.HasData); + + targetOptionsWithoutData.ApplyDefaults(defaultOptionsWithData); + + Assert.Equal(defaultOptionsWithData.Endpoint, targetOptionsWithoutData.Endpoint); + Assert.True(targetOptionsWithoutData.AppendSignalPathToEndpoint); + Assert.Equal(defaultOptionsWithData.Protocol, targetOptionsWithoutData.Protocol); + Assert.Equal(defaultOptionsWithData.Headers, targetOptionsWithoutData.Headers); + Assert.Equal(defaultOptionsWithData.TimeoutMilliseconds, targetOptionsWithoutData.TimeoutMilliseconds); + Assert.Equal(defaultOptionsWithData.HttpClientFactory, targetOptionsWithoutData.HttpClientFactory); + + var targetOptionsWithData = new OtlpExporterOptions + { + Endpoint = new Uri("http://metrics_endpoint/"), + Protocol = OtlpExportProtocol.Grpc, + Headers = "key2=value2", + TimeoutMilliseconds = 1800, + HttpClientFactory = () => throw new NotImplementedException(), + }; + + Assert.True(targetOptionsWithData.HasData); + + targetOptionsWithData.ApplyDefaults(defaultOptionsWithData); + + Assert.NotEqual(defaultOptionsWithData.Endpoint, targetOptionsWithData.Endpoint); + Assert.False(targetOptionsWithData.AppendSignalPathToEndpoint); + Assert.NotEqual(defaultOptionsWithData.Protocol, targetOptionsWithData.Protocol); + Assert.NotEqual(defaultOptionsWithData.Headers, targetOptionsWithData.Headers); + Assert.NotEqual(defaultOptionsWithData.TimeoutMilliseconds, targetOptionsWithData.TimeoutMilliseconds); + Assert.NotEqual(defaultOptionsWithData.HttpClientFactory, targetOptionsWithData.HttpClientFactory); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 451362ed927..ad2f8a6b4b2 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -18,6 +18,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; +[Collection("EnvVars")] public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests { private static readonly KeyValuePair[] KeyValues = new KeyValuePair[] @@ -26,6 +27,11 @@ public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests new KeyValuePair("key2", 123), }; + public OtlpMetricsExporterTests() + { + OtlpSpecConfigDefinitionTests.ClearEnvVars(); + } + [Fact] public void TestAddOtlpExporter_SetsCorrectMetricReaderDefaults() { @@ -727,47 +733,58 @@ public void TestHistogramToOtlpMetric(string name, string description, string un } [Theory] - [InlineData("cumulative", MetricReaderTemporalityPreference.Cumulative)] - [InlineData("Cumulative", MetricReaderTemporalityPreference.Cumulative)] - [InlineData("CUMULATIVE", MetricReaderTemporalityPreference.Cumulative)] - [InlineData("delta", MetricReaderTemporalityPreference.Delta)] - [InlineData("Delta", MetricReaderTemporalityPreference.Delta)] - [InlineData("DELTA", MetricReaderTemporalityPreference.Delta)] - public void TestTemporalityPreferenceConfiguration(string configValue, MetricReaderTemporalityPreference expectedTemporality) + [InlineData("cuMulative", MetricReaderTemporalityPreference.Cumulative)] + [InlineData("DeltA", MetricReaderTemporalityPreference.Delta)] + [InlineData("invalid", MetricReaderTemporalityPreference.Cumulative)] + public void TestTemporalityPreferenceUsingConfiguration(string configValue, MetricReaderTemporalityPreference expectedTemporality) { - var configData = new Dictionary { ["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = configValue }; + var testExecuted = false; + + var configData = new Dictionary { [OtlpSpecConfigDefinitionTests.MetricsData.TemporalityKeyName] = configValue }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(configData) .Build(); - // Check for both the code paths: - // 1. The final extension method which accepts `Action`. - // 2. The final extension method which accepts `Action`. + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(configuration); - // Test 1st code path - using var meterProvider1 = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddOtlpExporter() // This would in turn call the extension method which accepts `Action` + services.PostConfigure(o => + { + testExecuted = true; + Assert.Equal(expectedTemporality, o.TemporalityPreference); + }); + }) + .AddOtlpExporter() .Build(); - var assembly = typeof(Sdk).Assembly; - var type = assembly.GetType("OpenTelemetry.Metrics.MeterProviderSdk"); - var fieldInfo = type.GetField("reader", BindingFlags.Instance | BindingFlags.NonPublic); - var reader = fieldInfo.GetValue(meterProvider1) as MetricReader; - var temporality = reader.TemporalityPreference; + Assert.True(testExecuted); + } - Assert.Equal(expectedTemporality, temporality); + [Theory] + [InlineData("cuMulative", MetricReaderTemporalityPreference.Cumulative)] + [InlineData("DeltA", MetricReaderTemporalityPreference.Delta)] + [InlineData("invalid", MetricReaderTemporalityPreference.Cumulative)] + public void TestTemporalityPreferenceUsingEnvVar(string configValue, MetricReaderTemporalityPreference expectedTemporality) + { + Environment.SetEnvironmentVariable(OtlpSpecConfigDefinitionTests.MetricsData.TemporalityKeyName, configValue); - // Test 2nd code path - using var meterProvider2 = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => services.AddSingleton(configuration)) - .AddOtlpExporter((_, _) => { }) // This would in turn call the extension method which accepts `Action` - .Build(); + var testExecuted = false; - reader = fieldInfo.GetValue(meterProvider2) as MetricReader; - temporality = reader.TemporalityPreference; + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.PostConfigure(o => + { + testExecuted = true; + Assert.Equal(expectedTemporality, o.TemporalityPreference); + }); + }) + .AddOtlpExporter() + .Build(); - Assert.Equal(expectedTemporality, temporality); + Assert.True(testExecuted); } [Theory] @@ -899,6 +916,13 @@ void AssertExemplars(T value, Metric metric) } } + protected override void Dispose(bool disposing) + { + OtlpSpecConfigDefinitionTests.ClearEnvVars(); + + base.Dispose(disposing); + } + private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) { var exemplar = getExemplarFunc(state); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs new file mode 100644 index 00000000000..4e4e5469183 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs @@ -0,0 +1,314 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Collections; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Metrics; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpSpecConfigDefinitionTests : IEnumerable +{ + internal static TestData DefaultData { get; } = new TestData( + OtlpExporterOptionsConfigurationType.Default, + OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, + "http://default_endpoint/", + appendSignalPathToEndpoint: true, + OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName, + "key1=value1", + OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName, + "1001", + OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName, + "http/protobuf"); + + internal static TestData LoggingData { get; } = new TestData( + OtlpExporterOptionsConfigurationType.Logs, + OtlpSpecConfigDefinitions.LogsEndpointEnvVarName, + "http://logs_endpoint/", + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.LogsHeadersEnvVarName, + "key2=value2", + OtlpSpecConfigDefinitions.LogsTimeoutEnvVarName, + "1002", + OtlpSpecConfigDefinitions.LogsProtocolEnvVarName, + "http/protobuf"); + + internal static MetricsTestData MetricsData { get; } = new MetricsTestData( + OtlpSpecConfigDefinitions.MetricsEndpointEnvVarName, + "http://metrics_endpoint/", + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.MetricsHeadersEnvVarName, + "key3=value3", + OtlpSpecConfigDefinitions.MetricsTimeoutEnvVarName, + "1003", + OtlpSpecConfigDefinitions.MetricsProtocolEnvVarName, + "http/protobuf", + OtlpSpecConfigDefinitions.MetricsTemporalityPreferenceEnvVarName, + "Delta"); + + internal static TestData TracingData { get; } = new TestData( + OtlpExporterOptionsConfigurationType.Traces, + OtlpSpecConfigDefinitions.TracesEndpointEnvVarName, + "http://traces_endpoint/", + appendSignalPathToEndpoint: false, + OtlpSpecConfigDefinitions.TracesHeadersEnvVarName, + "key4=value4", + OtlpSpecConfigDefinitions.TracesTimeoutEnvVarName, + "1004", + OtlpSpecConfigDefinitions.TracesProtocolEnvVarName, + "http/protobuf"); + + [Fact] + public void VerifyKeyNamesMatchSpec() + { + Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", DefaultData.EndpointKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", DefaultData.HeadersKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", DefaultData.TimeoutKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", DefaultData.ProtocolKeyName); + + Assert.Equal("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", LoggingData.EndpointKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_LOGS_HEADERS", LoggingData.HeadersKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", LoggingData.TimeoutKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", LoggingData.ProtocolKeyName); + + Assert.Equal("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", MetricsData.EndpointKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_METRICS_HEADERS", MetricsData.HeadersKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", MetricsData.TimeoutKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", MetricsData.ProtocolKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", MetricsData.TemporalityKeyName); + + Assert.Equal("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", TracingData.EndpointKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_TRACES_HEADERS", TracingData.HeadersKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", TracingData.TimeoutKeyName); + Assert.Equal("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", TracingData.ProtocolKeyName); + } + + public IEnumerator GetEnumerator() + { + yield return new object[] + { + DefaultData, + }; + + yield return new object[] + { + LoggingData, + }; + + yield return new object[] + { + MetricsData, + }; + + yield return new object[] + { + TracingData, + }; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + internal static IConfiguration ToConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + + DefaultData.AddToConfiguration(configBuilder); + LoggingData.AddToConfiguration(configBuilder); + MetricsData.AddToConfiguration(configBuilder); + TracingData.AddToConfiguration(configBuilder); + + return configBuilder.Build(); + } + + internal static void SetEnvVars() + { + DefaultData.SetEnvVars(); + LoggingData.SetEnvVars(); + MetricsData.SetEnvVars(); + TracingData.SetEnvVars(); + } + + internal static void ClearEnvVars() + { + DefaultData.ClearEnvVars(); + LoggingData.ClearEnvVars(); + MetricsData.ClearEnvVars(); + TracingData.ClearEnvVars(); + } + + internal class TestData + { + public TestData( + OtlpExporterOptionsConfigurationType configurationType, + string endpointKeyName, + string endpointValue, + bool appendSignalPathToEndpoint, + string headersKeyName, + string headersValue, + string timeoutKeyName, + string timeoutValue, + string protocolKeyName, + string protocolValue) + { + this.ConfigurationType = configurationType; + this.EndpointKeyName = endpointKeyName; + this.EndpointValue = endpointValue; + this.AppendSignalPathToEndpoint = appendSignalPathToEndpoint; + this.HeadersKeyName = headersKeyName; + this.HeadersValue = headersValue; + this.TimeoutKeyName = timeoutKeyName; + this.TimeoutValue = timeoutValue; + this.ProtocolKeyName = protocolKeyName; + this.ProtocolValue = protocolValue; + } + + public OtlpExporterOptionsConfigurationType ConfigurationType { get; } + + public string EndpointKeyName { get; } + + public string EndpointValue { get; } + + public bool AppendSignalPathToEndpoint { get; } + + public string HeadersKeyName { get; } + + public string HeadersValue { get; } + + public string TimeoutKeyName { get; } + + public string TimeoutValue { get; } + + public string ProtocolKeyName { get; } + + public string ProtocolValue { get; } + + public IConfiguration ToConfiguration() + { + return this.AddToConfiguration(new ConfigurationBuilder()).Build(); + } + + public ConfigurationBuilder AddToConfiguration(ConfigurationBuilder configurationBuilder) + { + Dictionary dictionary = new(); + + dictionary[this.EndpointKeyName] = this.EndpointValue; + dictionary[this.HeadersKeyName] = this.HeadersValue; + dictionary[this.TimeoutKeyName] = this.TimeoutValue; + dictionary[this.ProtocolKeyName] = this.ProtocolValue; + + this.OnAddToDictionary(dictionary); + + configurationBuilder.AddInMemoryCollection(dictionary); + + return configurationBuilder; + } + + public void SetEnvVars() + { + Environment.SetEnvironmentVariable(this.EndpointKeyName, this.EndpointValue); + Environment.SetEnvironmentVariable(this.HeadersKeyName, this.HeadersValue); + Environment.SetEnvironmentVariable(this.TimeoutKeyName, this.TimeoutValue); + Environment.SetEnvironmentVariable(this.ProtocolKeyName, this.ProtocolValue); + + this.OnSetEnvVars(); + } + + public void ClearEnvVars() + { + Environment.SetEnvironmentVariable(this.EndpointKeyName, null); + Environment.SetEnvironmentVariable(this.HeadersKeyName, null); + Environment.SetEnvironmentVariable(this.TimeoutKeyName, null); + Environment.SetEnvironmentVariable(this.ProtocolKeyName, null); + + this.OnClearEnvVars(); + } + + public void AssertMatches(IOtlpExporterOptions otlpExporterOptions) + { + Assert.Equal(new Uri(this.EndpointValue), otlpExporterOptions.Endpoint); + Assert.Equal(this.HeadersValue, otlpExporterOptions.Headers); + Assert.Equal(int.Parse(this.TimeoutValue), otlpExporterOptions.TimeoutMilliseconds); + + if (!OtlpExportProtocolParser.TryParse(this.ProtocolValue, out var protocol)) + { + Assert.Fail(); + } + + Assert.Equal(protocol, otlpExporterOptions.Protocol); + + var concreteOptions = otlpExporterOptions as OtlpExporterOptions; + Assert.NotNull(concreteOptions); + Assert.Equal(this.AppendSignalPathToEndpoint, concreteOptions.AppendSignalPathToEndpoint); + } + + protected virtual void OnSetEnvVars() + { + } + + protected virtual void OnClearEnvVars() + { + } + + protected virtual void OnAddToDictionary(Dictionary dictionary) + { + } + } + + internal sealed class MetricsTestData : TestData + { + public MetricsTestData( + string endpointKeyName, + string endpointValue, + bool appendSignalPathToEndpoint, + string headersKeyName, + string headersValue, + string timeoutKeyName, + string timeoutValue, + string protocolKeyName, + string protocolValue, + string temporalityKeyName, + string temporalityValue) + : base( + OtlpExporterOptionsConfigurationType.Metrics, + endpointKeyName, + endpointValue, + appendSignalPathToEndpoint, + headersKeyName, + headersValue, + timeoutKeyName, + timeoutValue, + protocolKeyName, + protocolValue) + { + this.TemporalityKeyName = temporalityKeyName; + this.TemporalityValue = temporalityValue; + } + + public string TemporalityKeyName { get; } + + public string TemporalityValue { get; } + + public void AssertMatches(MetricReaderOptions metricReaderOptions) + { + Assert.Equal(Enum.Parse(typeof(MetricReaderTemporalityPreference), this.TemporalityValue), metricReaderOptions.TemporalityPreference); + } + + protected override void OnSetEnvVars() + { + Environment.SetEnvironmentVariable(this.TemporalityKeyName, this.TemporalityValue); + } + + protected override void OnClearEnvVars() + { + Environment.SetEnvironmentVariable(this.TemporalityKeyName, null); + } + + protected override void OnAddToDictionary(Dictionary dictionary) + { + dictionary[this.TemporalityKeyName] = this.TemporalityValue; + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs new file mode 100644 index 00000000000..025bd3b97ec --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs @@ -0,0 +1,385 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +[Collection("EnvVars")] +public class UseOtlpExporterExtensionTests : IDisposable +{ + public UseOtlpExporterExtensionTests() + { + OtlpSpecConfigDefinitionTests.ClearEnvVars(); + } + + public void Dispose() + { + OtlpSpecConfigDefinitionTests.ClearEnvVars(); + } + + [Fact] + public void UseOtlpExporterDefaultTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter(); + + using var sp = services.BuildServiceProvider(); + + var exporterOptions = sp.GetRequiredService>().CurrentValue; + + Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), exporterOptions.DefaultOptions.Endpoint); + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, exporterOptions.DefaultOptions.Protocol); + Assert.False(((OtlpExporterOptions)exporterOptions.DefaultOptions).HasData); + + Assert.False(((OtlpExporterOptions)exporterOptions.LoggingOptions).HasData); + Assert.False(((OtlpExporterOptions)exporterOptions.MetricsOptions).HasData); + Assert.False(((OtlpExporterOptions)exporterOptions.TracingOptions).HasData); + } + + [Theory] + [InlineData(OtlpExportProtocol.Grpc)] + [InlineData(OtlpExportProtocol.HttpProtobuf)] + public void UseOtlpExporterSetEndpointAndProtocolTest(OtlpExportProtocol protocol) + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter( + protocol, + new Uri("http://test_base_endpoint/")); + + using var sp = services.BuildServiceProvider(); + + var exporterOptions = sp.GetRequiredService>().CurrentValue; + + Assert.Equal(protocol, exporterOptions.DefaultOptions.Protocol); + Assert.Equal(new Uri("http://test_base_endpoint/"), exporterOptions.DefaultOptions.Endpoint); + Assert.True(((OtlpExporterOptions)exporterOptions.DefaultOptions).HasData); + + Assert.False(((OtlpExporterOptions)exporterOptions.LoggingOptions).HasData); + Assert.False(((OtlpExporterOptions)exporterOptions.MetricsOptions).HasData); + Assert.False(((OtlpExporterOptions)exporterOptions.TracingOptions).HasData); + + Assert.Throws( + () => services.AddOpenTelemetry().UseOtlpExporter(OtlpExportProtocol.HttpProtobuf, baseEndpoint: null!)); + } + + [Theory] + [InlineData(null)] + [InlineData("testNamedOptions")] + public void UseOtlpExporterConfigureTest(string? name) + { + var services = new ServiceCollection(); + + if (!string.IsNullOrEmpty(name)) + { + services.AddOpenTelemetry() + .UseOtlpExporter(name, configuration: null, configure: Configure); + } + else + { + services.AddOpenTelemetry() + .UseOtlpExporter(Configure); + } + + using var sp = services.BuildServiceProvider(); + + VerifyOptionsApplied(sp, name); + + static void Configure(OtlpExporterBuilder builder) + { + builder.ConfigureDefaultExporterOptions( + defaultOptions => defaultOptions.Endpoint = new Uri("http://default_endpoint/")); + + builder.ConfigureLoggingExporterOptions( + exporterOptions => exporterOptions.Endpoint = new Uri("http://signal_endpoint/logs/")); + builder.ConfigureLoggingProcessorOptions( + processorOptions => + { + processorOptions.ExportProcessorType = ExportProcessorType.Simple; + processorOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 1000; + }); + + builder.ConfigureMetricsExporterOptions( + exporterOptions => exporterOptions.Endpoint = new Uri("http://signal_endpoint/metrics/")); + builder.ConfigureMetricsReaderOptions( + readerOptions => + { + readerOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1001; + }); + + builder.ConfigureTracingExporterOptions( + exporterOptions => exporterOptions.Endpoint = new Uri("http://signal_endpoint/traces/")); + builder.ConfigureTracingProcessorOptions( + processorOptions => + { + processorOptions.ExportProcessorType = ExportProcessorType.Simple; + processorOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 1002; + }); + } + } + + [Theory] + [InlineData(null)] + [InlineData("testNamedOptions")] + public void UseOtlpExporterConfigurationTest(string? name) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["DefaultOptions:Endpoint"] = "http://default_endpoint/", + ["LoggingOptions:Endpoint"] = "http://signal_endpoint/logs/", + ["LoggingOptions:ExportProcessorType"] = "Simple", + ["LoggingOptions:BatchExportProcessorOptions:ScheduledDelayMilliseconds"] = "1000", + ["MetricsOptions:Endpoint"] = "http://signal_endpoint/metrics/", + ["MetricsOptions:TemporalityPreference"] = "Delta", + ["MetricsOptions:PeriodicExportingMetricReaderOptions:ExportIntervalMilliseconds"] = "1001", + ["TracingOptions:Endpoint"] = "http://signal_endpoint/traces/", + ["TracingOptions:ExportProcessorType"] = "Simple", + ["TracingOptions:BatchExportProcessorOptions:ScheduledDelayMilliseconds"] = "1002", + }) + .Build(); + + var services = new ServiceCollection(); + + if (!string.IsNullOrEmpty(name)) + { + services.AddOpenTelemetry() + .UseOtlpExporter(name: name, configuration: config, configure: null); + } + else + { + services.AddOpenTelemetry() + .UseOtlpExporter(config); + + name = "otlp"; + } + + using var sp = services.BuildServiceProvider(); + + VerifyOptionsApplied(sp, name); + } + + [Fact] + public void UseOtlpExporterSingleCallsTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter(); + + using var sp = services.BuildServiceProvider(); + + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void UseOtlpExporterMultipleCallsTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .UseOtlpExporter(); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + Assert.Throws(() => sp.GetRequiredService()); + Assert.Throws(() => sp.GetRequiredService()); + } + + [Fact] + public void UseOtlpExporterWithAddOtlpExporterLoggingTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .WithLogging(builder => builder.AddOtlpExporter()); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + } + + [Fact] + public void UseOtlpExporterWithAddOtlpExporterMetricsTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .WithMetrics(builder => builder.AddOtlpExporter()); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + } + + [Fact] + public void UseOtlpExporterWithAddOtlpExporterTracingTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .WithTracing(builder => builder.AddOtlpExporter()); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + } + + [Fact] + public void UseOtlpExporterAddsTracingProcessorToPipelineEndTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .WithTracing(builder => builder.AddProcessor(new TestActivityProcessor())); + + using var sp = services.BuildServiceProvider(); + + var tracerProvider = sp.GetRequiredService() as TracerProviderSdk; + + Assert.NotNull(tracerProvider); + + var processor = tracerProvider.Processor as CompositeProcessor; + + Assert.NotNull(processor); + + var processors = processor.ToReadOnlyList(); + + Assert.True(processors[0] is TestActivityProcessor); + Assert.True(processors[1] is BatchActivityExportProcessor); + } + + [Fact] + public void UseOtlpExporterAddsLoggingProcessorToPipelineEndTest() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter() + .WithLogging(builder => builder.AddProcessor(new TestLogRecordProcessor())); + + using var sp = services.BuildServiceProvider(); + + var tracerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(tracerProvider); + + var processor = tracerProvider.Processor as CompositeProcessor; + + Assert.NotNull(processor); + + var processors = processor.ToReadOnlyList(); + + Assert.True(processors[0] is TestLogRecordProcessor); + Assert.True(processors[1] is BatchLogRecordExportProcessor); + } + + [Fact] + public void UseOtlpExporterRespectsSpecEnvVarsTest() + { + OtlpSpecConfigDefinitionTests.SetEnvVars(); + + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .UseOtlpExporter(); + + using var sp = services.BuildServiceProvider(); + + var exporterBuilderOptions = sp.GetRequiredService>().Get(Options.DefaultName); + + OtlpSpecConfigDefinitionTests.DefaultData.AssertMatches(exporterBuilderOptions.DefaultOptions); + OtlpSpecConfigDefinitionTests.LoggingData.AssertMatches(exporterBuilderOptions.LoggingOptions); + OtlpSpecConfigDefinitionTests.MetricsData.AssertMatches(exporterBuilderOptions.MetricsOptions); + OtlpSpecConfigDefinitionTests.TracingData.AssertMatches(exporterBuilderOptions.TracingOptions); + + var metricReaderOptions = sp.GetRequiredService>().Get(Options.DefaultName); + + OtlpSpecConfigDefinitionTests.MetricsData.AssertMatches(metricReaderOptions); + } + + [Fact] + public void UseOtlpExporterRespectsSpecEnvVarsSetUsingIConfigurationTest() + { + var services = new ServiceCollection(); + + services.AddSingleton(OtlpSpecConfigDefinitionTests.ToConfiguration()); + + services.AddOpenTelemetry() + .UseOtlpExporter(); + + using var sp = services.BuildServiceProvider(); + + var exporterBuilderOptions = sp.GetRequiredService>().Get(Options.DefaultName); + + OtlpSpecConfigDefinitionTests.DefaultData.AssertMatches(exporterBuilderOptions.DefaultOptions); + OtlpSpecConfigDefinitionTests.LoggingData.AssertMatches(exporterBuilderOptions.LoggingOptions); + OtlpSpecConfigDefinitionTests.MetricsData.AssertMatches(exporterBuilderOptions.MetricsOptions); + OtlpSpecConfigDefinitionTests.TracingData.AssertMatches(exporterBuilderOptions.TracingOptions); + + var metricReaderOptions = sp.GetRequiredService>().Get(Options.DefaultName); + + OtlpSpecConfigDefinitionTests.MetricsData.AssertMatches(metricReaderOptions); + } + + private static void VerifyOptionsApplied(ServiceProvider serviceProvider, string? name) + { + var exporterOptions = serviceProvider.GetRequiredService>().Get(name); + + Assert.Equal("http://default_endpoint/", exporterOptions.DefaultOptions.Endpoint.ToString()); + /* Note: False is OK here. For cross-cutting extension + AppendSignalPathToEndpoint on default options isn't used for anything */ + Assert.False(((OtlpExporterOptions)exporterOptions.DefaultOptions).AppendSignalPathToEndpoint); + + Assert.Equal("http://signal_endpoint/logs/", exporterOptions.LoggingOptions.Endpoint.ToString()); + Assert.False(((OtlpExporterOptions)exporterOptions.LoggingOptions).AppendSignalPathToEndpoint); + + Assert.Equal("http://signal_endpoint/metrics/", exporterOptions.MetricsOptions.Endpoint.ToString()); + Assert.False(((OtlpExporterOptions)exporterOptions.MetricsOptions).AppendSignalPathToEndpoint); + + Assert.Equal("http://signal_endpoint/traces/", exporterOptions.TracingOptions.Endpoint.ToString()); + Assert.False(((OtlpExporterOptions)exporterOptions.TracingOptions).AppendSignalPathToEndpoint); + + var logRecordProcessorOptions = serviceProvider.GetRequiredService>().Get(name); + + Assert.Equal(ExportProcessorType.Simple, logRecordProcessorOptions.ExportProcessorType); + Assert.Equal(1000, logRecordProcessorOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds); + + var metricReaderOptions = serviceProvider.GetRequiredService>().Get(name); + + Assert.Equal(MetricReaderTemporalityPreference.Delta, metricReaderOptions.TemporalityPreference); + Assert.Equal(1001, metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); + + var activityProcessorOptions = serviceProvider.GetRequiredService>().Get(name); + + Assert.Equal(ExportProcessorType.Simple, activityProcessorOptions.ExportProcessorType); + Assert.Equal(1002, activityProcessorOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds); + } + + private sealed class TestLogRecordProcessor : BaseProcessor + { + } +}