From e3759a1e0ea6e23b80cad33a206795fda8e240ff Mon Sep 17 00:00:00 2001
From: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com>
Date: Thu, 21 Dec 2023 17:22:41 -0800
Subject: [PATCH 1/2] Remove unnecessary package reference from SDK (#5198)
---
src/OpenTelemetry/OpenTelemetry.csproj | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj
index 4e5488c4622..45faafaec91 100644
--- a/src/OpenTelemetry/OpenTelemetry.csproj
+++ b/src/OpenTelemetry/OpenTelemetry.csproj
@@ -6,7 +6,6 @@
-
From 0889e8dc32badbb4c9be8fd24a974fb7e61fa779 Mon Sep 17 00:00:00 2001
From: Yun-Ting Lin
Date: Fri, 22 Dec 2023 18:29:00 -0800
Subject: [PATCH 2/2] Add otlp log extension methods for LoggerProviderBuilder
(#5103)
---
.../AssemblyInfo.cs | 2 +
.../Experimental/PublicAPI.Unshipped.txt | 5 +
.../CHANGELOG.md | 4 +
...etry.Exporter.OpenTelemetryProtocol.csproj | 3 +-
.../OtlpLogExporterHelperExtensions.cs | 308 ++++++++++++++++--
.../ILogger/OpenTelemetryLoggingExtensions.cs | 52 ++-
.../IntegrationTest/IntegrationTests.cs | 2 +
.../OtlpLogExporterTests.cs | 119 +++++++
.../OpenTelemetryLoggingExtensionsTests.cs | 53 +++
9 files changed, 518 insertions(+), 30 deletions(-)
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);
+ }
+ }
}