Skip to content

Commit fb519ea

Browse files
evgenyfedorov2evgenyfedorov2
andauthored
Fix dynamic config update for log buffering (#6435)
Co-authored-by: evgenyfedorov2 <evgenii.fedorov@microsoft.com>
1 parent b2298a6 commit fb519ea

File tree

9 files changed

+214
-19
lines changed

9 files changed

+214
-19
lines changed

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b
3939
_ = Throw.IfNull(configuration);
4040

4141
_ = builder.Services
42-
.AddSingleton<IConfigureOptions<PerRequestLogBufferingOptions>>(new PerRequestLogBufferingConfigureOptions(configuration))
42+
.AddSingleton<IConfigureOptions<PerRequestLogBufferingOptions>>(
43+
new PerRequestLogBufferingConfigureOptions(configuration))
44+
.AddSingleton<IOptionsChangeTokenSource<PerRequestLogBufferingOptions>>(
45+
new ConfigurationChangeTokenSource<PerRequestLogBufferingOptions>(configuration))
4346
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
4447
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>();
4548

@@ -64,8 +67,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b
6467
_ = Throw.IfNull(builder);
6568
_ = Throw.IfNull(configure);
6669

67-
_ = builder.Services
68-
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
70+
_ = builder
71+
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
6972
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>()
7073
.Configure(configure);
7174

@@ -92,8 +95,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b
9295
{
9396
_ = Throw.IfNull(builder);
9497

95-
_ = builder.Services
96-
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
98+
_ = builder
99+
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
97100
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>()
98101
.Configure(options =>
99102
{

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering;
1212

1313
internal sealed class PerRequestLogBufferManager : PerRequestLogBuffer
1414
{
15+
internal readonly IOptionsMonitor<PerRequestLogBufferingOptions> Options;
16+
1517
private readonly GlobalLogBuffer _globalBuffer;
1618
private readonly IHttpContextAccessor _httpContextAccessor;
1719
private readonly LogBufferingFilterRuleSelector _ruleSelector;
18-
private readonly IOptionsMonitor<PerRequestLogBufferingOptions> _options;
1920

2021
public PerRequestLogBufferManager(
2122
GlobalLogBuffer globalBuffer,
@@ -26,7 +27,7 @@ public PerRequestLogBufferManager(
2627
_globalBuffer = globalBuffer;
2728
_httpContextAccessor = httpContextAccessor;
2829
_ruleSelector = ruleSelector;
29-
_options = options;
30+
Options = options;
3031
}
3132

3233
public override void Flush()
@@ -47,7 +48,7 @@ public override bool TryEnqueue<TState>(IBufferedLogger bufferedLogger, in LogEn
4748
IncomingRequestLogBufferHolder? bufferHolder =
4849
httpContext.RequestServices.GetService<IncomingRequestLogBufferHolder>();
4950
IncomingRequestLogBuffer? buffer = bufferHolder?.GetOrAdd(category, _ =>
50-
new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, _options));
51+
new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, Options));
5152

5253
if (buffer is null)
5354
{

src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Microsoft.Extensions.Diagnostics.Buffering;
1717

1818
internal sealed class GlobalBuffer : IDisposable
1919
{
20+
internal LogBufferingFilterRule[] LastKnownGoodFilterRules;
21+
2022
private const int MaxBatchSize = 256;
2123
private static readonly ObjectPool<List<DeserializedLogRecord>> _recordsToEmitListPool =
2224
PoolFactory.CreateListPoolWithCapacity<DeserializedLogRecord>(MaxBatchSize);
@@ -34,7 +36,6 @@ internal sealed class GlobalBuffer : IDisposable
3436

3537
private DateTimeOffset _lastFlushTimestamp;
3638
private int _activeBufferSize;
37-
private LogBufferingFilterRule[] _lastKnownGoodFilterRules;
3839

3940
private volatile bool _disposed;
4041

@@ -50,7 +51,7 @@ public GlobalBuffer(
5051
_bufferedLogger = bufferedLogger;
5152
_category = Throw.IfNullOrEmpty(category);
5253
_ruleSelector = Throw.IfNull(ruleSelector);
53-
_lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category);
54+
LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category);
5455
_optionsChangeTokenRegistration = options.OnChange(OnOptionsChanged);
5556
}
5657

@@ -83,7 +84,7 @@ public bool TryEnqueue<TState>(LogEntry<TState> logEntry)
8384
$"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList<KeyValuePair<string, object?>>");
8485
}
8586

86-
if (_ruleSelector.Select(_lastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null)
87+
if (_ruleSelector.Select(LastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null)
8788
{
8889
// buffering is not enabled for this log entry,
8990
// return false to indicate that the log entry should be logged normally.
@@ -162,11 +163,11 @@ private void OnOptionsChanged(GlobalLogBufferingOptions? updatedOptions)
162163
{
163164
if (updatedOptions is null)
164165
{
165-
_lastKnownGoodFilterRules = [];
166+
LastKnownGoodFilterRules = [];
166167
}
167168
else
168169
{
169-
_lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category);
170+
LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category);
170171
}
171172

172173
_ruleSelector.InvalidateCache();

src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, ICon
3838
_ = builder
3939
.Services.AddOptionsWithValidateOnStart<GlobalLogBufferingOptions, GlobalLogBufferingOptionsValidator>()
4040
.Services.AddOptionsWithValidateOnStart<GlobalLogBufferingOptions, GlobalLogBufferingOptionsCustomValidator>()
41-
.Services.AddSingleton<IConfigureOptions<GlobalLogBufferingOptions>>(new GlobalLogBufferingConfigureOptions(configuration));
41+
.Services.AddSingleton<IConfigureOptions<GlobalLogBufferingOptions>>(
42+
new GlobalLogBufferingConfigureOptions(configuration))
43+
.AddSingleton<IOptionsChangeTokenSource<GlobalLogBufferingOptions>>(
44+
new ConfigurationChangeTokenSource<GlobalLogBufferingOptions>(configuration));
4245

4346
return builder.AddGlobalBufferManager();
4447
}

src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering;
1111

1212
internal sealed class GlobalLogBufferManager : GlobalLogBuffer
1313
{
14-
private readonly ConcurrentDictionary<string, GlobalBuffer> _buffers = [];
14+
internal readonly ConcurrentDictionary<string, GlobalBuffer> Buffers = [];
1515
private readonly IOptionsMonitor<GlobalLogBufferingOptions> _options;
1616
private readonly TimeProvider _timeProvider;
1717
private readonly LogBufferingFilterRuleSelector _ruleSelector;
@@ -35,7 +35,7 @@ internal GlobalLogBufferManager(
3535

3636
public override void Flush()
3737
{
38-
foreach (GlobalBuffer buffer in _buffers.Values)
38+
foreach (GlobalBuffer buffer in Buffers.Values)
3939
{
4040
buffer.Flush();
4141
}
@@ -44,7 +44,7 @@ public override void Flush()
4444
public override bool TryEnqueue<TState>(IBufferedLogger bufferedLogger, in LogEntry<TState> logEntry)
4545
{
4646
string category = logEntry.Category;
47-
GlobalBuffer buffer = _buffers.GetOrAdd(category, _ => new GlobalBuffer(
47+
GlobalBuffer buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer(
4848
bufferedLogger,
4949
category,
5050
_ruleSelector,

test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@
33
#if NET9_0_OR_GREATER
44
using System;
55
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Builder;
68
using Microsoft.AspNetCore.Diagnostics.Buffering;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.TestHost;
711
using Microsoft.Extensions.Configuration;
812
using Microsoft.Extensions.DependencyInjection;
913
using Microsoft.Extensions.Diagnostics.Buffering;
14+
using Microsoft.Extensions.Hosting;
15+
using Microsoft.Extensions.Hosting.Testing;
16+
using Microsoft.Extensions.Logging;
17+
using Microsoft.Extensions.Logging.Test;
1018
using Microsoft.Extensions.Options;
1119
using Xunit;
1220
using PerRequestLogBuffer = Microsoft.Extensions.Diagnostics.Buffering.PerRequestLogBuffer;
1321

14-
namespace Microsoft.Extensions.Logging;
22+
namespace Microsoft.AspNetCore.Diagnostics.Logging.Test;
1523

1624
public class PerIncomingRequestLoggingBuilderExtensionsTests
1725
{
@@ -82,5 +90,72 @@ public void WhenConfigurationActionProvided_RegistersInDI()
8290
Assert.NotNull(options.CurrentValue);
8391
Assert.Equivalent(expectedData, options.CurrentValue.Rules);
8492
}
93+
94+
[Fact]
95+
public async Task WhenConfigUpdated_PicksUpConfigChanges()
96+
{
97+
List<LogBufferingFilterRule> initialData =
98+
[
99+
new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"),
100+
new(logLevel : LogLevel.Information),
101+
];
102+
List<LogBufferingFilterRule> updatedData =
103+
[
104+
new(logLevel: LogLevel.Information),
105+
];
106+
string jsonConfig =
107+
@"
108+
{
109+
""PerIncomingRequestLogBuffering"": {
110+
""Rules"": [
111+
{
112+
""CategoryName"": ""Program.MyLogger"",
113+
""LogLevel"": ""Information"",
114+
""EventId"": 1,
115+
""EventName"": ""number one"",
116+
},
117+
{
118+
""LogLevel"": ""Information"",
119+
},
120+
]
121+
}
122+
}
123+
";
124+
125+
using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig);
126+
using IHost host = await FakeHost.CreateBuilder()
127+
.ConfigureWebHost(builder => builder
128+
.UseTestServer()
129+
.ConfigureServices(x => x.AddRouting())
130+
.ConfigureLogging(loggingBuilder => loggingBuilder
131+
.AddPerIncomingRequestBuffer(config))
132+
.Configure(app => app.UseRouting()))
133+
.StartAsync();
134+
135+
IOptionsMonitor<PerRequestLogBufferingOptions>? options = host.Services.GetService<IOptionsMonitor<PerRequestLogBufferingOptions>>();
136+
Assert.NotNull(options);
137+
Assert.NotNull(options.CurrentValue);
138+
Assert.Equivalent(initialData, options.CurrentValue.Rules);
139+
140+
jsonConfig =
141+
@"
142+
{
143+
""PerIncomingRequestLogBuffering"": {
144+
""Rules"": [
145+
{
146+
""LogLevel"": ""Information"",
147+
},
148+
]
149+
}
150+
}
151+
";
152+
config.Reload();
153+
154+
var bufferManager = host.Services.GetRequiredService<PerRequestLogBuffer>() as PerRequestLogBufferManager;
155+
Assert.NotNull(bufferManager);
156+
Assert.Equivalent(updatedData, bufferManager.Options.CurrentValue.Rules, strict: true);
157+
158+
await host.StopAsync();
159+
}
85160
}
86161
#endif
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.Configuration.Json;
9+
10+
namespace Microsoft.Extensions.Logging.Test;
11+
12+
internal class TestConfiguration : JsonConfigurationProvider
13+
{
14+
private Func<string> _json;
15+
16+
public TestConfiguration(JsonConfigurationSource source, Func<string> json)
17+
: base(source)
18+
{
19+
_json = json;
20+
}
21+
22+
public static ConfigurationRoot Create(Func<string> getJson)
23+
{
24+
var provider = new TestConfiguration(new JsonConfigurationSource { Optional = true }, getJson);
25+
return new ConfigurationRoot(new List<IConfigurationProvider> { provider });
26+
}
27+
28+
public override void Load()
29+
{
30+
var stream = new MemoryStream();
31+
using var writer = new StreamWriter(stream);
32+
writer.Write(_json());
33+
writer.Flush();
34+
stream.Seek(0, SeekOrigin.Begin);
35+
Load(stream);
36+
}
37+
}

test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Extensions.Configuration;
77
using Microsoft.Extensions.DependencyInjection;
88
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Logging.Test;
910
using Microsoft.Extensions.Options;
1011
using Xunit;
1112

@@ -68,5 +69,79 @@ public void WithConfiguration_RegistersInDI()
6869
Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.AutoFlushDuration); // value comes from default
6970
Assert.Equivalent(expectedData, options.CurrentValue.Rules);
7071
}
72+
73+
[Fact]
74+
public void WhenConfigUpdated_PicksUpConfigChanges()
75+
{
76+
List<LogBufferingFilterRule> initialData =
77+
[
78+
new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"),
79+
new(logLevel : LogLevel.Information),
80+
];
81+
List<LogBufferingFilterRule> updatedData =
82+
[
83+
new(logLevel: LogLevel.Information),
84+
];
85+
string jsonConfig =
86+
@"
87+
{
88+
""GlobalLogBuffering"": {
89+
""Rules"": [
90+
{
91+
""CategoryName"": ""Program.MyLogger"",
92+
""LogLevel"": ""Information"",
93+
""EventId"": 1,
94+
""EventName"": ""number one"",
95+
},
96+
{
97+
""LogLevel"": ""Information"",
98+
},
99+
]
100+
}
101+
}
102+
";
103+
104+
using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig);
105+
106+
using ExtendedLoggerTests.Provider provider = new ExtendedLoggerTests.Provider();
107+
using ILoggerFactory factory = Utils.CreateLoggerFactory(
108+
builder =>
109+
{
110+
builder.AddProvider(provider);
111+
builder.AddGlobalBuffer(config);
112+
});
113+
ILogger logger = factory.CreateLogger("Program.MyLogger");
114+
Utils.DisposingLoggerFactory dlf = (Utils.DisposingLoggerFactory)factory;
115+
var bufferManager = dlf.ServiceProvider.GetRequiredService<GlobalLogBuffer>() as GlobalLogBufferManager;
116+
117+
IOptionsMonitor<GlobalLogBufferingOptions>? options = dlf.ServiceProvider.GetService<IOptionsMonitor<GlobalLogBufferingOptions>>();
118+
Assert.NotNull(options);
119+
Assert.NotNull(options.CurrentValue);
120+
Assert.Equivalent(initialData, options.CurrentValue.Rules);
121+
122+
// this is just to trigger creating an internal buffer:
123+
logger.LogInformation(new EventId(1, "number one"), null);
124+
125+
jsonConfig =
126+
@"
127+
{
128+
""GlobalLogBuffering"": {
129+
""Rules"": [
130+
{
131+
""LogLevel"": ""Information"",
132+
},
133+
]
134+
}
135+
}
136+
";
137+
config.Reload();
138+
139+
Assert.NotNull(bufferManager);
140+
Assert.NotEmpty(bufferManager.Buffers);
141+
foreach (GlobalBuffer buffer in bufferManager.Buffers.Values)
142+
{
143+
Assert.Equivalent(updatedData, buffer.LastKnownGoodFilterRules, strict: true);
144+
}
145+
}
71146
}
72147
#endif

test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,7 @@ private enum ThrowExceptionAt
11141114
IsEnabled
11151115
}
11161116

1117-
private sealed class Provider : ILoggerProvider
1117+
internal sealed class Provider : ILoggerProvider
11181118
{
11191119
public FakeLogger? Logger { get; private set; }
11201120

0 commit comments

Comments
 (0)