From 731d2020fe1146c07cede82cb1ca8b9986f31791 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 Date: Tue, 13 May 2025 12:10:19 +0200 Subject: [PATCH] Initial commit --- ...IncomingRequestLoggingBuilderExtensions.cs | 13 ++-- .../Buffering/PerRequestLogBufferManager.cs | 7 +- .../Buffering/GlobalBuffer.cs | 11 +-- .../GlobalBufferLoggingBuilderExtensions.cs | 5 +- .../Buffering/GlobalLogBufferManager.cs | 6 +- ...ingRequestLoggingBuilderExtensionsTests.cs | 77 ++++++++++++++++++- .../Buffering/TestConfiguration.cs | 37 +++++++++ ...lobalBufferLoggerBuilderExtensionsTests.cs | 75 ++++++++++++++++++ .../Logging/ExtendedLoggerTests.cs | 2 +- 9 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs index 8d3f7411367..41664d466ce 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs @@ -39,7 +39,10 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b _ = Throw.IfNull(configuration); _ = builder.Services - .AddSingleton>(new PerRequestLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new PerRequestLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new ConfigurationChangeTokenSource(configuration)) .AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart(); @@ -64,8 +67,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - _ = builder.Services - .AddOptionsWithValidateOnStart() + _ = builder + .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() .Configure(configure); @@ -92,8 +95,8 @@ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder b { _ = Throw.IfNull(builder); - _ = builder.Services - .AddOptionsWithValidateOnStart() + _ = builder + .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() .Configure(options => { diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs index de51e26eb58..60dafd1e177 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs @@ -12,10 +12,11 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class PerRequestLogBufferManager : PerRequestLogBuffer { + internal readonly IOptionsMonitor Options; + private readonly GlobalLogBuffer _globalBuffer; private readonly IHttpContextAccessor _httpContextAccessor; private readonly LogBufferingFilterRuleSelector _ruleSelector; - private readonly IOptionsMonitor _options; public PerRequestLogBufferManager( GlobalLogBuffer globalBuffer, @@ -26,7 +27,7 @@ public PerRequestLogBufferManager( _globalBuffer = globalBuffer; _httpContextAccessor = httpContextAccessor; _ruleSelector = ruleSelector; - _options = options; + Options = options; } public override void Flush() @@ -47,7 +48,7 @@ public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEn IncomingRequestLogBufferHolder? bufferHolder = httpContext.RequestServices.GetService(); IncomingRequestLogBuffer? buffer = bufferHolder?.GetOrAdd(category, _ => - new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, _options)); + new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, Options)); if (buffer is null) { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 64bd8ffe439..5d0391e68e6 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -17,6 +17,8 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalBuffer : IDisposable { + internal LogBufferingFilterRule[] LastKnownGoodFilterRules; + private const int MaxBatchSize = 256; private static readonly ObjectPool> _recordsToEmitListPool = PoolFactory.CreateListPoolWithCapacity(MaxBatchSize); @@ -34,7 +36,6 @@ internal sealed class GlobalBuffer : IDisposable private DateTimeOffset _lastFlushTimestamp; private int _activeBufferSize; - private LogBufferingFilterRule[] _lastKnownGoodFilterRules; private volatile bool _disposed; @@ -50,7 +51,7 @@ public GlobalBuffer( _bufferedLogger = bufferedLogger; _category = Throw.IfNullOrEmpty(category); _ruleSelector = Throw.IfNull(ruleSelector); - _lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category); + LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), _category); _optionsChangeTokenRegistration = options.OnChange(OnOptionsChanged); } @@ -83,7 +84,7 @@ public bool TryEnqueue(LogEntry logEntry) $"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList>"); } - if (_ruleSelector.Select(_lastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null) + if (_ruleSelector.Select(LastKnownGoodFilterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null) { // buffering is not enabled for this log entry, // return false to indicate that the log entry should be logged normally. @@ -162,11 +163,11 @@ private void OnOptionsChanged(GlobalLogBufferingOptions? updatedOptions) { if (updatedOptions is null) { - _lastKnownGoodFilterRules = []; + LastKnownGoodFilterRules = []; } else { - _lastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category); + LastKnownGoodFilterRules = LogBufferingFilterRuleSelector.SelectByCategory(updatedOptions.Rules.ToArray(), _category); } _ruleSelector.InvalidateCache(); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs index bf06c546566..150f82b7414 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggingBuilderExtensions.cs @@ -38,7 +38,10 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, ICon _ = builder .Services.AddOptionsWithValidateOnStart() .Services.AddOptionsWithValidateOnStart() - .Services.AddSingleton>(new GlobalLogBufferingConfigureOptions(configuration)); + .Services.AddSingleton>( + new GlobalLogBufferingConfigureOptions(configuration)) + .AddSingleton>( + new ConfigurationChangeTokenSource(configuration)); return builder.AddGlobalBufferManager(); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs index 8cc25fe3a7c..449b12580fb 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferManager.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalLogBufferManager : GlobalLogBuffer { - private readonly ConcurrentDictionary _buffers = []; + internal readonly ConcurrentDictionary Buffers = []; private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; private readonly LogBufferingFilterRuleSelector _ruleSelector; @@ -35,7 +35,7 @@ internal GlobalLogBufferManager( public override void Flush() { - foreach (GlobalBuffer buffer in _buffers.Values) + foreach (GlobalBuffer buffer in Buffers.Values) { buffer.Flush(); } @@ -44,7 +44,7 @@ public override void Flush() public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry) { string category = logEntry.Category; - GlobalBuffer buffer = _buffers.GetOrAdd(category, _ => new GlobalBuffer( + GlobalBuffer buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer( bufferedLogger, category, _ruleSelector, diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs index 31ea8b45d4a..5bad831ab68 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/PerIncomingRequestLoggingBuilderExtensionsTests.cs @@ -3,15 +3,23 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.Buffering; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Buffering; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Test; using Microsoft.Extensions.Options; using Xunit; using PerRequestLogBuffer = Microsoft.Extensions.Diagnostics.Buffering.PerRequestLogBuffer; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; public class PerIncomingRequestLoggingBuilderExtensionsTests { @@ -82,5 +90,72 @@ public void WhenConfigurationActionProvided_RegistersInDI() Assert.NotNull(options.CurrentValue); Assert.Equivalent(expectedData, options.CurrentValue.Rules); } + + [Fact] + public async Task WhenConfigUpdated_PicksUpConfigChanges() + { + List initialData = + [ + new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"), + new(logLevel : LogLevel.Information), + ]; + List updatedData = + [ + new(logLevel: LogLevel.Information), + ]; + string jsonConfig = + @" +{ + ""PerIncomingRequestLogBuffering"": { + ""Rules"": [ + { + ""CategoryName"": ""Program.MyLogger"", + ""LogLevel"": ""Information"", + ""EventId"": 1, + ""EventName"": ""number one"", + }, + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + + using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig); + using IHost host = await FakeHost.CreateBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(x => x.AddRouting()) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddPerIncomingRequestBuffer(config)) + .Configure(app => app.UseRouting())) + .StartAsync(); + + IOptionsMonitor? options = host.Services.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(initialData, options.CurrentValue.Rules); + + jsonConfig = +@" +{ + ""PerIncomingRequestLogBuffering"": { + ""Rules"": [ + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + config.Reload(); + + var bufferManager = host.Services.GetRequiredService() as PerRequestLogBufferManager; + Assert.NotNull(bufferManager); + Assert.Equivalent(updatedData, bufferManager.Options.CurrentValue.Rules, strict: true); + + await host.StopAsync(); + } } #endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs new file mode 100644 index 00000000000..832c120873a --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/TestConfiguration.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; + +namespace Microsoft.Extensions.Logging.Test; + +internal class TestConfiguration : JsonConfigurationProvider +{ + private Func _json; + + public TestConfiguration(JsonConfigurationSource source, Func json) + : base(source) + { + _json = json; + } + + public static ConfigurationRoot Create(Func getJson) + { + var provider = new TestConfiguration(new JsonConfigurationSource { Optional = true }, getJson); + return new ConfigurationRoot(new List { provider }); + } + + public override void Load() + { + var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + writer.Write(_json()); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + Load(stream); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index a267c708f50..7a1899f8eeb 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Test; using Microsoft.Extensions.Options; using Xunit; @@ -68,5 +69,79 @@ public void WithConfiguration_RegistersInDI() Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.AutoFlushDuration); // value comes from default Assert.Equivalent(expectedData, options.CurrentValue.Rules); } + + [Fact] + public void WhenConfigUpdated_PicksUpConfigChanges() + { + List initialData = + [ + new(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"), + new(logLevel : LogLevel.Information), + ]; + List updatedData = + [ + new(logLevel: LogLevel.Information), + ]; + string jsonConfig = + @" +{ + ""GlobalLogBuffering"": { + ""Rules"": [ + { + ""CategoryName"": ""Program.MyLogger"", + ""LogLevel"": ""Information"", + ""EventId"": 1, + ""EventName"": ""number one"", + }, + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + + using ConfigurationRoot config = TestConfiguration.Create(() => jsonConfig); + + using ExtendedLoggerTests.Provider provider = new ExtendedLoggerTests.Provider(); + using ILoggerFactory factory = Utils.CreateLoggerFactory( + builder => + { + builder.AddProvider(provider); + builder.AddGlobalBuffer(config); + }); + ILogger logger = factory.CreateLogger("Program.MyLogger"); + Utils.DisposingLoggerFactory dlf = (Utils.DisposingLoggerFactory)factory; + var bufferManager = dlf.ServiceProvider.GetRequiredService() as GlobalLogBufferManager; + + IOptionsMonitor? options = dlf.ServiceProvider.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(initialData, options.CurrentValue.Rules); + + // this is just to trigger creating an internal buffer: + logger.LogInformation(new EventId(1, "number one"), null); + + jsonConfig = +@" +{ + ""GlobalLogBuffering"": { + ""Rules"": [ + { + ""LogLevel"": ""Information"", + }, + ] + } +} +"; + config.Reload(); + + Assert.NotNull(bufferManager); + Assert.NotEmpty(bufferManager.Buffers); + foreach (GlobalBuffer buffer in bufferManager.Buffers.Values) + { + Assert.Equivalent(updatedData, buffer.LastKnownGoodFilterRules, strict: true); + } + } } #endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index b31aabb0083..f91cbcabce7 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -1114,7 +1114,7 @@ private enum ThrowExceptionAt IsEnabled } - private sealed class Provider : ILoggerProvider + internal sealed class Provider : ILoggerProvider { public FakeLogger? Logger { get; private set; }