Skip to content
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Notes](../../RELEASENOTES.md).
* Update `OpenTelemetrySdkEventSource` to support the W3C randomness flag.
([#7301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7301))

* Fix circular reference when `LoggerProvider` requires `ILogger`.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO))
Comment thread
martincostello marked this conversation as resolved.
Outdated

## 1.15.3

Released 2026-Apr-21
Expand Down
39 changes: 34 additions & 5 deletions src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ namespace OpenTelemetry.Logs;
[ProviderAlias("OpenTelemetry")]
public class OpenTelemetryLoggerProvider : BaseProvider, ILoggerProvider, ISupportExternalScope
{
internal readonly LoggerProvider Provider;
private readonly Lock syncObject = new();
private readonly bool ownsProvider;
private readonly Hashtable loggers = [];
private Func<LoggerProvider>? loggerProviderFactory;
private LoggerProvider? provider;
private bool disposed;

static OpenTelemetryLoggerProvider()
Expand All @@ -40,7 +42,7 @@ public OpenTelemetryLoggerProvider(IOptionsMonitor<OpenTelemetryLoggerOptions> o
var optionsInstance = options.CurrentValue;
#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1

this.Provider = Sdk
this.provider = Sdk
.CreateLoggerProviderBuilder()
.ConfigureBuilder((sp, builder) =>
{
Expand All @@ -61,15 +63,42 @@ public OpenTelemetryLoggerProvider(IOptionsMonitor<OpenTelemetryLoggerOptions> o
}

internal OpenTelemetryLoggerProvider(
LoggerProvider loggerProvider,
Func<LoggerProvider> loggerProviderFactory,
OpenTelemetryLoggerOptions options,
bool disposeProvider)
{
this.Provider = loggerProvider;
Guard.ThrowIfNull(loggerProviderFactory);

this.loggerProviderFactory = loggerProviderFactory;
this.Options = options.Copy();
this.ownsProvider = disposeProvider;
}

internal LoggerProvider Provider
{
get
{
var provider = this.provider;
if (provider != null)
{
return provider;
}

lock (this.syncObject)
{
provider = this.provider;
if (provider == null)
{
provider = this.loggerProviderFactory!();
this.provider = provider;
this.loggerProviderFactory = null;
}

return provider;
}
}
}

internal OpenTelemetryLoggerOptions Options { get; }

internal IExternalScopeProvider? ScopeProvider { get; private set; }
Expand Down Expand Up @@ -130,7 +159,7 @@ protected override void Dispose(bool disposing)
{
if (this.ownsProvider)
{
this.Provider.Dispose();
this.provider?.Dispose();
}
}

Expand Down
65 changes: 34 additions & 31 deletions src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,38 +196,41 @@ private static ILoggingBuilder AddOpenTelemetryInternal(
{
var state = sp.GetRequiredService<LoggerProviderBuilderSdk>();

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<LoggerProvider>();
Debug.Assert(provider == state.Provider, "state.Provider did not match resolved LoggerProvider.");
}

return new OpenTelemetryLoggerProvider(
provider,
() =>
{
var provider = state.Provider;
if (provider != null)
{
return provider;
}

/*
* Note:
*
* There is a possibility of a circular reference when
* accessing LoggerProvider from the IServiceProvider.
*
* If ILoggerFactory is the first thing accessed while
* LoggerProvider is still being built, then
* OpenTelemetryLoggerProvider.CreateLogger will need
* to use the provider that is already under
* construction instead of asking DI to create a
* second provider:
*
* 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 and stop the loop.
*/
provider = sp.GetRequiredService<LoggerProvider>();
Debug.Assert(provider == state.Provider, "state.Provider did not match resolved LoggerProvider.");
return provider;
},
sp.GetRequiredService<IOptions<OpenTelemetryLoggerOptions>>().Value,
disposeProvider: false);
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public void UseOpenTelemetryDependencyInjectionTest()

Assert.NotNull(loggerProvider.Processor);

Assert.True(loggerProvider.Processor is TestLogProcessor);
Assert.IsType<TestLogProcessor>(loggerProvider.Processor);
}

[Fact]
Expand Down Expand Up @@ -246,7 +246,7 @@ public void VerifyAddProcessorOverloadWithImplementationFactory()
// assert
Assert.NotNull(loggerProvider);
Assert.NotNull(loggerProvider.Processor);
Assert.True(loggerProvider.Processor is TestLogProcessor);
Assert.IsType<TestLogProcessor>(loggerProvider.Processor);
}

[Fact]
Expand Down Expand Up @@ -288,13 +288,15 @@ public void CircularReferenceTest(bool requestLoggerProviderDirectly)
{
var factory = sp.GetRequiredService<ILoggerFactory>();
Assert.NotNull(factory);

var logger = factory.CreateLogger("MyLogger");
Assert.NotNull(logger);
}

var loggerProvider = sp.GetRequiredService<LoggerProvider>() as LoggerProviderSdk;

Assert.NotNull(loggerProvider);

Assert.True(loggerProvider.Processor is TestLogProcessorWithILoggerFactoryDependency);
Assert.IsType<TestLogProcessorWithILoggerFactoryDependency>(loggerProvider.Processor);
}

[Theory]
Expand Down
Loading