Skip to content

Commit 0bbebb5

Browse files
CodeBlanchreyang
andauthored
[logs] Mitigate unwanted object creation during configuration reload (#5514)
Co-authored-by: Reiley Yang <reyang@microsoft.com>
1 parent 876e4fa commit 0bbebb5

File tree

8 files changed

+172
-4
lines changed

8 files changed

+172
-4
lines changed

OpenTelemetry.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Options", "Options", "{4949
300300
ProjectSection(SolutionItems) = preProject
301301
src\Shared\Options\DelegatingOptionsFactory.cs = src\Shared\Options\DelegatingOptionsFactory.cs
302302
src\Shared\Options\DelegatingOptionsFactoryServiceCollectionExtensions.cs = src\Shared\Options\DelegatingOptionsFactoryServiceCollectionExtensions.cs
303+
src\Shared\Options\SingletonOptionsManager.cs = src\Shared\Options\SingletonOptionsManager.cs
303304
EndProjectSection
304305
EndProject
305306
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shims", "Shims", "{A0CB9A10-F22D-4E66-A449-74B3D0361A9C}"

src/OpenTelemetry/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
* Fixed an issue in Logging where unwanted objects (processors, exporters, etc.)
6+
could be created inside delegates automatically executed by the Options API
7+
during configuration reload.
8+
([#5514](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5514))
9+
510
## 1.8.0
611

712
Released 2024-Apr-02

src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public OpenTelemetryLoggerOptions SetResourceBuilder(ResourceBuilder resourceBui
117117
Guard.ThrowIfNull(resourceBuilder);
118118

119119
this.ResourceBuilder = resourceBuilder;
120+
120121
return this;
121122
}
122123

src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ private static ILoggingBuilder AddOpenTelemetryInternal(
173173
// Note: This will bind logger options element (e.g., "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions
174174
RegisterLoggerProviderOptions(services);
175175

176+
// Note: We disable built-in IOptionsMonitor and IOptionsSnapshot
177+
// features for OpenTelemetryLoggerOptions as a workaround to prevent
178+
// unwanted objects (processors, exporters, etc.) being created by
179+
// configuration delegates being re-run during reload of IConfiguration
180+
// or from options created while under scopes.
181+
services.DisableOptionsReloading<OpenTelemetryLoggerOptions>();
182+
176183
/* Note: This ensures IConfiguration is available when using
177184
* IServiceCollections NOT attached to a host. For example when
178185
* performing:
@@ -192,7 +199,7 @@ private static ILoggingBuilder AddOpenTelemetryInternal(
192199
var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder(
193200
(sp, logging) =>
194201
{
195-
var options = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>().CurrentValue;
202+
var options = sp.GetRequiredService<IOptions<OpenTelemetryLoggerOptions>>().Value;
196203

197204
if (options.ResourceBuilder != null)
198205
{
@@ -249,7 +256,7 @@ private static ILoggingBuilder AddOpenTelemetryInternal(
249256

250257
return new OpenTelemetryLoggerProvider(
251258
provider,
252-
sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>().CurrentValue,
259+
sp.GetRequiredService<IOptions<OpenTelemetryLoggerOptions>>().Value,
253260
disposeProvider: false);
254261
}));
255262

src/Shared/Options/DelegatingOptionsFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public DelegatingOptionsFactory(
8181
public TOptions Create(string name)
8282
{
8383
TOptions options = this.optionsFactoryFunc(this.configuration, name);
84+
8485
foreach (IConfigureOptions<TOptions> setup in _setups)
8586
{
8687
if (setup is IConfigureNamedOptions<TOptions> namedSetup)

src/Shared/Options/DelegatingOptionsFactoryServiceCollectionExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ public static IServiceCollection RegisterOptionsFactory<T>(
6262
sp.GetServices<IValidateOptions<T>>());
6363
});
6464

65+
return services!;
66+
}
67+
68+
#if NET6_0_OR_GREATER
69+
public static IServiceCollection DisableOptionsReloading<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
70+
#else
71+
public static IServiceCollection DisableOptionsReloading<T>(
72+
#endif
73+
this IServiceCollection services)
74+
where T : class
75+
{
76+
Debug.Assert(services != null, "services was null");
77+
78+
services!.TryAddSingleton<IOptionsMonitor<T>, SingletonOptionsManager<T>>();
79+
services!.TryAddScoped<IOptionsSnapshot<T>, SingletonOptionsManager<T>>();
80+
6581
return services!;
6682
}
6783
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#nullable enable
5+
6+
#if NET6_0_OR_GREATER
7+
using System.Diagnostics.CodeAnalysis;
8+
#endif
9+
10+
namespace Microsoft.Extensions.Options;
11+
12+
#if NET6_0_OR_GREATER
13+
internal sealed class SingletonOptionsManager<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> : IOptionsMonitor<TOptions>, IOptionsSnapshot<TOptions>
14+
#else
15+
internal sealed class SingletonOptionsManager<TOptions> : IOptionsMonitor<TOptions>, IOptionsSnapshot<TOptions>
16+
#endif
17+
where TOptions : class
18+
{
19+
private readonly TOptions instance;
20+
21+
public SingletonOptionsManager(IOptions<TOptions> options)
22+
{
23+
this.instance = options.Value;
24+
}
25+
26+
public TOptions CurrentValue => this.instance;
27+
28+
public TOptions Value => this.instance;
29+
30+
public TOptions Get(string? name) => this.instance;
31+
32+
public IDisposable? OnChange(Action<TOptions, string?> listener)
33+
=> NoopChangeNotification.Instance;
34+
35+
private sealed class NoopChangeNotification : IDisposable
36+
{
37+
private NoopChangeNotification()
38+
{
39+
}
40+
41+
public static NoopChangeNotification Instance { get; } = new();
42+
43+
public void Dispose()
44+
{
45+
}
46+
}
47+
}

test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.Configuration;
88
using Microsoft.Extensions.DependencyInjection;
99
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Options;
1011
using Xunit;
1112

1213
namespace OpenTelemetry.Logs.Tests;
@@ -297,11 +298,100 @@ public void CircularReferenceTest(bool requestLoggerProviderDirectly)
297298
Assert.True(loggerProvider.Processor is TestLogProcessorWithILoggerFactoryDependency);
298299
}
299300

300-
private class TestLogProcessor : BaseProcessor<LogRecord>
301+
[Theory]
302+
[InlineData(true, false)]
303+
[InlineData(false, true)]
304+
[InlineData(false, false)]
305+
public void OptionReloadingTest(bool useOptionsMonitor, bool useOptionsSnapshot)
306+
{
307+
var delegateInvocationCount = 0;
308+
309+
var root = new ConfigurationBuilder().Build();
310+
311+
var services = new ServiceCollection();
312+
313+
services.AddSingleton<IConfiguration>(root);
314+
315+
services.AddLogging(logging => logging
316+
.AddConfiguration(root.GetSection("logging"))
317+
.AddOpenTelemetry(options =>
318+
{
319+
delegateInvocationCount++;
320+
321+
options.AddProcessor(new TestLogProcessor());
322+
}));
323+
324+
using var sp = services.BuildServiceProvider();
325+
326+
if (useOptionsMonitor)
327+
{
328+
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>();
329+
330+
Assert.NotNull(optionsMonitor.CurrentValue);
331+
}
332+
333+
if (useOptionsSnapshot)
334+
{
335+
using var scope = sp.CreateScope();
336+
337+
var optionsSnapshot = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<OpenTelemetryLoggerOptions>>();
338+
339+
Assert.NotNull(optionsSnapshot.Value);
340+
}
341+
342+
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
343+
344+
Assert.Equal(1, delegateInvocationCount);
345+
346+
root.Reload();
347+
348+
Assert.Equal(1, delegateInvocationCount);
349+
}
350+
351+
[Fact]
352+
public void MixedOptionsUsageTest()
353+
{
354+
var root = new ConfigurationBuilder().Build();
355+
356+
var services = new ServiceCollection();
357+
358+
services.AddSingleton<IConfiguration>(root);
359+
360+
services.AddLogging(logging => logging
361+
.AddConfiguration(root.GetSection("logging"))
362+
.AddOpenTelemetry(options =>
363+
{
364+
options.AddProcessor(new TestLogProcessor());
365+
}));
366+
367+
using var sp = services.BuildServiceProvider();
368+
369+
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
370+
371+
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<OpenTelemetryLoggerOptions>>().CurrentValue;
372+
var options = sp.GetRequiredService<IOptions<OpenTelemetryLoggerOptions>>().Value;
373+
374+
Assert.True(ReferenceEquals(options, optionsMonitor));
375+
376+
using var scope = sp.CreateScope();
377+
378+
var optionsSnapshot = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<OpenTelemetryLoggerOptions>>().Value;
379+
Assert.True(ReferenceEquals(options, optionsSnapshot));
380+
}
381+
382+
private sealed class TestLogProcessor : BaseProcessor<LogRecord>
301383
{
384+
public bool Disposed;
385+
386+
protected override void Dispose(bool disposing)
387+
{
388+
this.Disposed = true;
389+
390+
base.Dispose(disposing);
391+
}
302392
}
303393

304-
private class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor<LogRecord>
394+
private sealed class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor<LogRecord>
305395
{
306396
private readonly ILogger logger;
307397

0 commit comments

Comments
 (0)