diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs index 2145d23c19e..c6a15f659eb 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs @@ -9,6 +9,8 @@ #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.Extensions.Hosting" + AssemblyInfo.PublicKey)] #endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt index e69de29bb2d..e6bd747c9de 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, string? name, System.Action? configureExporterAndProcessor) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, string? name, System.Action? configureExporter) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, System.Action! configureExporterAndProcessor) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, System.Action! configureExporter) -> OpenTelemetry.Logs.LoggerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 284ce9c8e64..959fe7c87ee 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* **Experimental (pre-release builds only):** Added + `LoggerProviderBuilder.AddOtlpExporter` registration extensions. + [#5103](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5103) + ## 1.7.0 Released 2023-Dec-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index 92329c814d6..bed60120451 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -34,10 +34,11 @@ - + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs index 353204d4715..85298517f95 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs @@ -4,6 +4,9 @@ #nullable enable using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -57,13 +60,18 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( return loggerOptions.AddProcessor(sp => { - var exporterOptions = GetOtlpExporterOptions(sp, name, finalOptionsName); + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); configure?.Invoke(exporterOptions); - return BuildOtlpLogExporter(sp, exporterOptions, processorOptions); + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + GetOptions(sp, Options.DefaultName, Options.DefaultName, (sp, c, n) => new SdkLimitOptions(c)), + GetOptions(sp, name, finalOptionsName, (sp, c, n) => new ExperimentalOptions(c))); }); } @@ -96,13 +104,240 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( return loggerOptions.AddProcessor(sp => { - var exporterOptions = GetOtlpExporterOptions(sp, name, finalOptionsName); + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); configureExporterAndProcessor?.Invoke(exporterOptions, processorOptions); - return BuildOtlpLogExporter(sp, exporterOptions, processorOptions); + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + GetOptions(sp, Options.DefaultName, Options.DefaultName, (sp, c, n) => new SdkLimitOptions(c)), + GetOptions(sp, name, finalOptionsName, (sp, c, n) => new ExperimentalOptions(c))); + }); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder) + => AddOtlpExporter(builder, name: null, configureExporter: null); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder, Action configureExporter) + => AddOtlpExporter(builder, name: null, configureExporter); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Callback action for + /// configuring and . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder, Action configureExporterAndProcessor) + => AddOtlpExporter(builder, name: null, configureExporterAndProcessor); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the LoggerProvider. + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter( + this LoggerProviderBuilder builder, + string? name, + Action? configureExporter) + { + var finalOptionsName = name ?? Options.DefaultName; + + builder.ConfigureServices(services => + { + if (name != null && configureExporter != null) + { + // If we are using named options we register the + // configuration delegate into options pipeline. + services.Configure(finalOptionsName, configureExporter); + } + + RegisterOptions(services); + }); + + return builder.AddProcessor(sp => + { + OtlpExporterOptions exporterOptions; + + if (name == null) + { + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + + // Configuration delegate is executed inline on the fresh instance. + configureExporter?.Invoke(exporterOptions); + } + else + { + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } + + // Note: Not using finalOptionsName here for SdkLimitOptions. + // There should only be one provider for a given service + // collection so SdkLimitOptions is treated as a single default + // instance. + var sdkLimitOptions = sp.GetRequiredService>().CurrentValue; + + return BuildOtlpLogExporter( + sp, + exporterOptions, + sp.GetRequiredService>().Get(finalOptionsName), + sdkLimitOptions, + sp.GetRequiredService>().Get(finalOptionsName)); + }); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for + /// configuring and . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for + /// configuring and . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter( + this LoggerProviderBuilder builder, + string? name, + Action? configureExporterAndProcessor) + { + var finalOptionsName = name ?? Options.DefaultName; + + builder.ConfigureServices(RegisterOptions); + + return builder.AddProcessor(sp => + { + OtlpExporterOptions exporterOptions; + + if (name == null) + { + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + } + else + { + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } + + var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); + + // Configuration delegate is executed inline. + configureExporterAndProcessor?.Invoke(exporterOptions, processorOptions); + + // Note: Not using finalOptionsName here for SdkLimitOptions. + // There should only be one provider for a given service + // collection so SdkLimitOptions is treated as a single default + // instance. + var sdkLimitOptions = sp.GetRequiredService>().CurrentValue; + + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + sdkLimitOptions, + sp.GetRequiredService>().Get(finalOptionsName)); }); } @@ -110,25 +345,38 @@ internal static BaseProcessor BuildOtlpLogExporter( IServiceProvider sp, OtlpExporterOptions exporterOptions, LogRecordExportProcessorOptions processorOptions, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, Func, BaseExporter>? configureExporterInstance = null) { - if (sp == null) - { - throw new ArgumentNullException(nameof(sp)); - } - + // 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(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"); - var config = sp.GetRequiredService(); - - var sdkLimitOptions = new SdkLimitOptions(config); - var experimentalOptions = new ExperimentalOptions(config); + /* + * Note: + * + * We don't currently enable IHttpClientFactory for OtlpLogExporter. + * + * The DefaultHttpClientFactory requires the ILoggerFactory in its ctor: + * https://github.com/dotnet/runtime/blob/fa40ecf7d36bf4e31d7ae968807c1c529bac66d6/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs#L64 + * + * This creates a circular reference: ILoggerFactory -> + * OpenTelemetryLoggerProvider -> OtlpLogExporter -> IHttpClientFactory + * -> ILoggerFactory + * + * exporterOptions.TryEnableIHttpClientFactoryIntegration(sp, + * "OtlpLogExporter"); + */ BaseExporter otlpExporter = new OtlpLogExporter( exporterOptions!, - sdkLimitOptions, - experimentalOptions); + sdkLimitOptions!, + experimentalOptions!); if (configureExporterInstance != null) { @@ -152,7 +400,19 @@ internal static BaseProcessor BuildOtlpLogExporter( } } - private static OtlpExporterOptions GetOtlpExporterOptions(IServiceProvider sp, string? name, string finalName) + private static void RegisterOptions(IServiceCollection services) + { + OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); + services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); + services.RegisterOptionsFactory(configuration => new ExperimentalOptions(configuration)); + } + + private static T GetOptions( + IServiceProvider sp, + string? name, + string finalName, + Func createOptionsFunc) + where T : class, new() { // Note: If OtlpExporter has been registered for tracing and/or metrics // then IOptionsFactory will be set by a call to @@ -160,15 +420,15 @@ private static OtlpExporterOptions GetOtlpExporterOptions(IServiceProvider sp, s // are only using logging, we don't have an opportunity to do that // registration so we manually create a factory. - var optionsFactory = sp.GetRequiredService>(); - if (optionsFactory is not DelegatingOptionsFactory) + var optionsFactory = sp.GetRequiredService>(); + if (optionsFactory is not DelegatingOptionsFactory) { - optionsFactory = new DelegatingOptionsFactory( - (c, n) => OtlpExporterOptions.CreateOtlpExporterOptions(sp, c, n), + optionsFactory = new DelegatingOptionsFactory( + (c, n) => createOptionsFunc(sp, c, n), sp.GetRequiredService(), - sp.GetServices>(), - sp.GetServices>(), - sp.GetServices>()); + sp.GetServices>(), + sp.GetServices>(), + sp.GetServices>()); return optionsFactory.Create(finalName); } @@ -184,6 +444,6 @@ private static OtlpExporterOptions GetOtlpExporterOptions(IServiceProvider sp, s // If we have a valid factory AND we are using named options, we can // safely use the Options API fully. - return sp.GetRequiredService>().Get(finalName); + return sp.GetRequiredService>().Get(finalName); } } diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs index f81045c00c0..6d7afc1ee68 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs @@ -7,6 +7,7 @@ #if EXPOSE_EXPERIMENTAL_FEATURES using System.ComponentModel; #endif +using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -169,9 +170,15 @@ private static ILoggingBuilder AddOpenTelemetryInternal( var services = builder.Services; - // Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions + // Note: This will bind logger options element (e.g., "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions RegisterLoggerProviderOptions(services); + /* Note: This ensures IConfiguration is available when using + * IServiceCollections NOT attached to a host. For example when + * performing: + * + * new ServiceCollection().AddLogging(b => b.AddOpenTelemetry()) + */ services.AddOpenTelemetrySharedProviderBuilderServices(); if (configureOptions != null) @@ -206,10 +213,45 @@ private static ILoggingBuilder AddOpenTelemetryInternal( services.TryAddEnumerable( ServiceDescriptor.Singleton( - sp => new OpenTelemetryLoggerProvider( - sp.GetRequiredService(), - sp.GetRequiredService>().CurrentValue, - disposeProvider: false))); + sp => + { + var state = sp.GetRequiredService(); + + var provider = state.Provider; + if (provider == null) + { + /* + * Note: + * + * There is a possibility of a circular reference when + * accessing LoggerProvider from the IServiceProvider. + * + * If LoggerProvider is the first thing accessed, and it + * requires some service which accesses ILogger (for + * example, IHttpClientFactory), then the + * OpenTelemetryLoggerProvider will try to access a new + * (second) LoggerProvider while still in the process of + * building the first one: + * + * LoggerProvider -> IHttpClientFactory -> + * ILoggerFactory -> OpenTelemetryLoggerProvider -> + * LoggerProvider + * + * This check uses the provider reference captured on + * LoggerProviderBuilderSdk during construction of + * LoggerProviderSdk to detect if a provider has already + * been created to give to OpenTelemetryLoggerProvider + * and stop the loop. + */ + provider = sp.GetRequiredService(); + Debug.Assert(provider == state.Provider, "state.Provider did not match resolved LoggerProvider."); + } + + return new OpenTelemetryLoggerProvider( + provider, + sp.GetRequiredService>().CurrentValue, + disposeProvider: false); + })); return builder; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index 0fc08270cdf..11b5b797127 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -238,6 +238,8 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin sp, exporterOptions, processorOptions, + new SdkLimitOptions(), + new ExperimentalOptions(), configureExporterInstance: otlpExporter => { delegatingExporter = new DelegatingExporter diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 80df9effcdb..9d338f8ec7a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -27,6 +27,125 @@ public class OtlpLogExporterTests : Http2UnencryptedSupportTests { private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + [Fact] + public void AddOtlpExporterWithNamedOptions() + { + int defaultConfigureExporterOptionsInvocations = 0; + int namedConfigureExporterOptionsInvocations = 0; + + int defaultConfigureSdkLimitsOptionsInvocations = 0; + int namedConfigureSdkLimitsOptionsInvocations = 0; + + using var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + + services.Configure(o => defaultConfigureSdkLimitsOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureSdkLimitsOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureSdkLimitsOptionsInvocations++); + }) + .AddOtlpExporter() + .AddOtlpExporter("Exporter2", o => { }) + .AddOtlpExporter("Exporter3", o => { }) + .Build(); + + Assert.Equal(3, defaultConfigureExporterOptionsInvocations); + Assert.Equal(6, namedConfigureExporterOptionsInvocations); + + // Note: SdkLimitOptions does NOT support named options. We only allow a + // single instance for a given IServiceCollection. + Assert.Equal(1, defaultConfigureSdkLimitsOptionsInvocations); + Assert.Equal(0, namedConfigureSdkLimitsOptionsInvocations); + } + + [Fact] + public void UserHttpFactoryCalledWhenUsingHttpProtobuf() + { + OtlpExporterOptions options = new OtlpExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new OtlpLogExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateLoggerProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); + } + + options.HttpClientFactory = null; + Assert.Throws(() => + { + using var exporter = new OtlpLogExporter(options); + }); + } + + [Fact] + public void AddOtlpExporterSetsDefaultBatchExportProcessor() + { + if (Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .AddOtlpExporter() + .Build(); + + CheckProcessorDefaults(); + + loggerProvider.Dispose(); + + void CheckProcessorDefaults() + { + var bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic; + + var processor = typeof(BaseProcessor) + .Assembly + .GetType("OpenTelemetry.Logs.LoggerProviderSdk") + .GetProperty("Processor", bindingFlags) + .GetValue(loggerProvider) as BatchExportProcessor; + + Assert.NotNull(processor); + + var scheduledDelayMilliseconds = typeof(BatchExportProcessor) + .GetField("scheduledDelayMilliseconds", bindingFlags) + .GetValue(processor); + + Assert.Equal(5000, scheduledDelayMilliseconds); + } + } + [Fact] public void AddOtlpLogExporterReceivesAttributesWithParseStateValueSetToFalse() { diff --git a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs index ddf0c27a6ec..cdebc61f176 100644 --- a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs @@ -266,7 +266,60 @@ public void VerifyExceptionIsThrownWhenImplementationFactoryIsNull() Assert.Throws(() => sp.GetRequiredService() as LoggerProviderSdk); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CircularReferenceTest(bool requestLoggerProviderDirectly) + { + var services = new ServiceCollection(); + + services.AddLogging(logging => logging.AddOpenTelemetry()); + + services.ConfigureOpenTelemetryLoggerProvider(builder => builder.AddProcessor()); + + using var sp = services.BuildServiceProvider(); + + if (requestLoggerProviderDirectly) + { + var provider = sp.GetRequiredService(); + Assert.NotNull(provider); + } + else + { + var factory = sp.GetRequiredService(); + Assert.NotNull(factory); + } + + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(loggerProvider); + + Assert.True(loggerProvider.Processor is TestLogProcessorWithILoggerFactoryDependency); + } + private class TestLogProcessor : BaseProcessor { } + + private class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor + { + private readonly ILogger logger; + + public TestLogProcessorWithILoggerFactoryDependency(ILoggerFactory loggerFactory) + { + // Note: It is NOT recommended to log from inside a processor. This + // test is meant to mirror someone injecting IHttpClientFactory + // (which itself uses ILoggerFactory) as part of an exporter. That + // is a more realistic scenario but needs a dependency to do that so + // here we approximate the graph. + this.logger = loggerFactory.CreateLogger("MyLogger"); + } + + protected override void Dispose(bool disposing) + { + this.logger.LogInformation("Dispose called"); + + base.Dispose(disposing); + } + } }