From 988c7094985d9a4ed8f9cb9dba67c958a5aa4819 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:17:35 +0100 Subject: [PATCH 01/15] Buffering --- .../Buffering/HttpRequestBuffer.cs | 97 +++++++++++++ .../HttpRequestBufferConfigureOptions.cs | 43 ++++++ ...ttpRequestBufferLoggerBuilderExtensions.cs | 97 +++++++++++++ .../Buffering/HttpRequestBufferOptions.cs | 47 +++++++ .../Buffering/HttpRequestBufferProvider.cs | 30 ++++ .../Buffering/HttpRequestBufferedLogRecord.cs | 38 +++++ ...t.AspNetCore.Diagnostics.Middleware.csproj | 1 - .../Logging/FakeLogger.cs | 31 +++++ ...crosoft.Extensions.Http.Diagnostics.csproj | 2 - .../Buffering/BufferFilterRule.cs | 48 +++++++ .../Buffering/GlobalBuffer.cs | 129 +++++++++++++++++ .../Buffering/GlobalBufferConfigureOptions.cs | 43 ++++++ .../GlobalBufferLoggerBuilderExtensions.cs | 99 +++++++++++++ .../Buffering/GlobalBufferOptions.cs | 46 ++++++ .../Buffering/GlobalBufferProvider.cs | 18 +++ .../Buffering/GlobalBufferedLogRecord.cs | 38 +++++ .../Buffering/ILoggingBuffer.cs | 38 +++++ .../Buffering/ILoggingBufferProvider.cs | 21 +++ .../ILoggerFilterRule.cs | 27 ++++ .../LoggerFilterRuleSelector.cs | 131 ++++++++++++++++++ .../Logging/ExtendedLogger.cs | 34 ++++- .../Logging/ExtendedLoggerFactory.cs | 16 +++ .../Logging/LoggerConfig.cs | 11 ++ .../Microsoft.Extensions.Telemetry.csproj | 4 +- ...questBufferLoggerBuilderExtensionsTests.cs | 81 +++++++++++ .../Logging/AcceptanceTests.cs | 60 ++++++++ .../Logging/HeaderNormalizerTests.cs | 2 + .../appsettings.json | 12 ++ ...lobalBufferLoggerBuilderExtensionsTests.cs | 67 +++++++++ .../Logging/ExtendedLoggerTests.cs | 55 ++++++++ ...icrosoft.Extensions.Telemetry.Tests.csproj | 1 + .../appsettings.json | 12 ++ 32 files changed, 1374 insertions(+), 5 deletions(-) create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs new file mode 100644 index 00000000000..1d25077f8f0 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Extensions.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Diagnostics.Logging; + +internal sealed class HttpRequestBuffer : ILoggingBuffer +{ + private readonly IOptionsMonitor _options; + private readonly ConcurrentDictionary> _buffers; + private readonly TimeProvider _timeProvider = TimeProvider.System; + private DateTimeOffset _lastFlushTimestamp; + + public HttpRequestBuffer(IOptionsMonitor options) + { + _options = options; + _buffers = new ConcurrentDictionary>(); + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + } + + internal HttpRequestBuffer(IOptionsMonitor options, TimeProvider timeProvider) + : this(options) + { + _timeProvider = timeProvider; + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + } + + public bool TryEnqueue( + IBufferedLogger logger, + LogLevel logLevel, + string category, + EventId eventId, + IReadOnlyList> joiner, + Exception? exception, + string formatter) + { + if (!IsEnabled(category, logLevel, eventId)) + { + return false; + } + + var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter); + var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue()); + + // probably don't need to limit buffer capacity? + // because buffer is disposed when the respective HttpContext is disposed + // don't expect it to grow so much to cause a problem? + if (queue.Count >= _options.CurrentValue.PerRequestCapacity) + { + _ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _); + } + + queue.Enqueue(record); + + return true; + } + + public void Flush() + { + foreach (var (logger, queue) in _buffers) + { + var result = new List(); + while (!queue.IsEmpty) + { + if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item)) + { + result.Add(item); + } + } + + logger.LogRecords(result); + } + + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + } + + public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) + { + if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration) + { + return false; + } + + LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); + + return rule is not null; + } +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs new file mode 100644 index 00000000000..a30a489b6bb --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Diagnostics.Logging; + +internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions +{ + private const string BufferingKey = "Buffering"; + private readonly IConfiguration _configuration; + + public HttpRequestBufferConfigureOptions(IConfiguration configuration) + { + _configuration = configuration; + } + + public void Configure(HttpRequestBufferOptions options) + { + if (_configuration == null) + { + return; + } + + var section = _configuration.GetSection(BufferingKey); + if (!section.Exists()) + { + return; + } + + var parsedOptions = section.Get(); + if (parsedOptions is null) + { + return; + } + + options.Rules.AddRange(parsedOptions.Rules); + } +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs new file mode 100644 index 00000000000..50faa1522e0 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Diagnostics.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Logging; + +/// +/// Lets you register log buffers in a dependency injection container. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public static class HttpRequestBufferLoggerBuilderExtensions +{ + /// + /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in + /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. + /// + /// The . + /// The to add. + /// The value of . + /// is . + public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configuration); + + return builder + .AddHttpRequestBufferConfiguration(configuration) + .AddHttpRequestBufferProvider(); + } + + /// + /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in + /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. + /// + /// The . + /// The log level (and below) to apply the buffer to. + /// The buffer configuration options. + /// The value of . + /// is . + public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) + { + _ = Throw.IfNull(builder); + + _ = builder.Services + .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) + .Configure(configure ?? new Action(_ => { })); + + return builder.AddHttpRequestBufferProvider(); + } + + /// + /// Adds HTTP request buffer provider to the logging infrastructure. + /// + /// The . + /// The so that additional calls can be chained. + /// is . + public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder) + { + _ = Throw.IfNull(builder); + + builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(sp => sp.GetRequiredService()); + builder.Services.TryAddSingleton(); + builder.Services.TryAddActivatedSingleton(); + + return builder.AddGlobalBufferProvider(); + } + + /// + /// Configures from an instance of . + /// + /// The . + /// The to add. + /// The value of . + /// is . + internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddSingleton>(new HttpRequestBufferConfigureOptions(configuration)); + + return builder; + } +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs new file mode 100644 index 00000000000..cf04cd2da8c --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.AspNetCore.Diagnostics.Logging; + +/// +/// The options for LoggerBuffer. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public class HttpRequestBufferOptions +{ + /// + /// Gets or sets the time to suspend the buffer after flushing. + /// + /// + /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, + /// so the buffering will be suspended for the time. + /// + public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the size of the buffer for a request. + /// + public int PerRequestCapacity { get; set; } = 1_000; + + /// + /// Gets or sets the size of the global buffer which applies to non-request logs only. + /// + public int GlobalCapacity { get; set; } = 1_000_000; + +#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() +#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern + /// + /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. + /// + public List Rules { get; set; } = []; +#pragma warning restore CA2227 // Collection properties should be read only +#pragma warning restore CA1002 // Do not expose generic lists +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs new file mode 100644 index 00000000000..3ed5c08b1d7 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Diagnostics.Logging; + +internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider +{ + private readonly GlobalBufferProvider _globalBufferProvider; + private readonly IHttpContextAccessor _accessor; + private readonly ConcurrentDictionary _requestBuffers = new(); + + public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor) + { + _globalBufferProvider = globalBufferProvider; + _accessor = accessor; + } + + public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null + ? _globalBufferProvider.CurrentBuffer + : _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService()); + + // TO DO: Dispose request buffer when the respective HttpContext is disposed +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs new file mode 100644 index 00000000000..8983e97843b --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Diagnostics.Logging; + +internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord +{ + public HttpRequestBufferedLogRecord( + LogLevel logLevel, + EventId eventId, + IReadOnlyList> state, + Exception? exception, + string? formatter) + { + LogLevel = logLevel; + EventId = eventId; + Attributes = state; + Exception = exception?.ToString(); // wtf?? + FormattedMessage = formatter; + } + + public override IReadOnlyList> Attributes { get; } + public override string? FormattedMessage { get; } + public override string? Exception { get; } + + public override DateTimeOffset Timestamp { get; } + + public override LogLevel LogLevel { get; } + + public override EventId EventId { get; } +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj index 5484aa8f5af..238b4364d5d 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj @@ -17,7 +17,6 @@ false true false - true true diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs index 56973c9e78d..9ba5586dba6 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs @@ -6,6 +6,12 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +#if NET9_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.DiagnosticIds; + +#endif using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging.Testing; @@ -17,7 +23,11 @@ namespace Microsoft.Extensions.Logging.Testing; /// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it /// to validate that your code is logging what it should. /// +#if NET9_0_OR_GREATER +public class FakeLogger : ILogger, IBufferedLogger +#else public class FakeLogger : ILogger +#endif { private readonly ConcurrentDictionary _disabledLevels = new(); // used as a set, the value is ignored @@ -105,6 +115,27 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// public string? Category { get; } +#if NET9_0_OR_GREATER + /// + [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] + public void LogRecords(IEnumerable records) + { + _ = Throw.IfNull(records); + + var l = new List(); + ScopeProvider.ForEachScope((scopeState, list) => list.Add(scopeState), l); + + foreach (var rec in records) + { +#pragma warning disable CA2201 // TO DO: Remove suppression and implement propert Exception deserialization + var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), new Exception(rec.Exception), rec.FormattedMessage ?? string.Empty, + l.ToArray(), Category, !_disabledLevels.ContainsKey(rec.LogLevel), rec.Timestamp); +#pragma warning restore CA2201 + Collector.AddRecord(record); + } + } +#endif + internal IExternalScopeProvider ScopeProvider { get; set; } = new LoggerExternalScopeProvider(); private static object? ConsumeTState(object? state) diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj index f6c98baefce..e6914c42dff 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj @@ -14,8 +14,6 @@ true false true - true - true false true false diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs new file mode 100644 index 00000000000..fd8a070fec5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// Defines a rule used to filter log messages for purposes of futher buffering. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public class BufferFilterRule : ILoggerFilterRule +{ + /// + /// Initializes a new instance of the class. + /// + public BufferFilterRule() + : this(null, null, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The category name to use in this filter rule. + /// The to use in this filter rule. + /// The to use in this filter rule. + public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId) + { + Category = categoryName; + LogLevel = logLevel; + EventId = eventId; + } + + /// + public string? Category { get; set; } + + /// + public LogLevel? LogLevel { get; set; } + + /// + public int? EventId { get; set; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs new file mode 100644 index 00000000000..ffba82f84c6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging; + +internal sealed class GlobalBuffer : BackgroundService, ILoggingBuffer +{ + private readonly IOptionsMonitor _options; + private readonly ConcurrentDictionary> _buffers; + private readonly TimeProvider _timeProvider = TimeProvider.System; + private DateTimeOffset _lastFlushTimestamp; + + public GlobalBuffer(IOptionsMonitor options) + { + _options = options; + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + _buffers = new ConcurrentDictionary>(); + } + + internal GlobalBuffer(IOptionsMonitor options, TimeProvider timeProvider) + : this(options) + { + _timeProvider = timeProvider; + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + } + + public bool TryEnqueue( + IBufferedLogger logger, + LogLevel logLevel, + string category, + EventId eventId, + IReadOnlyList> joiner, + Exception? exception, string formatter) + { + if (!IsEnabled(category, logLevel, eventId)) + { + return false; + } + + var record = new GlobalBufferedLogRecord(logLevel, eventId, joiner, exception, formatter); + var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue()); + if (queue.Count >= _options.CurrentValue.Capacity) + { + _ = queue.TryDequeue(out GlobalBufferedLogRecord? _); + } + + queue.Enqueue(record); + + return true; + } + + public void Flush() + { + foreach (var (logger, queue) in _buffers) + { + var result = new List(); + while (!queue.IsEmpty) + { + if (queue.TryDequeue(out GlobalBufferedLogRecord? item)) + { + result.Add(item); + } + } + + logger.LogRecords(result); + } + + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + } + + internal void RemoveExpiredItems() + { + foreach (var (logger, queue) in _buffers) + { + while (!queue.IsEmpty) + { + if (queue.TryPeek(out GlobalBufferedLogRecord? item)) + { + if (_timeProvider.GetUtcNow() - item.Timestamp > _options.CurrentValue.Duration) + { + _ = queue.TryDequeue(out _); + } + else + { + break; + } + } + else + { + break; + } + } + } + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await _timeProvider.Delay(_options.CurrentValue.Duration, cancellationToken).ConfigureAwait(false); + RemoveExpiredItems(); + } + } + + private bool IsEnabled(string category, LogLevel logLevel, EventId eventId) + { + if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration) + { + return false; + } + + LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); + + return rule is not null; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs new file mode 100644 index 00000000000..8b6bd724873 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging; + +internal sealed class GlobalBufferConfigureOptions : IConfigureOptions +{ + private const string BufferingKey = "Buffering"; + private readonly IConfiguration _configuration; + + public GlobalBufferConfigureOptions(IConfiguration configuration) + { + _configuration = configuration; + } + + public void Configure(GlobalBufferOptions options) + { + if (_configuration == null) + { + return; + } + + var section = _configuration.GetSection(BufferingKey); + if (!section.Exists()) + { + return; + } + + var parsedOptions = section.Get(); + if (parsedOptions is null) + { + return; + } + + options.Rules.AddRange(parsedOptions.Rules); + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs new file mode 100644 index 00000000000..3789dc9c0f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Logging; + +/// +/// Lets you register log buffers in a dependency injection container. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public static class GlobalBufferLoggerBuilderExtensions +{ + /// + /// Adds global buffer to the logging infrastructure. + /// Matched logs will be buffered and can optionally be flushed and emitted./>. + /// + /// The . + /// The to add. + /// The value of . + /// is . + public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, IConfiguration configuration) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configuration); + + return builder + .AddGlobalBufferConfiguration(configuration) + .AddGlobalBufferProvider(); + } + + /// + /// Adds global buffer to the logging infrastructure. + /// Matched logs will be buffered and can optionally be flushed and emitted./>. + /// + /// The . + /// The log level (and below) to apply the buffer to. + /// Configure buffer options. + /// The value of . + /// is . + public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) + { + _ = Throw.IfNull(builder); + + _ = builder.Services + .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) + .Configure(configure ?? new Action(_ => { })); + + return builder.AddGlobalBufferProvider(); + } + + /// + /// Adds global logging buffer provider. + /// + /// The . + /// The so that additional calls can be chained. + public static ILoggingBuilder AddGlobalBufferProvider(this ILoggingBuilder builder) + { + _ = Throw.IfNull(builder); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); + + return builder; + } + + /// + /// Configures from an instance of . + /// + /// The . + /// The to add. + /// The value of . + /// is . + internal static ILoggingBuilder AddGlobalBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddSingleton>(new GlobalBufferConfigureOptions(configuration)); + + return builder; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs new file mode 100644 index 00000000000..7c15a8f3b9b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// The options for LoggerBuffer. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public class GlobalBufferOptions +{ + /// + /// Gets or sets the time to suspend the buffer after flushing. + /// + /// + /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, + /// so the buffering will be suspended for the time. + /// + public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the duration to keep logs in the buffer. + /// + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the size of the buffer. + /// + public int Capacity { get; set; } = 1_000_000; + +#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() +#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern + /// + /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. + /// + public List Rules { get; set; } = []; +#pragma warning restore CA2227 // Collection properties should be read only +#pragma warning restore CA1002 // Do not expose generic lists +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs new file mode 100644 index 00000000000..5bfedc32516 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +namespace Microsoft.Extensions.Logging; + +internal sealed class GlobalBufferProvider : ILoggingBufferProvider +{ + private readonly GlobalBuffer _buffer; + + public GlobalBufferProvider(GlobalBuffer buffer) + { + _buffer = buffer; + } + + public ILoggingBuffer CurrentBuffer => _buffer; +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs new file mode 100644 index 00000000000..e72fc3bed2f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.Logging; + +internal sealed class GlobalBufferedLogRecord : BufferedLogRecord +{ + public GlobalBufferedLogRecord( + LogLevel logLevel, + EventId eventId, + IReadOnlyList> state, + Exception? exception, + string? formatter) + { + LogLevel = logLevel; + EventId = eventId; + Attributes = state; + Exception = exception?.ToString(); // wtf?? + FormattedMessage = formatter; + } + + public override IReadOnlyList> Attributes { get; } + public override string? FormattedMessage { get; } + public override string? Exception { get; } + + public override DateTimeOffset Timestamp { get; } + + public override LogLevel LogLevel { get; } + + public override EventId EventId { get; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs new file mode 100644 index 00000000000..baf15325156 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// Interface for a logging buffer. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface ILoggingBuffer +{ + /// + /// Flushes the buffer and emits all buffered logs. + /// + void Flush(); + + /// + /// Enqueues a log record. + /// + /// true or false. + bool TryEnqueue( + IBufferedLogger logger, + LogLevel logLevel, + string category, + EventId eventId, + IReadOnlyList> joiner, + Exception? exception, + string formatter); +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs new file mode 100644 index 00000000000..52742297d0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// Interface providing access to the current logging buffer. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface ILoggingBufferProvider +{ + /// + /// Gets current logging buffer. + /// + public ILoggingBuffer CurrentBuffer { get; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs new file mode 100644 index 00000000000..2d7e40bc321 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics; + +/// +/// Represents a rule used for filtering log messages for purposes of log sampling and buffering. +/// +internal interface ILoggerFilterRule +{ + /// + /// Gets the logger category this rule applies to. + /// + public string? Category { get; } + + /// + /// Gets the maximum of messages. + /// + public LogLevel? LogLevel { get; } + + /// + /// Gets the maximum of messages where this rule applies to. + /// + public int? EventId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs new file mode 100644 index 00000000000..6864c38f6fb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1307 // Specify StringComparison for clarity +#pragma warning disable S1659 // Multiple variables should not be declared on the same line +#pragma warning disable S2302 // "nameof" should be used + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics +{ + internal static class LoggerFilterRuleSelector + { + public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, + out T? bestRule) + where T : class, ILoggerFilterRule + { + bestRule = null; + + // TO DO: update the comment and logic + // Filter rule selection: + // 1. Select rules with longest matching categories + // 2. If there is nothing matched by category take all rules without category + // 3. If there is only one rule use it + // 4. If there are multiple rules use last + + T? current = null; + foreach (T rule in rules) + { + if (IsBetter(rule, current, category, logLevel, eventId)) + { + current = rule; + } + } + + if (current != null) + { + bestRule = current; + } + } + + private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId) + where T : class, ILoggerFilterRule + { + // Skip rules with inapplicable log level + if (rule.LogLevel != null && rule.LogLevel < logLevel) + { + return false; + } + + // Skip rules with inapplicable event id + if (rule.EventId != null && rule.EventId != eventId) + { + return false; + } + + // Skip rules with inapplicable category + string? categoryName = rule.Category; + if (categoryName != null) + { + const char WildcardChar = '*'; + + int wildcardIndex = categoryName.IndexOf(WildcardChar); + if (wildcardIndex != -1 && + categoryName.IndexOf(WildcardChar, wildcardIndex + 1) != -1) + { + throw new InvalidOperationException("Only one wildcard character is allowed in category name."); + } + + ReadOnlySpan prefix, suffix; + if (wildcardIndex == -1) + { + prefix = categoryName.AsSpan(); + suffix = default; + } + else + { + prefix = categoryName.AsSpan(0, wildcardIndex); + suffix = categoryName.AsSpan(wildcardIndex + 1); + } + + if (!category.AsSpan().StartsWith(prefix, StringComparison.OrdinalIgnoreCase) || + !category.AsSpan().EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + // Decide whose category is better - rule vs current + if (current?.Category != null) + { + if (rule.Category == null) + { + return false; + } + + if (current.Category.Length > rule.Category.Length) + { + return false; + } + } + + // Decide whose log level is better - rule vs current + if (current?.LogLevel != null) + { + if (rule.LogLevel == null) + { + return false; + } + + if (current.LogLevel < rule.LogLevel) + { + return false; + } + } + + // Decide whose event id is better - rule vs current + if (rule.EventId is null) + { + if (current?.EventId != null) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index 693637781c8..b39803404dc 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Extensions.Logging; +#if NET9_0_OR_GREATER +using Microsoft.Extensions.Logging.Abstractions; +#endif using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Logging; @@ -266,6 +268,20 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { +#if NET9_0_OR_GREATER + if (loggerInfo.Logger is IBufferedLogger bufferedLogger) + { + if (config.BufferProvider is not null && + config.BufferProvider.CurrentBuffer.TryEnqueue(bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, joiner.Formatter!(joiner.State, exception))) + { + // The record was buffered, so we skip logging it for now. + // When a caller needs to flush the buffer and calls ILoggerBuffer.Flush(), + // the buffer will internally call IBufferedLogger.LogRecords to emit log records. + continue; + } + } +#endif + try { loggerInfo.LoggerLog(logLevel, eventId, joiner, exception, static (s, e) => @@ -350,6 +366,22 @@ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { +#if NET9_0_OR_GREATER + if (loggerInfo.Logger is IBufferedLogger bufferedLogger) + { + if (config.BufferProvider is not null && + config.BufferProvider.CurrentBuffer.TryEnqueue( + bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, + ((Func)joiner.Formatter)((TState)joiner.State!, exception))) + { + // The record was buffered, so we skip logging it for now. + // When a caller needs to flush the buffer and calls ILoggerBuffer.Flush(), + // the buffer will internally call IBufferedLogger.LogRecords to emit log records. + continue; + } + } +#endif + try { loggerInfo.Logger.Log(logLevel, eventId, joiner, exception, static (s, e) => diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 9a98b8446f4..1ea4f0dc0b3 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -23,6 +23,9 @@ internal sealed class ExtendedLoggerFactory : ILoggerFactory private readonly IDisposable? _enrichmentOptionsChangeTokenRegistration; private readonly IDisposable? _redactionOptionsChangeTokenRegistration; private readonly Action[] _enrichers; +#if NET9_0_OR_GREATER + private readonly ILoggingBufferProvider? _bufferProvider; +#endif private readonly KeyValuePair[] _staticTags; private readonly Func _redactorProvider; private volatile bool _disposed; @@ -39,10 +42,18 @@ public ExtendedLoggerFactory( IExternalScopeProvider? scopeProvider = null, IOptionsMonitor? enrichmentOptions = null, IOptionsMonitor? redactionOptions = null, +#if NET9_0_OR_GREATER + IRedactorProvider? redactorProvider = null, + ILoggingBufferProvider? bufferProvider = null) +#else IRedactorProvider? redactorProvider = null) +#endif #pragma warning restore S107 // Methods should not have too many parameters { _scopeProvider = scopeProvider; +#if NET9_0_OR_GREATER + _bufferProvider = bufferProvider; +#endif _factoryOptions = factoryOptions == null || factoryOptions.Value == null ? new LoggerFactoryOptions() : factoryOptions.Value; @@ -289,7 +300,12 @@ private LoggerConfig ComputeConfig(LoggerEnrichmentOptions? enrichmentOptions, L enrichmentOptions.IncludeExceptionMessage, enrichmentOptions.MaxStackTraceLength, _redactorProvider, +#if NET9_0_OR_GREATER + redactionOptions.ApplyDiscriminator, + _bufferProvider); +#else redactionOptions.ApplyDiscriminator); +#endif } private void UpdateEnrichmentOptions(LoggerEnrichmentOptions enrichmentOptions) => Config = ComputeConfig(enrichmentOptions, null); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs index 34716ca4e38..fba961defd5 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs @@ -20,7 +20,12 @@ public LoggerConfig( bool includeExceptionMessage, int maxStackTraceLength, Func getRedactor, +#if NET9_0_OR_GREATER + bool addRedactionDiscriminator, + ILoggingBufferProvider? bufferProvider) +#else bool addRedactionDiscriminator) +#endif { #pragma warning restore S107 // Methods should not have too many parameters StaticTags = staticTags; @@ -31,6 +36,9 @@ public LoggerConfig( IncludeExceptionMessage = includeExceptionMessage; GetRedactor = getRedactor; AddRedactionDiscriminator = addRedactionDiscriminator; +#if NET9_0_OR_GREATER + BufferProvider = bufferProvider; +#endif } public KeyValuePair[] StaticTags { get; } @@ -41,4 +49,7 @@ public LoggerConfig( public int MaxStackTraceLength { get; } public Func GetRedactor { get; } public bool AddRedactionDiscriminator { get; } +#if NET9_0_OR_GREATER + public ILoggingBufferProvider? BufferProvider { get; } +#endif } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index 3d39591e547..f303698ed82 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -7,6 +7,8 @@ true + true + true true true true @@ -31,7 +33,7 @@ - + diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs new file mode 100644 index 00000000000..e389d65c57b --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; + +public class HttpRequestBufferLoggerBuilderExtensionsTests +{ + [Fact] + public void AddHttpRequestBuffer_RegistersInDI() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.AddHttpRequestBuffer(LogLevel.Warning); + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var buffer = serviceProvider.GetService(); + + Assert.NotNull(buffer); + Assert.IsAssignableFrom(buffer); + } + + [Fact] + public void AddHttpRequestBufferProvider_RegistersInDI() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.AddHttpRequestBufferProvider(); + }); + var serviceProvider = serviceCollection.BuildServiceProvider(); + var bufferProvider = serviceProvider.GetService(); + Assert.NotNull(bufferProvider); + Assert.IsAssignableFrom(bufferProvider); + } + + [Fact] + public void WhenArgumentNull_Throws() + { + var builder = null as ILoggingBuilder; + var configuration = null as IConfiguration; + + Assert.Throws(() => builder!.AddHttpRequestBuffer(LogLevel.Warning)); + Assert.Throws(() => builder!.AddHttpRequestBuffer(configuration!)); + Assert.Throws(() => builder!.AddHttpRequestBufferProvider()); + } + + [Fact] + public void AddHttpRequestBufferConfiguration_RegistersInDI() + { + List expectedData = + [ + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1), + new BufferFilterRule(null, LogLevel.Information, null), + ]; + ConfigurationBuilder configBuilder = new ConfigurationBuilder(); + configBuilder.AddJsonFile("appsettings.json"); + IConfigurationRoot configuration = configBuilder.Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.AddHttpRequestBufferConfiguration(configuration); + }); + var serviceProvider = serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(expectedData, options.CurrentValue.Rules); + } +} +#endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index 5794709560f..6856b9751a8 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -4,6 +4,7 @@ #if NET8_0_OR_GREATER using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Http; @@ -24,6 +25,9 @@ using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +#if NET9_0_OR_GREATER +using Microsoft.Extensions.Options; +#endif using Microsoft.Extensions.Time.Testing; using Microsoft.Net.Http.Headers; using Microsoft.Shared.Text; @@ -714,6 +718,48 @@ await RunAsync( }); } +#if NET9_0_OR_GREATER + [Fact] + public async Task HttpRequestBuffering() + { + var clock = new FakeTimeProvider(TimeProvider.System.GetUtcNow()); + + HttpRequestBufferOptions options = new() + { + SuspendAfterFlushDuration = TimeSpan.FromSeconds(0), + Rules = new List + { + new(null, LogLevel.Information, null), + } + }; + var buffer = new HttpRequestBuffer(new StaticOptionsMonitor(options), clock); + + await RunAsync( + LogLevel.Information, + services => services.AddLogging(builder => + { + builder.Services.AddScoped(sp => buffer); + builder.Services.AddScoped(sp => buffer); + builder.AddHttpRequestBuffer(LogLevel.Information); + }), + async (logCollector, client, sp) => + { + using var response = await client.GetAsync("/home").ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + + Assert.Equal(0, logCollector.Count); + + using var scope = sp.CreateScope(); + var buffer = scope.ServiceProvider.GetRequiredService(); + buffer.Flush(); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + Assert.Equal(1, logCollector.Count); + }); + } +#endif + [Fact] public async Task HttpLogging_LogRecordIsNotCreated_If_Disabled() { @@ -757,5 +803,19 @@ private class ThrowingEnricher : IHttpLogEnricher { public void Enrich(IEnrichmentTagCollector collector, HttpContext httpContext) => throw new InvalidOperationException(); } + +#if NET9_0_OR_GREATER + private sealed class StaticOptionsMonitor : IOptionsMonitor + { + public StaticOptionsMonitor(T currentValue) + { + CurrentValue = currentValue; + } + + public IDisposable? OnChange(Action listener) => null; + public T Get(string? name) => CurrentValue; + public T CurrentValue { get; } + } +#endif } #endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs index 481a186a08d..82a063cc55c 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Compliance.Classification; using Xunit; +namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; + public class HeaderNormalizerTests { [Fact] diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json index c1503ee98da..79676b0e1e9 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json @@ -17,5 +17,17 @@ "userId": "EUII", "userContent": "CustomerContent" } + }, + "Buffering": { + "Rules": [ + { + "Category": "Program.MyLogger", + "LogLevel": "Information", + "EventId": 1 + }, + { + "LogLevel": "Information" + } + ] } } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs new file mode 100644 index 00000000000..be6b42197d3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test; + +public class GlobalBufferLoggerBuilderExtensionsTests +{ + [Fact] + public void AddGlobalBuffer_RegistersInDI() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.AddGlobalBuffer(LogLevel.Warning); + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var buffer = serviceProvider.GetService(); + + Assert.NotNull(buffer); + Assert.IsAssignableFrom(buffer); + } + + [Fact] + public void WhenArgumentNull_Throws() + { + var builder = null as ILoggingBuilder; + var configuration = null as IConfiguration; + + Assert.Throws(() => builder!.AddGlobalBuffer(LogLevel.Warning)); + Assert.Throws(() => builder!.AddGlobalBuffer(configuration!)); + Assert.Throws(() => builder!.AddGlobalBufferProvider()); + } + + [Fact] + public void AddGlobalBufferConfiguration_RegistersInDI() + { + List expectedData = + [ + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1), + new BufferFilterRule(null, LogLevel.Information, null), + ]; + ConfigurationBuilder configBuilder = new ConfigurationBuilder(); + configBuilder.AddJsonFile("appsettings.json"); + IConfigurationRoot configuration = configBuilder.Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => + { + builder.AddGlobalBufferConfiguration(configuration); + }); + var serviceProvider = serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options.CurrentValue); + Assert.Equivalent(expectedData, options.CurrentValue.Rules); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index 69d1d0e8f57..7410a2e0954 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -9,6 +9,9 @@ using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; +#if NET9_0_OR_GREATER +using Microsoft.Extensions.Time.Testing; +#endif using Moq; using Xunit; @@ -119,6 +122,58 @@ public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment } } +#if NET9_0_OR_GREATER + [Fact] + public static void GlobalBuffering() + { + const string Category = "B1"; + var clock = new FakeTimeProvider(TimeProvider.System.GetUtcNow()); + + GlobalBufferOptions options = new() + { + Duration = TimeSpan.FromSeconds(60), + SuspendAfterFlushDuration = TimeSpan.FromSeconds(0), + Rules = new List + { + new(null, LogLevel.Warning, null), + } + }; + using var buffer = new GlobalBuffer(new StaticOptionsMonitor(options), clock); + + using var provider = new Provider(); + using var factory = Utils.CreateLoggerFactory( + builder => + { + builder.AddProvider(provider); + builder.Services.AddSingleton(buffer); + builder.AddGlobalBufferProvider(); + }); + + var logger = factory.CreateLogger(Category); + logger.LogWarning("MSG0"); + logger.Log(LogLevel.Warning, new EventId(2, "ID2"), "some state", null, (_, _) => "MSG2"); + + // nothing is logged because the buffer is not flushed + Assert.Equal(0, provider.Logger!.Collector.Count); + + buffer.Flush(); + + // 2 log records emitted because the buffer was flushed + Assert.Equal(2, provider.Logger!.Collector.Count); + + logger.LogWarning("MSG0"); + logger.Log(LogLevel.Warning, new EventId(2, "ID2"), "some state", null, (_, _) => "MSG2"); + + clock.Advance(options.Duration); + + // forcefully clear buffer instead of async waiting for it to be done on its own which is inherently racy. + buffer.RemoveExpiredItems(); + + // still 2 because the buffer is cleared after Duration time elapses + Assert.Equal(2, provider.Logger!.Collector.Count); + } +#endif + [Theory] [CombinatorialData] public static void BagAndJoiner(bool objectVersion) diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj index 7273b05c6c7..686a095d822 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json index 16ea15c7ed8..9cbf9c7b85c 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -16,5 +16,17 @@ "MeterStateOverrides": { "": "Disabled" } + }, + "Buffering": { + "Rules": [ + { + "Category": "Program.MyLogger", + "LogLevel": "Information", + "EventId": 1 + }, + { + "LogLevel": "Information" + } + ] } } From 2f1a335562a48027df00c632abc627485a8f7cbf Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:39:51 +0100 Subject: [PATCH 02/15] Major update --- eng/MSBuild/Shared.props | 4 + .../Buffering/HttpRequestBuffer.cs | 97 ++++++---- ...ttpRequestBufferLoggerBuilderExtensions.cs | 28 ++- .../Buffering/HttpRequestBufferManager.cs | 87 +++++++++ .../Buffering/HttpRequestBufferOptions.cs | 13 +- .../Buffering/HttpRequestBufferProvider.cs | 30 --- .../Buffering/HttpRequestBufferedLogRecord.cs | 38 ---- .../Buffering/IHttpRequestBufferManager.cs} | 12 +- .../Logging/FakeLogger.cs | 12 +- ...soft.Extensions.Diagnostics.Testing.csproj | 1 + .../Buffering/BufferSink.cs | 94 ++++++++++ .../Buffering/GlobalBuffer.cs | 104 ++++------- .../GlobalBufferLoggerBuilderExtensions.cs | 18 +- .../Buffering/GlobalBufferManager.cs | 69 +++++++ .../Buffering/GlobalBufferOptions.cs | 4 +- .../Buffering/GlobalBufferProvider.cs | 18 -- .../Buffering/GlobalBufferedLogRecord.cs | 38 ---- .../Buffering/IBufferManager.cs | 46 +++++ .../Buffering/IBufferSink.cs | 25 +++ .../Buffering/ILoggingBuffer.cs | 42 +++-- .../Buffering/ISerializedLogRecord.cs | 46 +++++ .../Buffering/PooledLogRecord.cs | 76 ++++++++ .../Buffering/SerializedLogRecord.cs | 53 ++++++ .../Logging/ExtendedLogger.cs | 79 ++++++-- .../Logging/ExtendedLoggerFactory.cs | 14 +- .../Logging/LoggerConfig.cs | 9 +- .../Microsoft.Extensions.Telemetry.csproj | 1 + .../ExceptionConverter.cs | 171 ++++++++++++++++++ ...questBufferLoggerBuilderExtensionsTests.cs | 28 +-- .../Logging/AcceptanceTests.cs | 92 +++++----- .../Buffering/ExceptionConverterTests.cs | 89 +++++++++ ...lobalBufferLoggerBuilderExtensionsTests.cs | 8 +- .../Logging/ExtendedLoggerTests.cs | 42 +---- .../Logging/Utils.cs | 8 +- ...icrosoft.Extensions.Telemetry.Tests.csproj | 1 - 35 files changed, 1081 insertions(+), 416 deletions(-) create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs delete mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs delete mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs rename src/Libraries/{Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs => Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs} (53%) create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs create mode 100644 src/Shared/JsonExceptionConverter/ExceptionConverter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index a68b0e4298f..599fc2bde44 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -42,4 +42,8 @@ + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 1d25077f8f0..64591cd910b 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -4,94 +4,111 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.AspNetCore.Diagnostics.Logging; internal sealed class HttpRequestBuffer : ILoggingBuffer { private readonly IOptionsMonitor _options; - private readonly ConcurrentDictionary> _buffers; + private readonly IOptionsMonitor _globalOptions; + private readonly ConcurrentQueue _buffer; private readonly TimeProvider _timeProvider = TimeProvider.System; + private readonly IBufferSink _bufferSink; + private readonly object _bufferCapacityLocker = new(); + private DateTimeOffset _truncateAfter; private DateTimeOffset _lastFlushTimestamp; - public HttpRequestBuffer(IOptionsMonitor options) + public HttpRequestBuffer(IBufferSink bufferSink, + IOptionsMonitor options, + IOptionsMonitor globalOptions) { _options = options; - _buffers = new ConcurrentDictionary>(); - _lastFlushTimestamp = _timeProvider.GetUtcNow(); - } + _globalOptions = globalOptions; + _bufferSink = bufferSink; + _buffer = new ConcurrentQueue(); - internal HttpRequestBuffer(IOptionsMonitor options, TimeProvider timeProvider) - : this(options) - { - _timeProvider = timeProvider; - _lastFlushTimestamp = _timeProvider.GetUtcNow(); + _truncateAfter = _timeProvider.GetUtcNow(); } - public bool TryEnqueue( - IBufferedLogger logger, + [RequiresUnreferencedCode( + "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] + public bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - IReadOnlyList> joiner, + TState attributes, Exception? exception, - string formatter) + Func formatter) { if (!IsEnabled(category, logLevel, eventId)) { return false; } - var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter); - var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue()); - - // probably don't need to limit buffer capacity? - // because buffer is disposed when the respective HttpContext is disposed - // don't expect it to grow so much to cause a problem? - if (queue.Count >= _options.CurrentValue.PerRequestCapacity) + switch (attributes) { - _ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _); + case ModernTagJoiner modernTagJoiner: + _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, + ((Func)(object)formatter)(modernTagJoiner, exception))); + break; + case LegacyTagJoiner legacyTagJoiner: + _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, + ((Func)(object)formatter)(legacyTagJoiner, exception))); + break; + default: + Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}"); + break; } - queue.Enqueue(record); + var now = _timeProvider.GetUtcNow(); + lock (_bufferCapacityLocker) + { + if (now >= _truncateAfter) + { + _truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration); + TruncateOverlimit(); + } + } return true; } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] public void Flush() { - foreach (var (logger, queue) in _buffers) - { - var result = new List(); - while (!queue.IsEmpty) - { - if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item)) - { - result.Add(item); - } - } - - logger.LogRecords(result); - } + var result = _buffer.ToArray(); + _buffer.Clear(); _lastFlushTimestamp = _timeProvider.GetUtcNow(); + + _bufferSink.LogRecords(result); } public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) { - if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration) + if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration) { return false; } - LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); + LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); return rule is not null; } + + public void TruncateOverlimit() + { + // Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments. + while (_buffer.Count > _options.CurrentValue.PerRequestCapacity) + { + _ = _buffer.TryDequeue(out _); + } + } } #endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs index 50faa1522e0..b3c4f94f786 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; @@ -30,14 +31,16 @@ public static class HttpRequestBufferLoggerBuilderExtensions /// The to add. /// The value of . /// is . - public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration) + public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration) { _ = Throw.IfNull(builder); _ = Throw.IfNull(configuration); return builder .AddHttpRequestBufferConfiguration(configuration) - .AddHttpRequestBufferProvider(); + .AddHttpRequestBufferManager() + .AddGlobalBufferConfiguration(configuration) + .AddGlobalBufferManager(); } /// @@ -49,7 +52,7 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, /// The buffer configuration options. /// The value of . /// is . - public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) + public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) { _ = Throw.IfNull(builder); @@ -57,7 +60,10 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) .Configure(configure ?? new Action(_ => { })); - return builder.AddHttpRequestBufferProvider(); + return builder + .AddHttpRequestBufferManager() + .AddGlobalBuffer(level) + .AddGlobalBufferManager(); } /// @@ -66,16 +72,20 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, /// The . /// The so that additional calls can be chained. /// is . - public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder) + internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder) { _ = Throw.IfNull(builder); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(sp => sp.GetRequiredService()); builder.Services.TryAddSingleton(); - builder.Services.TryAddActivatedSingleton(); - return builder.AddGlobalBufferProvider(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + + return builder; } /// diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs new file mode 100644 index 00000000000..16b51bf6528 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Diagnostics.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Buffering; +internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager +{ + private readonly GlobalBufferManager _globalBufferManager; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IOptionsMonitor _requestOptions; + private readonly IOptionsMonitor _globalOptions; + + public HttpRequestBufferManager( + GlobalBufferManager globalBufferManager, + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor requestOptions, + IOptionsMonitor globalOptions) + { + _globalBufferManager = globalBufferManager; + _httpContextAccessor = httpContextAccessor; + _requestOptions = requestOptions; + _globalOptions = globalOptions; + } + + public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext is null) + { + return _globalBufferManager.CreateBuffer(bufferSink, category); + } + + if (!httpContext.Items.TryGetValue(category, out var buffer)) + { + var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions, _globalOptions); + httpContext.Items[category] = httpRequestBuffer; + return httpRequestBuffer; + } + + if (buffer is not ILoggingBuffer loggingBuffer) + { + throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}"); + } + + return loggingBuffer; + } + + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] + public void Flush() => _globalBufferManager.Flush(); + + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] + public void FlushCurrentRequestLogs() + { + if (_httpContextAccessor.HttpContext is not null) + { + foreach (var kvp in _httpContextAccessor.HttpContext!.Items) + { + if (kvp.Value is ILoggingBuffer buffer) + { + buffer.Flush(); + } + } + } + } + + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] + public bool TryEnqueue( + IBufferSink bufferSink, + LogLevel logLevel, + string category, + EventId eventId, + TState attributes, + Exception? exception, + Func formatter) + { + var buffer = CreateBuffer(bufferSink, category); + return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); + } +} +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs index cf04cd2da8c..cc266422026 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs @@ -17,24 +17,15 @@ namespace Microsoft.AspNetCore.Diagnostics.Logging; public class HttpRequestBufferOptions { /// - /// Gets or sets the time to suspend the buffer after flushing. + /// Gets or sets the duration to check and remove the buffered items exceeding the . /// - /// - /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, - /// so the buffering will be suspended for the time. - /// - public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10); /// /// Gets or sets the size of the buffer for a request. /// public int PerRequestCapacity { get; set; } = 1_000; - /// - /// Gets or sets the size of the global buffer which applies to non-request logs only. - /// - public int GlobalCapacity { get; set; } = 1_000_000; - #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() #pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs deleted file mode 100644 index 3ed5c08b1d7..00000000000 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NET9_0_OR_GREATER -using System.Collections.Concurrent; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Diagnostics.Logging; - -internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider -{ - private readonly GlobalBufferProvider _globalBufferProvider; - private readonly IHttpContextAccessor _accessor; - private readonly ConcurrentDictionary _requestBuffers = new(); - - public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor) - { - _globalBufferProvider = globalBufferProvider; - _accessor = accessor; - } - - public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null - ? _globalBufferProvider.CurrentBuffer - : _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService()); - - // TO DO: Dispose request buffer when the respective HttpContext is disposed -} -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs deleted file mode 100644 index 8983e97843b..00000000000 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NET9_0_OR_GREATER -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Diagnostics.Logging; - -internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord -{ - public HttpRequestBufferedLogRecord( - LogLevel logLevel, - EventId eventId, - IReadOnlyList> state, - Exception? exception, - string? formatter) - { - LogLevel = logLevel; - EventId = eventId; - Attributes = state; - Exception = exception?.ToString(); // wtf?? - FormattedMessage = formatter; - } - - public override IReadOnlyList> Attributes { get; } - public override string? FormattedMessage { get; } - public override string? Exception { get; } - - public override DateTimeOffset Timestamp { get; } - - public override LogLevel LogLevel { get; } - - public override EventId EventId { get; } -} -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs index 52742297d0b..d237cd9cf51 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs @@ -1,21 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - #if NET9_0_OR_GREATER using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// -/// Interface providing access to the current logging buffer. +/// Interface for a global buffer manager. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface ILoggingBufferProvider +public interface IHttpRequestBufferManager : IBufferManager { /// - /// Gets current logging buffer. + /// Flushes the buffer and emits buffered logs for the current request. /// - public ILoggingBuffer CurrentBuffer { get; } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] + public void FlushCurrentRequestLogs(); } #endif diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs index 9ba5586dba6..d3fb76cec0b 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs @@ -8,9 +8,10 @@ using System.Globalization; #if NET9_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; - +using Microsoft.Shared.JsonExceptionConverter; #endif using Microsoft.Shared.Diagnostics; @@ -118,19 +119,20 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except #if NET9_0_OR_GREATER /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public void LogRecords(IEnumerable records) { _ = Throw.IfNull(records); var l = new List(); - ScopeProvider.ForEachScope((scopeState, list) => list.Add(scopeState), l); foreach (var rec in records) { -#pragma warning disable CA2201 // TO DO: Remove suppression and implement propert Exception deserialization - var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), new Exception(rec.Exception), rec.FormattedMessage ?? string.Empty, +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + var exception = rec.Exception is not null ? JsonSerializer.Deserialize(rec.Exception, ExceptionConverter.JsonSerializerOptions) : null; +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), exception, rec.FormattedMessage ?? string.Empty, l.ToArray(), Category, !_disabledLevels.ContainsKey(rec.LogLevel), rec.Timestamp); -#pragma warning restore CA2201 Collector.AddRecord(record); } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj index 01f3b954262..db21a9e64b9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj @@ -11,6 +11,7 @@ true true true + true $(NoWarn);SYSLIB1100;SYSLIB1101 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs new file mode 100644 index 00000000000..850bbb52277 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.Diagnostics.Buffering; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.JsonExceptionConverter; +using Microsoft.Shared.Pools; +using static Microsoft.Extensions.Logging.ExtendedLoggerFactory; + +namespace Microsoft.Extensions.Logging; +internal sealed class BufferSink : IBufferSink +{ + private readonly ExtendedLoggerFactory _factory; + private readonly string _category; + private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); + + public BufferSink(ExtendedLoggerFactory factory, string category) + { + _factory = factory; + _category = category; + } + + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + public void LogRecords(IEnumerable serializedRecords) + where T : ISerializedLogRecord + { + var providers = _factory.ProviderRegistrations; + + List? pooledList = null; + try + { + foreach (var provider in providers) + { + var logger = provider.Provider.CreateLogger(_category); + + if (logger is IBufferedLogger bufferedLogger) + { + if (pooledList is null) + { + pooledList = _logRecordPool.Get(); + + foreach (var serializedRecord in serializedRecords) + { + pooledList.Add( + new PooledLogRecord( + serializedRecord.Timestamp, + serializedRecord.LogLevel, + serializedRecord.EventId, + serializedRecord.Exception, + serializedRecord.FormattedMessage, + serializedRecord.Attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)).ToArray())); + } + } + + bufferedLogger.LogRecords(pooledList); + + } + else + { + foreach (var serializedRecord in serializedRecords) + { + Exception? exception = null; + if (serializedRecord.Exception is not null) + { + exception = JsonSerializer.Deserialize(serializedRecord.Exception, ExceptionConverter.JsonSerializerOptions); + } + + logger.Log( + serializedRecord.LogLevel, + serializedRecord.EventId, + serializedRecord.Attributes, + exception, + (_, _) => serializedRecord.FormattedMessage ?? string.Empty); + } + } + } + } + finally + { + if (pooledList is not null) + { + _logRecordPool.Return(pooledList); + } + } + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index ffba82f84c6..57ae8da929d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -4,113 +4,81 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.Extensions.Logging; -internal sealed class GlobalBuffer : BackgroundService, ILoggingBuffer +internal sealed class GlobalBuffer : ILoggingBuffer { private readonly IOptionsMonitor _options; - private readonly ConcurrentDictionary> _buffers; - private readonly TimeProvider _timeProvider = TimeProvider.System; + private readonly ConcurrentQueue _buffer; + private readonly IBufferSink _bufferSink; + private readonly TimeProvider _timeProvider; private DateTimeOffset _lastFlushTimestamp; - public GlobalBuffer(IOptionsMonitor options) + public GlobalBuffer(IBufferSink bufferSink, IOptionsMonitor options, TimeProvider timeProvider) { _options = options; - _lastFlushTimestamp = _timeProvider.GetUtcNow(); - _buffers = new ConcurrentDictionary>(); - } - - internal GlobalBuffer(IOptionsMonitor options, TimeProvider timeProvider) - : this(options) - { _timeProvider = timeProvider; - _lastFlushTimestamp = _timeProvider.GetUtcNow(); + _buffer = new ConcurrentQueue(); + _bufferSink = bufferSink; } - public bool TryEnqueue( - IBufferedLogger logger, + [RequiresUnreferencedCode( + "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] + public bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - IReadOnlyList> joiner, - Exception? exception, string formatter) + T attributes, + Exception? exception, + Func formatter) { if (!IsEnabled(category, logLevel, eventId)) { return false; } - var record = new GlobalBufferedLogRecord(logLevel, eventId, joiner, exception, formatter); - var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue()); - if (queue.Count >= _options.CurrentValue.Capacity) + switch (attributes) { - _ = queue.TryDequeue(out GlobalBufferedLogRecord? _); + case ModernTagJoiner modernTagJoiner: + _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, + ((Func)(object)formatter)(modernTagJoiner, exception))); + break; + case LegacyTagJoiner legacyTagJoiner: + _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, + ((Func)(object)formatter)(legacyTagJoiner, exception))); + break; + default: + Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(T)}"); + break; } - queue.Enqueue(record); - return true; } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] public void Flush() { - foreach (var (logger, queue) in _buffers) - { - var result = new List(); - while (!queue.IsEmpty) - { - if (queue.TryDequeue(out GlobalBufferedLogRecord? item)) - { - result.Add(item); - } - } - - logger.LogRecords(result); - } + var result = _buffer.ToArray(); + _buffer.Clear(); _lastFlushTimestamp = _timeProvider.GetUtcNow(); - } - internal void RemoveExpiredItems() - { - foreach (var (logger, queue) in _buffers) - { - while (!queue.IsEmpty) - { - if (queue.TryPeek(out GlobalBufferedLogRecord? item)) - { - if (_timeProvider.GetUtcNow() - item.Timestamp > _options.CurrentValue.Duration) - { - _ = queue.TryDequeue(out _); - } - else - { - break; - } - } - else - { - break; - } - } - } + _bufferSink.LogRecords(result); } - protected override async Task ExecuteAsync(CancellationToken cancellationToken) + public void TruncateOverlimit() { - while (!cancellationToken.IsCancellationRequested) + // Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments. + while (_buffer.Count > _options.CurrentValue.Capacity) { - await _timeProvider.Delay(_options.CurrentValue.Duration, cancellationToken).ConfigureAwait(false); - RemoveExpiredItems(); + _ = _buffer.TryDequeue(out _); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs index 3789dc9c0f6..c4800c05af3 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -36,7 +37,7 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, ICon return builder .AddGlobalBufferConfiguration(configuration) - .AddGlobalBufferProvider(); + .AddGlobalBufferManager(); } /// @@ -56,26 +57,23 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, LogL .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) .Configure(configure ?? new Action(_ => { })); - return builder.AddGlobalBufferProvider(); + return builder.AddGlobalBufferManager(); } /// - /// Adds global logging buffer provider. + /// Adds global logging buffer manager. /// /// The . /// The so that additional calls can be chained. - public static ILoggingBuilder AddGlobalBufferProvider(this ILoggingBuilder builder) + internal static ILoggingBuilder AddGlobalBufferManager(this ILoggingBuilder builder) { _ = Throw.IfNull(builder); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs new file mode 100644 index 00000000000..6a6cef25ce1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Buffering; +internal sealed class GlobalBufferManager : BackgroundService, IBufferManager +{ + internal readonly ConcurrentDictionary Buffers = []; + private readonly IOptionsMonitor _options; + private readonly TimeProvider _timeProvider = TimeProvider.System; + + public GlobalBufferManager(IOptionsMonitor options) + { + _options = options; + } + + internal GlobalBufferManager(IOptionsMonitor options, TimeProvider timeProvider) + { + _timeProvider = timeProvider; + _options = options; + } + + public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) + => Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferSink, _options, _timeProvider)); + + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] + public void Flush() + { + foreach (var buffer in Buffers.Values) + { + buffer.Flush(); + } + } + + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] + public bool TryEnqueue( + IBufferSink bufferSink, + LogLevel logLevel, + string category, + EventId eventId, TState attributes, + Exception? exception, + Func formatter) + { + var buffer = CreateBuffer(bufferSink, category); + return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await _timeProvider.Delay(_options.CurrentValue.Duration, cancellationToken).ConfigureAwait(false); + foreach (var buffer in Buffers.Values) + { + buffer.TruncateOverlimit(); + } + } + } + +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs index 7c15a8f3b9b..19009fb9a0d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -25,14 +25,14 @@ public class GlobalBufferOptions public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); /// - /// Gets or sets the duration to keep logs in the buffer. + /// Gets or sets the duration to check and remove the buffered items exceeding the . /// public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(30); /// /// Gets or sets the size of the buffer. /// - public int Capacity { get; set; } = 1_000_000; + public int Capacity { get; set; } = 10_000; #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() #pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs deleted file mode 100644 index 5bfedc32516..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NET9_0_OR_GREATER -namespace Microsoft.Extensions.Logging; - -internal sealed class GlobalBufferProvider : ILoggingBufferProvider -{ - private readonly GlobalBuffer _buffer; - - public GlobalBufferProvider(GlobalBuffer buffer) - { - _buffer = buffer; - } - - public ILoggingBuffer CurrentBuffer => _buffer; -} -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs deleted file mode 100644 index e72fc3bed2f..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferedLogRecord.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NET9_0_OR_GREATER -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.Extensions.Logging; - -internal sealed class GlobalBufferedLogRecord : BufferedLogRecord -{ - public GlobalBufferedLogRecord( - LogLevel logLevel, - EventId eventId, - IReadOnlyList> state, - Exception? exception, - string? formatter) - { - LogLevel = logLevel; - EventId = eventId; - Attributes = state; - Exception = exception?.ToString(); // wtf?? - FormattedMessage = formatter; - } - - public override IReadOnlyList> Attributes { get; } - public override string? FormattedMessage { get; } - public override string? Exception { get; } - - public override DateTimeOffset Timestamp { get; } - - public override LogLevel LogLevel { get; } - - public override EventId EventId { get; } -} -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs new file mode 100644 index 00000000000..2905c9b269c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Diagnostics.Buffering; + +/// +/// Interface for a global buffer manager. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface IBufferManager +{ + /// + /// Flushes the buffer and emits all buffered logs. + /// + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] + void Flush(); + + /// + /// Enqueues a log record in the underlying buffer. + /// + /// Buffer sink. + /// Log level. + /// Category. + /// Event ID. + /// Attributes. + /// Exception. + /// Formatter delegate. + /// Type of the instance. + /// if the log record was buffered; otherwise, . + [RequiresUnreferencedCode( + "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] + bool TryEnqueue( + IBufferSink bufferSink, + LogLevel logLevel, + string category, + EventId eventId, + TState attributes, + Exception? exception, + Func formatter); +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs new file mode 100644 index 00000000000..757f9d2b111 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// Interface for a buffer sink. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface IBufferSink +{ + /// + /// Forward the to all currently registered loggers. + /// + /// serialized log records. + /// Type of the log records. + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] + void LogRecords(IEnumerable serializedRecords) + where T : ISerializedLogRecord; +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index baf15325156..a25d249d5d7 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -3,36 +3,46 @@ #if NET9_0_OR_GREATER using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Logging; /// /// Interface for a logging buffer. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface ILoggingBuffer +internal interface ILoggingBuffer { /// - /// Flushes the buffer and emits all buffered logs. + /// Enqueues a log record in the underlying buffer.. /// - void Flush(); - - /// - /// Enqueues a log record. - /// - /// true or false. - bool TryEnqueue( - IBufferedLogger logger, + /// Log level. + /// Category. + /// Event ID. + /// Attributes. + /// Exception. + /// Formatter delegate. + /// Type of the instance. + /// if the log record was buffered; otherwise, . + [RequiresUnreferencedCode( + "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] + bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - IReadOnlyList> joiner, + TState attributes, Exception? exception, - string formatter); + Func formatter); + + /// + /// Flushes the buffer. + /// + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] + void Flush(); + + /// + /// Removes items exceeding the buffer limit. + /// + void TruncateOverlimit(); } #endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs new file mode 100644 index 00000000000..23f03323893 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Logging; + +/// +/// Represents a buffered log record that has been serialized. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface ISerializedLogRecord +{ + /// + /// Gets the time when the log record was first created. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Gets the record's logging severity level. + /// + public LogLevel LogLevel { get; } + + /// + /// Gets the record's event id. + /// + public EventId EventId { get; } + + /// + /// Gets an exception string for this record. + /// + public string? Exception { get; } + + /// + /// Gets the formatted log message. + /// + public string? FormattedMessage { get; } + + /// + /// Gets the variable set of name/value pairs associated with the record. + /// + public IReadOnlyList> Attributes { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs new file mode 100644 index 00000000000..e20bc1069a3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.Diagnostics.Buffering; +internal sealed class PooledLogRecord : BufferedLogRecord, IResettable +{ + public PooledLogRecord( + DateTimeOffset timestamp, + LogLevel logLevel, + EventId eventId, + string? exception, + string? formattedMessage, + IReadOnlyList> attributes) + { + _timestamp = timestamp; + _logLevel = logLevel; + _eventId = eventId; + _exception = exception; + _formattedMessage = formattedMessage; + _attributes = attributes; + } + + public override DateTimeOffset Timestamp => _timestamp; + private DateTimeOffset _timestamp; + + public override LogLevel LogLevel => _logLevel; + private LogLevel _logLevel; + + public override EventId EventId => _eventId; + private EventId _eventId; + + public override string? Exception => _exception; + private string? _exception; + + public override ActivitySpanId? ActivitySpanId => _activitySpanId; + private ActivitySpanId? _activitySpanId; + + public override ActivityTraceId? ActivityTraceId => _activityTraceId; + private ActivityTraceId? _activityTraceId; + + public override int? ManagedThreadId => _managedThreadId; + private int? _managedThreadId; + + public override string? FormattedMessage => _formattedMessage; + private string? _formattedMessage; + + public override string? MessageTemplate => _messageTemplate; + private string? _messageTemplate; + + public override IReadOnlyList> Attributes => _attributes; + private IReadOnlyList> _attributes; + + public bool TryReset() + { + _timestamp = default; + _logLevel = default; + _eventId = default; + _exception = default; + _activitySpanId = default; + _activityTraceId = default; + _managedThreadId = default; + _formattedMessage = default; + _messageTemplate = default; + _attributes = []; + + return true; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs new file mode 100644 index 00000000000..7aa9df3056f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.JsonExceptionConverter; + +namespace Microsoft.Extensions.Logging; + +internal readonly struct SerializedLogRecord : ISerializedLogRecord +{ + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + public SerializedLogRecord( + LogLevel logLevel, + EventId eventId, + DateTimeOffset timestamp, + IReadOnlyList> attributes, + Exception? exception, + string formattedMessage) + { + LogLevel = logLevel; + EventId = eventId; + Timestamp = timestamp; + + var serializedAttributes = new List>(attributes.Count); + for (int i = 0; i < attributes.Count; i++) + { + serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); + } + + Attributes = serializedAttributes; + + // Serialize without StackTrace, whis is already optionally available in the log attributes via the ExtendedLogger. + Exception = JsonSerializer.Serialize(exception, ExceptionConverter.JsonSerializerOptions); + FormattedMessage = formattedMessage; + } + + public IReadOnlyList> Attributes { get; } + public string? FormattedMessage { get; } + public string? Exception { get; } + + public DateTimeOffset Timestamp { get; } + + public LogLevel LogLevel { get; } + + public EventId EventId { get; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index b39803404dc..18675854551 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; #if NET9_0_OR_GREATER -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Diagnostics.Buffering; #endif using Microsoft.Shared.Pools; @@ -33,13 +34,36 @@ internal sealed partial class ExtendedLogger : ILogger public MessageLogger[] MessageLoggers { get; set; } = Array.Empty(); public ScopeLogger[] ScopeLoggers { get; set; } = Array.Empty(); +#if NET9_0_OR_GREATER + private readonly IBufferManager? _bufferManager; + private readonly IBufferSink? _bufferSink; + + public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers) + { + _factory = factory; + Loggers = loggers; + + _bufferManager = _factory.Config.BufferManager; + if (_bufferManager is not null) + { + Debug.Assert(loggers.Length > 0, "There should be at least one logger provider."); + + _bufferSink = new BufferSink(factory, loggers[0].Category); + } + } + +#else public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers) { _factory = factory; Loggers = loggers; } +#endif + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ExtendedLogger.ModernPath(LogLevel, EventId, LoggerMessageState, Exception, Func)")] +#pragma warning disable IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) +#pragma warning restore IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. { if (typeof(TState) == typeof(LoggerMessageState)) { @@ -207,6 +231,7 @@ void HandleException(Exception exception, int indent) } } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState msgState, Exception? exception, Func formatter) { var loggers = MessageLoggers; @@ -263,21 +288,35 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m RecordException(exception, joiner.EnrichmentTagCollector, config); } +#if NET9_0_OR_GREATER + bool? shouldBuffer = null; +#endif for (int i = 0; i < loggers.Length; i++) { ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { #if NET9_0_OR_GREATER - if (loggerInfo.Logger is IBufferedLogger bufferedLogger) + if (shouldBuffer is null or true) { - if (config.BufferProvider is not null && - config.BufferProvider.CurrentBuffer.TryEnqueue(bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, joiner.Formatter!(joiner.State, exception))) + if (_bufferManager is not null) { + var result = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + { + var fmt = s.Formatter!; + return fmt(s.State!, e); + }); + shouldBuffer = result; + // The record was buffered, so we skip logging it for now. - // When a caller needs to flush the buffer and calls ILoggerBuffer.Flush(), - // the buffer will internally call IBufferedLogger.LogRecords to emit log records. + // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. continue; + + } + else + { + shouldBuffer = false; } } #endif @@ -311,6 +350,7 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m HandleExceptions(exceptions); } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] private void LegacyPath(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { var loggers = MessageLoggers; @@ -360,24 +400,35 @@ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state { RecordException(exception, joiner.EnrichmentTagCollector, config); } - +#if NET9_0_OR_GREATER + bool? shouldBuffer = null; +#endif for (int i = 0; i < loggers.Length; i++) { ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { #if NET9_0_OR_GREATER - if (loggerInfo.Logger is IBufferedLogger bufferedLogger) + if (shouldBuffer is null or true) { - if (config.BufferProvider is not null && - config.BufferProvider.CurrentBuffer.TryEnqueue( - bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, - ((Func)joiner.Formatter)((TState)joiner.State!, exception))) + if (_bufferManager is not null) { + var result = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + { + var fmt = (Func)s.Formatter!; + return fmt((TState)s.State!, e); + }); + shouldBuffer = result; + // The record was buffered, so we skip logging it for now. - // When a caller needs to flush the buffer and calls ILoggerBuffer.Flush(), - // the buffer will internally call IBufferedLogger.LogRecords to emit log records. + // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. continue; + + } + else + { + shouldBuffer = false; } } #endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 1ea4f0dc0b3..8d2b4923d4c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -7,6 +7,9 @@ using System.Linq; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; +#if NET9_0_OR_GREATER +using Microsoft.Extensions.Diagnostics.Buffering; +#endif using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; @@ -24,13 +27,14 @@ internal sealed class ExtendedLoggerFactory : ILoggerFactory private readonly IDisposable? _redactionOptionsChangeTokenRegistration; private readonly Action[] _enrichers; #if NET9_0_OR_GREATER - private readonly ILoggingBufferProvider? _bufferProvider; + private readonly IBufferManager? _bufferManager; #endif private readonly KeyValuePair[] _staticTags; private readonly Func _redactorProvider; private volatile bool _disposed; private LoggerFilterOptions _filterOptions; private IExternalScopeProvider? _scopeProvider; + public IReadOnlyCollection ProviderRegistrations => _providerRegistrations; #pragma warning disable S107 // Methods should not have too many parameters public ExtendedLoggerFactory( @@ -44,7 +48,7 @@ public ExtendedLoggerFactory( IOptionsMonitor? redactionOptions = null, #if NET9_0_OR_GREATER IRedactorProvider? redactorProvider = null, - ILoggingBufferProvider? bufferProvider = null) + IBufferManager? bufferManager = null) #else IRedactorProvider? redactorProvider = null) #endif @@ -52,7 +56,7 @@ public ExtendedLoggerFactory( { _scopeProvider = scopeProvider; #if NET9_0_OR_GREATER - _bufferProvider = bufferProvider; + _bufferManager = bufferManager; #endif _factoryOptions = factoryOptions == null || factoryOptions.Value == null ? new LoggerFactoryOptions() : factoryOptions.Value; @@ -302,7 +306,7 @@ private LoggerConfig ComputeConfig(LoggerEnrichmentOptions? enrichmentOptions, L _redactorProvider, #if NET9_0_OR_GREATER redactionOptions.ApplyDiscriminator, - _bufferProvider); + _bufferManager); #else redactionOptions.ApplyDiscriminator); #endif @@ -311,7 +315,7 @@ private LoggerConfig ComputeConfig(LoggerEnrichmentOptions? enrichmentOptions, L private void UpdateEnrichmentOptions(LoggerEnrichmentOptions enrichmentOptions) => Config = ComputeConfig(enrichmentOptions, null); private void UpdateRedactionOptions(LoggerRedactionOptions redactionOptions) => Config = ComputeConfig(null, redactionOptions); - private struct ProviderRegistration + public struct ProviderRegistration { public ILoggerProvider Provider; public bool ShouldDispose; diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs index fba961defd5..c84ba772e86 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs @@ -5,6 +5,9 @@ using System.Collections.Generic; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; +#if NET9_0_OR_GREATER +using Microsoft.Extensions.Diagnostics.Buffering; +#endif using Microsoft.Extensions.Diagnostics.Enrichment; namespace Microsoft.Extensions.Logging; @@ -22,7 +25,7 @@ public LoggerConfig( Func getRedactor, #if NET9_0_OR_GREATER bool addRedactionDiscriminator, - ILoggingBufferProvider? bufferProvider) + IBufferManager? bufferManager) #else bool addRedactionDiscriminator) #endif @@ -37,7 +40,7 @@ public LoggerConfig( GetRedactor = getRedactor; AddRedactionDiscriminator = addRedactionDiscriminator; #if NET9_0_OR_GREATER - BufferProvider = bufferProvider; + BufferManager = bufferManager; #endif } @@ -50,6 +53,6 @@ public LoggerConfig( public Func GetRedactor { get; } public bool AddRedactionDiscriminator { get; } #if NET9_0_OR_GREATER - public ILoggingBufferProvider? BufferProvider { get; } + public IBufferManager? BufferManager { get; } #endif } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index f303698ed82..fd344853ab2 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -16,6 +16,7 @@ true true true + true true true diff --git a/src/Shared/JsonExceptionConverter/ExceptionConverter.cs b/src/Shared/JsonExceptionConverter/ExceptionConverter.cs new file mode 100644 index 00000000000..7b62eb81528 --- /dev/null +++ b/src/Shared/JsonExceptionConverter/ExceptionConverter.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.JsonExceptionConverter; +#pragma warning restore CA1716 + +internal sealed class ExceptionConverter : JsonConverter +{ +#pragma warning disable CA2201 // Do not raise reserved exception types + private static Exception _failedDeserialization = new Exception("Failed to deserialize the exception object."); +#pragma warning restore CA2201 // Do not raise reserved exception types + + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + return _failedDeserialization; + } + + return DeserializeException(ref reader); + } + + public override void Write(Utf8JsonWriter writer, Exception exception, JsonSerializerOptions options) + { + HandleException(writer, exception); + } + + public static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + Converters = { new ExceptionConverter() } + }; + + public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); + + private static Exception DeserializeException(ref Utf8JsonReader reader) + { + string? type = null; + string? message = null; + string? source = null; + Exception? innerException = null; + List? innerExceptions = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + _ = reader.Read(); + + switch (propertyName) + { + case "Type": + type = reader.GetString(); + break; + case "Message": + message = reader.GetString(); + break; + case "Source": + source = reader.GetString(); + break; + case "InnerException": + if (reader.TokenType == JsonTokenType.StartObject) + { + innerException = DeserializeException(ref reader); + } + + break; + case "InnerExceptions": + innerExceptions = new List(); + if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.StartObject) + { + var innerEx = DeserializeException(ref reader); + innerExceptions.Add(innerEx); + } + } + } + + break; + } + } + else if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + } + + if (type is null) + { + return _failedDeserialization; + } + +#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + Type? deserializedType = Type.GetType(type); +#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + if (deserializedType is null) + { + return _failedDeserialization; + } + + Exception? exception; + + if (innerExceptions != null && innerExceptions.Count > 0) + { + if (deserializedType == typeof(AggregateException)) + { + exception = new AggregateException(message, innerExceptions); + } + else + { + exception = Activator.CreateInstance(deserializedType, message, innerExceptions.First()) as Exception; + } + } + else if (innerException != null) + { + exception = Activator.CreateInstance(deserializedType, message, innerException) as Exception; + } + else + { + exception = Activator.CreateInstance(deserializedType, message) as Exception; + } + + if (exception == null) + { + return _failedDeserialization; + } + + exception.Source = source; + + return exception; + } + + private static void HandleException(Utf8JsonWriter writer, Exception exception) + { + writer.WriteStartObject(); + writer.WriteString("Type", exception.GetType().FullName); + writer.WriteString("Message", exception.Message); + writer.WriteString("Source", exception.Source); + if (exception is AggregateException aggregateException) + { + writer.WritePropertyName("InnerExceptions"); + writer.WriteStartArray(); + foreach (var ex in aggregateException.InnerExceptions) + { + HandleException(writer, ex); + } + + writer.WriteEndArray(); + } + else if (exception.InnerException != null) + { + writer.WritePropertyName("InnerException"); + HandleException(writer, exception.InnerException); + } + + writer.WriteEndObject(); + } +} +#endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs index e389d65c57b..76162297cf7 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; @@ -15,33 +16,19 @@ namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; public class HttpRequestBufferLoggerBuilderExtensionsTests { [Fact] - public void AddHttpRequestBuffer_RegistersInDI() + public void AddHttpRequestBuffering_RegistersInDI() { var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { - builder.AddHttpRequestBuffer(LogLevel.Warning); + builder.AddHttpRequestBuffering(LogLevel.Warning); }); var serviceProvider = serviceCollection.BuildServiceProvider(); - var buffer = serviceProvider.GetService(); + var buffer = serviceProvider.GetService(); Assert.NotNull(buffer); - Assert.IsAssignableFrom(buffer); - } - - [Fact] - public void AddHttpRequestBufferProvider_RegistersInDI() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(builder => - { - builder.AddHttpRequestBufferProvider(); - }); - var serviceProvider = serviceCollection.BuildServiceProvider(); - var bufferProvider = serviceProvider.GetService(); - Assert.NotNull(bufferProvider); - Assert.IsAssignableFrom(bufferProvider); + Assert.IsAssignableFrom(buffer); } [Fact] @@ -50,9 +37,8 @@ public void WhenArgumentNull_Throws() var builder = null as ILoggingBuilder; var configuration = null as IConfiguration; - Assert.Throws(() => builder!.AddHttpRequestBuffer(LogLevel.Warning)); - Assert.Throws(() => builder!.AddHttpRequestBuffer(configuration!)); - Assert.Throws(() => builder!.AddHttpRequestBufferProvider()); + Assert.Throws(() => builder!.AddHttpRequestBuffering(LogLevel.Warning)); + Assert.Throws(() => builder!.AddHttpRequestBuffering(configuration!)); } [Fact] diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index 6856b9751a8..5bb7f99cc43 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -26,7 +26,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; #if NET9_0_OR_GREATER -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Diagnostics.Buffering; #endif using Microsoft.Extensions.Time.Testing; using Microsoft.Net.Http.Headers; @@ -59,6 +59,31 @@ public static void Configure(IApplicationBuilder app) app.UseRouting(); app.UseHttpLogging(); +#if NET9_0_OR_GREATER + app.Map("/flushrequestlogs", static x => + x.Run(static async context => + { + await context.Request.Body.DrainAsync(default); + + // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor + var bufferManager = context.RequestServices.GetService(); + bufferManager?.FlushCurrentRequestLogs(); + })); + + app.Map("/flushalllogs", static x => + x.Run(static async context => + { + await context.Request.Body.DrainAsync(default); + + // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor + var bufferManager = context.RequestServices.GetService(); + if (bufferManager is not null) + { + bufferManager.FlushCurrentRequestLogs(); + bufferManager.Flush(); + } + })); +#endif app.Map("/error", static x => x.Run(static async context => { @@ -722,40 +747,39 @@ await RunAsync( [Fact] public async Task HttpRequestBuffering() { - var clock = new FakeTimeProvider(TimeProvider.System.GetUtcNow()); - - HttpRequestBufferOptions options = new() - { - SuspendAfterFlushDuration = TimeSpan.FromSeconds(0), - Rules = new List - { - new(null, LogLevel.Information, null), - } - }; - var buffer = new HttpRequestBuffer(new StaticOptionsMonitor(options), clock); - await RunAsync( - LogLevel.Information, - services => services.AddLogging(builder => + LogLevel.Trace, + services => services + .AddLogging(builder => { - builder.Services.AddScoped(sp => buffer); - builder.Services.AddScoped(sp => buffer); - builder.AddHttpRequestBuffer(LogLevel.Information); + // enable Microsoft.AspNetCore.Routing.Matching.DfaMatcher debug logs + // which we are produced by ASP.NET Core within HTTP context: + builder.AddFilter("Microsoft.AspNetCore.Routing.Matching.DfaMatcher", LogLevel.Debug); + + builder.AddHttpRequestBuffering(LogLevel.Debug); }), async (logCollector, client, sp) => { - using var response = await client.GetAsync("/home").ConfigureAwait(false); - + // just HTTP request logs: + using var response = await client.GetAsync("/flushrequestlogs").ConfigureAwait(false); Assert.True(response.IsSuccessStatusCode); - - Assert.Equal(0, logCollector.Count); - - using var scope = sp.CreateScope(); - var buffer = scope.ServiceProvider.GetRequiredService(); - buffer.Flush(); - await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); Assert.Equal(1, logCollector.Count); + Assert.Equal(LogLevel.Debug, logCollector.LatestRecord.Level); + Assert.Equal("Microsoft.AspNetCore.Routing.Matching.DfaMatcher", logCollector.LatestRecord.Category); + + // HTTP request logs + global logs: + using var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("test"); + logger.LogTrace("This is a log message"); + using var response2 = await client.GetAsync("/flushalllogs").ConfigureAwait(false); + Assert.True(response2.IsSuccessStatusCode); + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + // 1 and 2 records are from DfaMatcher, and 3rd is from our test category + Assert.Equal(3, logCollector.Count); + Assert.Equal(LogLevel.Trace, logCollector.LatestRecord.Level); + Assert.Equal("test", logCollector.LatestRecord.Category); }); } #endif @@ -803,19 +827,5 @@ private class ThrowingEnricher : IHttpLogEnricher { public void Enrich(IEnrichmentTagCollector collector, HttpContext httpContext) => throw new InvalidOperationException(); } - -#if NET9_0_OR_GREATER - private sealed class StaticOptionsMonitor : IOptionsMonitor - { - public StaticOptionsMonitor(T currentValue) - { - CurrentValue = currentValue; - } - - public IDisposable? OnChange(Action listener) => null; - public T Get(string? name) => CurrentValue; - public T CurrentValue { get; } - } -#endif } #endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs new file mode 100644 index 00000000000..8233f29a322 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NET9_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Shared.JsonExceptionConverter; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test; + +public class ExceptionConverterTests +{ + private static readonly JsonSerializerOptions _options = new() + { + Converters = { new ExceptionConverter() } + }; + + [Fact] + public void SerializeAndDeserialize_SimpleException() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new ExceptionConverter()); + + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var json = JsonSerializer.Serialize(exception, options); + var deserializedException = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Equal(exception.Message, deserializedException.Message); + } + + [Fact] + public void SerializeAndDeserialize_ExceptionWithInnerException() + { + // Arrange + var innerException = new ArgumentNullException("paramName", "Inner exception message"); + var exception = new InvalidOperationException("Test exception with inner exception", innerException); + + // Act + var json = JsonSerializer.Serialize(exception, _options); + var deserializedException = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Equal(exception.Message, deserializedException.Message); + + Assert.NotNull(deserializedException.InnerException); + Assert.IsType(deserializedException.InnerException); + Assert.Contains(innerException.Message, deserializedException.InnerException.Message); + } + + [Fact] + public void SerializeAndDeserialize_AggregateException() + { + // Arrange + var innerException1 = new ArgumentException("First inner exception"); +#pragma warning disable CA2201 // Do not raise reserved exception types + var innerException2 = new NullReferenceException("Second inner exception"); +#pragma warning restore CA2201 // Do not raise reserved exception types + var exception = new AggregateException("Aggregate exception message", innerException1, innerException2); + + // Act + var json = JsonSerializer.Serialize(exception, _options); + var deserializedException = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Contains(exception.Message, deserializedException.Message); + + var aggregateException = (AggregateException)deserializedException; + Assert.NotNull(aggregateException.InnerExceptions); + Assert.Equal(2, aggregateException.InnerExceptions.Count); + + Assert.IsType(aggregateException.InnerExceptions[0]); + Assert.Equal(innerException1.Message, aggregateException.InnerExceptions[0].Message); + + Assert.IsType(aggregateException.InnerExceptions[1]); + Assert.Equal(innerException2.Message, aggregateException.InnerExceptions[1].Message); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index be6b42197d3..e28b8b246b8 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 System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; @@ -24,10 +25,10 @@ public void AddGlobalBuffer_RegistersInDI() }); var serviceProvider = serviceCollection.BuildServiceProvider(); - var buffer = serviceProvider.GetService(); + var bufferManager = serviceProvider.GetService(); - Assert.NotNull(buffer); - Assert.IsAssignableFrom(buffer); + Assert.NotNull(bufferManager); + Assert.IsAssignableFrom(bufferManager); } [Fact] @@ -38,7 +39,6 @@ public void WhenArgumentNull_Throws() Assert.Throws(() => builder!.AddGlobalBuffer(LogLevel.Warning)); Assert.Throws(() => builder!.AddGlobalBuffer(configuration!)); - Assert.Throws(() => builder!.AddGlobalBufferProvider()); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index 7410a2e0954..8b7cb598d1f 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; #if NET9_0_OR_GREATER -using Microsoft.Extensions.Time.Testing; +using Microsoft.Extensions.Diagnostics.Buffering; #endif using Moq; using Xunit; @@ -124,52 +124,30 @@ public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment #if NET9_0_OR_GREATER [Fact] - public static void GlobalBuffering() + public static void GlobalBuffering_CanonicalUsecase() { - const string Category = "B1"; - var clock = new FakeTimeProvider(TimeProvider.System.GetUtcNow()); - - GlobalBufferOptions options = new() - { - Duration = TimeSpan.FromSeconds(60), - SuspendAfterFlushDuration = TimeSpan.FromSeconds(0), - Rules = new List - { - new(null, LogLevel.Warning, null), - } - }; - using var buffer = new GlobalBuffer(new StaticOptionsMonitor(options), clock); - using var provider = new Provider(); using var factory = Utils.CreateLoggerFactory( builder => { builder.AddProvider(provider); - builder.Services.AddSingleton(buffer); - builder.AddGlobalBufferProvider(); + builder.AddGlobalBuffer(LogLevel.Warning); }); - var logger = factory.CreateLogger(Category); + var logger = factory.CreateLogger("my category"); logger.LogWarning("MSG0"); logger.Log(LogLevel.Warning, new EventId(2, "ID2"), "some state", null, (_, _) => "MSG2"); - // nothing is logged because the buffer is not flushed + // nothing is logged because the buffer not flushed yet Assert.Equal(0, provider.Logger!.Collector.Count); - buffer.Flush(); - - // 2 log records emitted because the buffer was flushed - Assert.Equal(2, provider.Logger!.Collector.Count); - - logger.LogWarning("MSG0"); - logger.Log(LogLevel.Warning, new EventId(2, "ID2"), "some state", null, (_, _) => "MSG2"); - - clock.Advance(options.Duration); + // instead of this, users would get IBufferManager from DI and call Flush on it + var dlf = (Utils.DisposingLoggerFactory)factory; + var bufferManager = dlf.ServiceProvider.GetRequiredService(); - // forcefully clear buffer instead of async waiting for it to be done on its own which is inherently racy. - buffer.RemoveExpiredItems(); + bufferManager.Flush(); - // still 2 because the buffer is cleared after Duration time elapses + // 2 log records emitted because the buffer has been flushed Assert.Equal(2, provider.Logger!.Collector.Count); } #endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs index bf09f8cb91c..cb6a39a29e8 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs @@ -24,21 +24,21 @@ public static ILoggerFactory CreateLoggerFactory(Action? config return new DisposingLoggerFactory(loggerFactory, serviceProvider); } - private sealed class DisposingLoggerFactory : ILoggerFactory + internal sealed class DisposingLoggerFactory : ILoggerFactory { private readonly ILoggerFactory _loggerFactory; - private readonly ServiceProvider _serviceProvider; + internal readonly ServiceProvider ServiceProvider; public DisposingLoggerFactory(ILoggerFactory loggerFactory, ServiceProvider serviceProvider) { _loggerFactory = loggerFactory; - _serviceProvider = serviceProvider; + ServiceProvider = serviceProvider; } public void Dispose() { - _serviceProvider.Dispose(); + ServiceProvider.Dispose(); } public ILogger CreateLogger(string categoryName) diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj index 686a095d822..7273b05c6c7 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj @@ -20,7 +20,6 @@ - From 2d2412e1bcfb5c72d4e8e7a7b96e44ae38546da2 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:52:09 +0100 Subject: [PATCH 03/15] Remove Json exception converter --- eng/MSBuild/Shared.props | 4 - .../Buffering/HttpRequestBuffer.cs | 4 - .../Buffering/HttpRequestBufferManager.cs | 4 - .../Buffering/IHttpRequestBufferManager.cs | 3 +- .../Logging/FakeLogger.cs | 6 +- ...soft.Extensions.Diagnostics.Testing.csproj | 1 - .../Buffering/BufferSink.cs | 10 +- .../Buffering/GlobalBuffer.cs | 4 - .../Buffering/GlobalBufferManager.cs | 3 - .../Buffering/IBufferManager.cs | 3 - .../Buffering/IBufferSink.cs | 6 +- .../Buffering/ILoggingBuffer.cs | 4 - .../Buffering/SerializedLogRecord.cs | 12 +- .../Logging/ExtendedLogger.cs | 6 - .../Microsoft.Extensions.Telemetry.csproj | 1 - .../ExceptionConverter.cs | 171 ------------------ .../Buffering/ExceptionConverterTests.cs | 89 --------- 17 files changed, 18 insertions(+), 313 deletions(-) delete mode 100644 src/Shared/JsonExceptionConverter/ExceptionConverter.cs delete mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index 599fc2bde44..a68b0e4298f 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -42,8 +42,4 @@ - - - - diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 64591cd910b..88f0b64a67f 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -4,7 +4,6 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -36,8 +35,6 @@ public HttpRequestBuffer(IBufferSink bufferSink, _truncateAfter = _timeProvider.GetUtcNow(); } - [RequiresUnreferencedCode( - "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] public bool TryEnqueue( LogLevel logLevel, string category, @@ -79,7 +76,6 @@ public bool TryEnqueue( return true; } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] public void Flush() { var result = _buffer.ToArray(); diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index 16b51bf6528..96917c87da6 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -3,7 +3,6 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Diagnostics.Logging; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -52,10 +51,8 @@ public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) return loggingBuffer; } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] public void Flush() => _globalBufferManager.Flush(); - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] public void FlushCurrentRequestLogs() { if (_httpContextAccessor.HttpContext is not null) @@ -70,7 +67,6 @@ public void FlushCurrentRequestLogs() } } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] public bool TryEnqueue( IBufferSink bufferSink, LogLevel logLevel, diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs index d237cd9cf51..b84c0ab8fb9 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// -/// Interface for a global buffer manager. +/// Interface for a HTTP request buffer manager. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public interface IHttpRequestBufferManager : IBufferManager @@ -15,7 +15,6 @@ public interface IHttpRequestBufferManager : IBufferManager /// /// Flushes the buffer and emits buffered logs for the current request. /// - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] public void FlushCurrentRequestLogs(); } #endif diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs index d3fb76cec0b..6639bb990ba 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs @@ -11,7 +11,6 @@ using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.JsonExceptionConverter; #endif using Microsoft.Shared.Diagnostics; @@ -119,7 +118,6 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except #if NET9_0_OR_GREATER /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public void LogRecords(IEnumerable records) { _ = Throw.IfNull(records); @@ -129,7 +127,9 @@ public void LogRecords(IEnumerable records) foreach (var rec in records) { #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - var exception = rec.Exception is not null ? JsonSerializer.Deserialize(rec.Exception, ExceptionConverter.JsonSerializerOptions) : null; +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + var exception = rec.Exception is not null ? JsonSerializer.Deserialize(rec.Exception) : null; +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), exception, rec.FormattedMessage ?? string.Empty, l.ToArray(), Category, !_disabledLevels.ContainsKey(rec.LogLevel), rec.Timestamp); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj index db21a9e64b9..01f3b954262 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj @@ -11,7 +11,6 @@ true true true - true $(NoWarn);SYSLIB1100;SYSLIB1101 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs index 850bbb52277..7d30adae071 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs @@ -3,13 +3,11 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; -using Microsoft.Shared.JsonExceptionConverter; using Microsoft.Shared.Pools; using static Microsoft.Extensions.Logging.ExtendedLoggerFactory; @@ -26,8 +24,6 @@ public BufferSink(ExtendedLoggerFactory factory, string category) _category = category; } - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public void LogRecords(IEnumerable serializedRecords) where T : ISerializedLogRecord { @@ -69,7 +65,11 @@ public void LogRecords(IEnumerable serializedRecords) Exception? exception = null; if (serializedRecord.Exception is not null) { - exception = JsonSerializer.Deserialize(serializedRecord.Exception, ExceptionConverter.JsonSerializerOptions); +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + exception = JsonSerializer.Deserialize(serializedRecord.Exception); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code } logger.Log( diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 57ae8da929d..aa6dfa5adbd 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -4,7 +4,6 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -29,8 +28,6 @@ public GlobalBuffer(IBufferSink bufferSink, IOptionsMonitor _bufferSink = bufferSink; } - [RequiresUnreferencedCode( - "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] public bool TryEnqueue( LogLevel logLevel, string category, @@ -62,7 +59,6 @@ public bool TryEnqueue( return true; } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] public void Flush() { var result = _buffer.ToArray(); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index 6a6cef25ce1..763e8b9518c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -3,7 +3,6 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -31,7 +30,6 @@ internal GlobalBufferManager(IOptionsMonitor options, TimeP public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) => Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferSink, _options, _timeProvider)); - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] public void Flush() { foreach (var buffer in Buffers.Values) @@ -40,7 +38,6 @@ public void Flush() } } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] public bool TryEnqueue( IBufferSink bufferSink, LogLevel logLevel, diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs index 2905c9b269c..a06128ccf2a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs @@ -17,7 +17,6 @@ public interface IBufferManager /// /// Flushes the buffer and emits all buffered logs. /// - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")] void Flush(); /// @@ -32,8 +31,6 @@ public interface IBufferManager /// Formatter delegate. /// Type of the instance. /// if the log record was buffered; otherwise, . - [RequiresUnreferencedCode( - "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] bool TryEnqueue( IBufferSink bufferSink, LogLevel logLevel, diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs index 757f9d2b111..3a39659194f 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs @@ -8,17 +8,17 @@ namespace Microsoft.Extensions.Logging; /// -/// Interface for a buffer sink. +/// Represents a sink for buffered log records of all categories which can be forwarded +/// to all currently registered loggers. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public interface IBufferSink { /// - /// Forward the to all currently registered loggers. + /// Forwards the to all currently registered loggers. /// /// serialized log records. /// Type of the log records. - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] void LogRecords(IEnumerable serializedRecords) where T : ISerializedLogRecord; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index a25d249d5d7..d8367954852 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -3,7 +3,6 @@ #if NET9_0_OR_GREATER using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Logging; @@ -24,8 +23,6 @@ internal interface ILoggingBuffer /// Formatter delegate. /// Type of the instance. /// if the log record was buffered; otherwise, . - [RequiresUnreferencedCode( - "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")] bool TryEnqueue( LogLevel logLevel, string category, @@ -37,7 +34,6 @@ bool TryEnqueue( /// /// Flushes the buffer. /// - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")] void Flush(); /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index 7aa9df3056f..edb2cec7b1a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -4,17 +4,13 @@ #if NET9_0_OR_GREATER using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.Extensions.Logging; -using Microsoft.Shared.JsonExceptionConverter; namespace Microsoft.Extensions.Logging; internal readonly struct SerializedLogRecord : ISerializedLogRecord { - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public SerializedLogRecord( LogLevel logLevel, EventId eventId, @@ -35,8 +31,12 @@ public SerializedLogRecord( Attributes = serializedAttributes; - // Serialize without StackTrace, whis is already optionally available in the log attributes via the ExtendedLogger. - Exception = JsonSerializer.Serialize(exception, ExceptionConverter.JsonSerializerOptions); + // Serialize without StackTrace, which is already optionally available in the log attributes via the ExtendedLogger. +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + Exception = JsonSerializer.Serialize(exception); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code FormattedMessage = formattedMessage; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index 18675854551..7a542350bf4 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; #if NET9_0_OR_GREATER using Microsoft.Extensions.Diagnostics.Buffering; #endif @@ -60,10 +59,7 @@ public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers } #endif - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ExtendedLogger.ModernPath(LogLevel, EventId, LoggerMessageState, Exception, Func)")] -#pragma warning disable IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) -#pragma warning restore IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. { if (typeof(TState) == typeof(LoggerMessageState)) { @@ -231,7 +227,6 @@ void HandleException(Exception exception, int indent) } } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState msgState, Exception? exception, Func formatter) { var loggers = MessageLoggers; @@ -350,7 +345,6 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m HandleExceptions(exceptions); } - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")] private void LegacyPath(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { var loggers = MessageLoggers; diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index fd344853ab2..f303698ed82 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -16,7 +16,6 @@ true true true - true true true diff --git a/src/Shared/JsonExceptionConverter/ExceptionConverter.cs b/src/Shared/JsonExceptionConverter/ExceptionConverter.cs deleted file mode 100644 index 7b62eb81528..00000000000 --- a/src/Shared/JsonExceptionConverter/ExceptionConverter.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; - -#pragma warning disable CA1716 -namespace Microsoft.Shared.JsonExceptionConverter; -#pragma warning restore CA1716 - -internal sealed class ExceptionConverter : JsonConverter -{ -#pragma warning disable CA2201 // Do not raise reserved exception types - private static Exception _failedDeserialization = new Exception("Failed to deserialize the exception object."); -#pragma warning restore CA2201 // Do not raise reserved exception types - - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] - public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - return _failedDeserialization; - } - - return DeserializeException(ref reader); - } - - public override void Write(Utf8JsonWriter writer, Exception exception, JsonSerializerOptions options) - { - HandleException(writer, exception); - } - - public static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - Converters = { new ExceptionConverter() } - }; - - public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); - - private static Exception DeserializeException(ref Utf8JsonReader reader) - { - string? type = null; - string? message = null; - string? source = null; - Exception? innerException = null; - List? innerExceptions = null; - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.PropertyName) - { - string? propertyName = reader.GetString(); - _ = reader.Read(); - - switch (propertyName) - { - case "Type": - type = reader.GetString(); - break; - case "Message": - message = reader.GetString(); - break; - case "Source": - source = reader.GetString(); - break; - case "InnerException": - if (reader.TokenType == JsonTokenType.StartObject) - { - innerException = DeserializeException(ref reader); - } - - break; - case "InnerExceptions": - innerExceptions = new List(); - if (reader.TokenType == JsonTokenType.StartArray) - { - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.StartObject) - { - var innerEx = DeserializeException(ref reader); - innerExceptions.Add(innerEx); - } - } - } - - break; - } - } - else if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - } - - if (type is null) - { - return _failedDeserialization; - } - -#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - Type? deserializedType = Type.GetType(type); -#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - if (deserializedType is null) - { - return _failedDeserialization; - } - - Exception? exception; - - if (innerExceptions != null && innerExceptions.Count > 0) - { - if (deserializedType == typeof(AggregateException)) - { - exception = new AggregateException(message, innerExceptions); - } - else - { - exception = Activator.CreateInstance(deserializedType, message, innerExceptions.First()) as Exception; - } - } - else if (innerException != null) - { - exception = Activator.CreateInstance(deserializedType, message, innerException) as Exception; - } - else - { - exception = Activator.CreateInstance(deserializedType, message) as Exception; - } - - if (exception == null) - { - return _failedDeserialization; - } - - exception.Source = source; - - return exception; - } - - private static void HandleException(Utf8JsonWriter writer, Exception exception) - { - writer.WriteStartObject(); - writer.WriteString("Type", exception.GetType().FullName); - writer.WriteString("Message", exception.Message); - writer.WriteString("Source", exception.Source); - if (exception is AggregateException aggregateException) - { - writer.WritePropertyName("InnerExceptions"); - writer.WriteStartArray(); - foreach (var ex in aggregateException.InnerExceptions) - { - HandleException(writer, ex); - } - - writer.WriteEndArray(); - } - else if (exception.InnerException != null) - { - writer.WritePropertyName("InnerException"); - HandleException(writer, exception.InnerException); - } - - writer.WriteEndObject(); - } -} -#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs deleted file mode 100644 index 8233f29a322..00000000000 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/ExceptionConverterTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.Shared.JsonExceptionConverter; -using Xunit; - -namespace Microsoft.Extensions.Logging.Test; - -public class ExceptionConverterTests -{ - private static readonly JsonSerializerOptions _options = new() - { - Converters = { new ExceptionConverter() } - }; - - [Fact] - public void SerializeAndDeserialize_SimpleException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new ExceptionConverter()); - - // Arrange - var exception = new InvalidOperationException("Test exception"); - - // Act - var json = JsonSerializer.Serialize(exception, options); - var deserializedException = JsonSerializer.Deserialize(json, options); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Equal(exception.Message, deserializedException.Message); - } - - [Fact] - public void SerializeAndDeserialize_ExceptionWithInnerException() - { - // Arrange - var innerException = new ArgumentNullException("paramName", "Inner exception message"); - var exception = new InvalidOperationException("Test exception with inner exception", innerException); - - // Act - var json = JsonSerializer.Serialize(exception, _options); - var deserializedException = JsonSerializer.Deserialize(json, _options); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Equal(exception.Message, deserializedException.Message); - - Assert.NotNull(deserializedException.InnerException); - Assert.IsType(deserializedException.InnerException); - Assert.Contains(innerException.Message, deserializedException.InnerException.Message); - } - - [Fact] - public void SerializeAndDeserialize_AggregateException() - { - // Arrange - var innerException1 = new ArgumentException("First inner exception"); -#pragma warning disable CA2201 // Do not raise reserved exception types - var innerException2 = new NullReferenceException("Second inner exception"); -#pragma warning restore CA2201 // Do not raise reserved exception types - var exception = new AggregateException("Aggregate exception message", innerException1, innerException2); - - // Act - var json = JsonSerializer.Serialize(exception, _options); - var deserializedException = JsonSerializer.Deserialize(json, _options); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Contains(exception.Message, deserializedException.Message); - - var aggregateException = (AggregateException)deserializedException; - Assert.NotNull(aggregateException.InnerExceptions); - Assert.Equal(2, aggregateException.InnerExceptions.Count); - - Assert.IsType(aggregateException.InnerExceptions[0]); - Assert.Equal(innerException1.Message, aggregateException.InnerExceptions[0].Message); - - Assert.IsType(aggregateException.InnerExceptions[1]); - Assert.Equal(innerException2.Message, aggregateException.InnerExceptions[1].Message); - } -} -#endif From f7eaab11e758ef52c524429ffef6f088eddaf89e Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:32:57 +0100 Subject: [PATCH 04/15] Fix namespaces Address PR comments Add .NET 8 support Add ExceptionJsonConverter --- eng/MSBuild/Shared.props | 4 + .../Buffering/HttpRequestBuffer.cs | 6 +- .../HttpRequestBufferConfigureOptions.cs | 4 +- ...ttpRequestBufferLoggerBuilderExtensions.cs | 6 +- .../Buffering/HttpRequestBufferManager.cs | 9 +- .../Buffering/HttpRequestBufferOptions.cs | 6 +- .../Buffering/IHttpRequestBufferManager.cs | 8 +- ...t.AspNetCore.Diagnostics.Middleware.csproj | 1 + .../Logging/FakeLogger.cs | 18 +- ...soft.Extensions.Diagnostics.Testing.csproj | 7 +- ...icrosoft.Extensions.Hosting.Testing.csproj | 1 + ...crosoft.Extensions.Http.Diagnostics.csproj | 1 + ...icrosoft.Extensions.Http.Resilience.csproj | 1 + .../Buffering/BufferFilterRule.cs | 5 +- .../Buffering/BufferSink.cs | 16 +- .../Buffering/GlobalBuffer.cs | 19 +- .../Buffering/GlobalBufferConfigureOptions.cs | 5 +- .../GlobalBufferLoggerBuilderExtensions.cs | 2 - .../Buffering/GlobalBufferManager.cs | 5 +- .../Buffering/GlobalBufferOptions.cs | 6 +- .../Buffering/IBufferManager.cs | 3 +- .../Buffering/IBufferSink.cs | 5 +- .../Buffering/ILoggingBuffer.cs | 4 +- .../Buffering/ISerializedLogRecord.cs | 3 +- .../Buffering/PooledLogRecord.cs | 3 +- .../Buffering/SerializedLogRecord.cs | 16 +- .../ILoggerFilterRule.cs | 4 +- .../LoggerFilterRuleSelector.cs | 174 +++++++++--------- .../Logging/ExtendedLogger.cs | 71 +++---- .../Logging/ExtendedLoggerFactory.cs | 14 -- .../Logging/LoggerConfig.cs | 10 - .../Microsoft.Extensions.Telemetry.csproj | 7 +- .../ExceptionJsonContext.cs | 15 ++ .../ExceptionJsonConverter.cs | 163 ++++++++++++++++ src/Shared/Shared.csproj | 1 + ...questBufferLoggerBuilderExtensionsTests.cs | 4 +- .../Logging/AcceptanceTests.cs | 17 +- .../Microsoft.Extensions.AI.Tests.csproj | 1 + ...Extensions.Diagnostics.Probes.Tests.csproj | 1 + ...t.Extensions.Http.Diagnostics.Tests.csproj | 1 + ...Extensions.Options.Contextual.Tests.csproj | 1 + ...crosoft.Extensions.Resilience.Tests.csproj | 1 + ...lobalBufferLoggerBuilderExtensionsTests.cs | 3 - .../Logging/ExtendedLoggerTests.cs | 6 +- ...icrosoft.Extensions.Telemetry.Tests.csproj | 1 + .../ExceptionJsonConverterTests.cs | 78 ++++++++ 46 files changed, 466 insertions(+), 271 deletions(-) create mode 100644 src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs create mode 100644 src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs create mode 100644 test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index a68b0e4298f..40cffd86fa5 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -42,4 +42,8 @@ + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 88f0b64a67f..7efd2706e5f 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using Microsoft.Extensions.Diagnostics; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; using static Microsoft.Extensions.Logging.ExtendedLogger; -namespace Microsoft.AspNetCore.Diagnostics.Logging; +namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class HttpRequestBuffer : ILoggingBuffer { @@ -107,4 +106,3 @@ public void TruncateOverlimit() } } } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs index a30a489b6bb..81cc67d4e22 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Diagnostics.Logging; +namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions { @@ -40,4 +39,3 @@ public void Configure(HttpRequestBufferOptions options) options.Rules.AddRange(parsedOptions.Rules); } } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs index b3c4f94f786..dd82ee079b8 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Diagnostics.Logging; +using Microsoft.AspNetCore.Diagnostics.Buffering; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -44,7 +43,7 @@ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder build } /// - /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in + /// Adds HTTP request-aware buffering to the logging infrastructure. Matched logs will be buffered in /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. /// /// The . @@ -104,4 +103,3 @@ internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingB return builder; } } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index 96917c87da6..578364277f8 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -1,14 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Diagnostics.Logging; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.Diagnostics.Buffering; +namespace Microsoft.AspNetCore.Diagnostics.Buffering; + internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager { private readonly GlobalBufferManager _globalBufferManager; @@ -80,4 +80,3 @@ public bool TryEnqueue( return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); } } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs index cc266422026..a61f5c8b3cf 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs @@ -1,14 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.AspNetCore.Diagnostics.Logging; +namespace Microsoft.AspNetCore.Diagnostics.Buffering; /// /// The options for LoggerBuffer. @@ -35,4 +34,3 @@ public class HttpRequestBufferOptions #pragma warning restore CA2227 // Collection properties should be read only #pragma warning restore CA1002 // Do not expose generic lists } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs index b84c0ab8fb9..4bc77040fbe 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Diagnostics.Buffering; +namespace Microsoft.AspNetCore.Diagnostics.Buffering; /// -/// Interface for a HTTP request buffer manager. +/// Interface for an HTTP request buffer manager. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public interface IHttpRequestBufferManager : IBufferManager @@ -17,4 +18,3 @@ public interface IHttpRequestBufferManager : IBufferManager /// public void FlushCurrentRequestLogs(); } -#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj index 238b4364d5d..75b3604fc63 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj @@ -8,6 +8,7 @@ $(NetCoreTargetFrameworks) + true true true false diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs index 6639bb990ba..04758493df0 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs @@ -5,14 +5,13 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; -#if NET9_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; -#endif using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.ExceptionJsonConverter; namespace Microsoft.Extensions.Logging.Testing; @@ -23,11 +22,7 @@ namespace Microsoft.Extensions.Logging.Testing; /// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it /// to validate that your code is logging what it should. /// -#if NET9_0_OR_GREATER public class FakeLogger : ILogger, IBufferedLogger -#else -public class FakeLogger : ILogger -#endif { private readonly ConcurrentDictionary _disabledLevels = new(); // used as a set, the value is ignored @@ -115,7 +110,6 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// public string? Category { get; } -#if NET9_0_OR_GREATER /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public void LogRecords(IEnumerable records) @@ -126,17 +120,13 @@ public void LogRecords(IEnumerable records) foreach (var rec in records) { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - var exception = rec.Exception is not null ? JsonSerializer.Deserialize(rec.Exception) : null; -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + var exception = rec.Exception is null ? + null : JsonSerializer.Deserialize(rec.Exception, ExceptionJsonContext.Default.Exception); var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), exception, rec.FormattedMessage ?? string.Empty, l.ToArray(), Category, !_disabledLevels.ContainsKey(rec.LogLevel), rec.Timestamp); Collector.AddRecord(record); } } -#endif internal IExternalScopeProvider ScopeProvider { get; set; } = new LoggerExternalScopeProvider(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj index 01f3b954262..690755328d5 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj @@ -1,4 +1,4 @@ - + Microsoft.Extensions.Diagnostics.Testing Hand-crafted fakes to make telemetry-related testing easier. @@ -7,11 +7,13 @@ + true true true true true - $(NoWarn);SYSLIB1100;SYSLIB1101 + true + $(NoWarn);IL2026;SYSLIB1100;SYSLIB1101 @@ -29,6 +31,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj index fdc40c84838..140d55aa8c9 100644 --- a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj @@ -7,6 +7,7 @@ + true true true true diff --git a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj index e6914c42dff..8712bd005f6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Diagnostics/Microsoft.Extensions.Http.Diagnostics.csproj @@ -9,6 +9,7 @@ $(NoWarn);LA0006 + true true true true diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index f0499dada26..4248d7ac931 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -6,6 +6,7 @@ + true true true true diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs index fd8a070fec5..b69d133a3f6 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Defines a rule used to filter log messages for purposes of futher buffering. @@ -45,4 +43,3 @@ public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId) /// public int? EventId { get; set; } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs index 7d30adae071..9b1708bf55a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs @@ -1,17 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; -using Microsoft.Extensions.Diagnostics.Buffering; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.ExceptionJsonConverter; using Microsoft.Shared.Pools; -using static Microsoft.Extensions.Logging.ExtendedLoggerFactory; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; + internal sealed class BufferSink : IBufferSink { private readonly ExtendedLoggerFactory _factory; @@ -65,11 +66,7 @@ public void LogRecords(IEnumerable serializedRecords) Exception? exception = null; if (serializedRecord.Exception is not null) { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - exception = JsonSerializer.Deserialize(serializedRecord.Exception); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + exception = JsonSerializer.Deserialize(serializedRecord.Exception, ExceptionJsonContext.Default.Exception); } logger.Log( @@ -91,4 +88,3 @@ public void LogRecords(IEnumerable serializedRecords) } } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index aa6dfa5adbd..6873a63cd00 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -1,16 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Concurrent; -using Microsoft.Extensions.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; using static Microsoft.Extensions.Logging.ExtendedLogger; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalBuffer : ILoggingBuffer { @@ -19,6 +17,9 @@ internal sealed class GlobalBuffer : ILoggingBuffer private readonly IBufferSink _bufferSink; private readonly TimeProvider _timeProvider; private DateTimeOffset _lastFlushTimestamp; +#if NETFRAMEWORK + private object _netfxBufferLocker = new(); +#endif public GlobalBuffer(IBufferSink bufferSink, IOptionsMonitor options, TimeProvider timeProvider) { @@ -62,7 +63,18 @@ public bool TryEnqueue( public void Flush() { var result = _buffer.ToArray(); + +#if NETFRAMEWORK + lock (_netfxBufferLocker) + { + while (_buffer.TryDequeue(out _)) + { + // Clear the buffer + } + } +#else _buffer.Clear(); +#endif _lastFlushTimestamp = _timeProvider.GetUtcNow(); @@ -90,4 +102,3 @@ private bool IsEnabled(string category, LogLevel logLevel, EventId eventId) return rule is not null; } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs index 8b6bd724873..d847f8a59d9 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER -using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalBufferConfigureOptions : IConfigureOptions { @@ -40,4 +38,3 @@ public void Configure(GlobalBufferOptions options) options.Rules.AddRange(parsedOptions.Rules); } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs index c4800c05af3..c553b9cd472 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; @@ -94,4 +93,3 @@ internal static ILoggingBuilder AddGlobalBufferConfiguration(this ILoggingBuilde return builder; } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index 763e8b9518c..549d641afa9 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System; using System.Collections.Concurrent; using System.Threading; @@ -10,6 +10,7 @@ using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.Buffering; + internal sealed class GlobalBufferManager : BackgroundService, IBufferManager { internal readonly ConcurrentDictionary Buffers = []; @@ -61,6 +62,4 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) } } } - } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs index 19009fb9a0d..22df6eb3de2 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// The options for LoggerBuffer. @@ -16,7 +15,7 @@ namespace Microsoft.Extensions.Logging; public class GlobalBufferOptions { /// - /// Gets or sets the time to suspend the buffer after flushing. + /// Gets or sets the time to suspend the buffering after flushing. /// /// /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, @@ -43,4 +42,3 @@ public class GlobalBufferOptions #pragma warning restore CA2227 // Collection properties should be read only #pragma warning restore CA1002 // Do not expose generic lists } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs index a06128ccf2a..42024e13d1f 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -40,4 +40,3 @@ bool TryEnqueue( Exception? exception, Func formatter); } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs index 3a39659194f..812f5f02906 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Represents a sink for buffered log records of all categories which can be forwarded @@ -22,4 +22,3 @@ public interface IBufferSink void LogRecords(IEnumerable serializedRecords) where T : ISerializedLogRecord; } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index d8367954852..e8a642cf6de 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using Microsoft.Extensions.Logging; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Interface for a logging buffer. @@ -41,4 +40,3 @@ bool TryEnqueue( /// void TruncateOverlimit(); } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs index 23f03323893..672f11266f3 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs @@ -4,9 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Represents a buffered log record that has been serialized. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs index e20bc1069a3..41776f37fc5 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER + using System; using System.Collections.Generic; using System.Diagnostics; @@ -73,4 +73,3 @@ public bool TryReset() return true; } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index edb2cec7b1a..b122f859852 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -1,13 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Generic; using System.Text.Json; using Microsoft.Extensions.Logging; +using Microsoft.Shared.ExceptionJsonConverter; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; internal readonly struct SerializedLogRecord : ISerializedLogRecord { @@ -26,17 +26,18 @@ public SerializedLogRecord( var serializedAttributes = new List>(attributes.Count); for (int i = 0; i < attributes.Count; i++) { +#if NETFRAMEWORK + serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key.ToCharArray()), attributes[i].Value?.ToString() ?? string.Empty)); +#else serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); +#endif } Attributes = serializedAttributes; // Serialize without StackTrace, which is already optionally available in the log attributes via the ExtendedLogger. -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code -#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. - Exception = JsonSerializer.Serialize(exception); -#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + Exception = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); + FormattedMessage = formattedMessage; } @@ -50,4 +51,3 @@ public SerializedLogRecord( public EventId EventId { get; } } -#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs index 2d7e40bc321..9dd56a1da1c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.Diagnostics; +namespace Microsoft.Extensions.Logging; /// /// Represents a rule used for filtering log messages for purposes of log sampling and buffering. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs index 6864c38f6fb..a9f739cadfd 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs @@ -7,125 +7,123 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Logging; -namespace Microsoft.Extensions.Diagnostics +namespace Microsoft.Extensions.Logging; + +internal static class LoggerFilterRuleSelector { - internal static class LoggerFilterRuleSelector + public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, + out T? bestRule) + where T : class, ILoggerFilterRule { - public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, - out T? bestRule) - where T : class, ILoggerFilterRule + bestRule = null; + + // TO DO: update the comment and logic + // Filter rule selection: + // 1. Select rules with longest matching categories + // 2. If there is nothing matched by category take all rules without category + // 3. If there is only one rule use it + // 4. If there are multiple rules use last + + T? current = null; + foreach (T rule in rules) { - bestRule = null; + if (IsBetter(rule, current, category, logLevel, eventId)) + { + current = rule; + } + } + + if (current != null) + { + bestRule = current; + } + } + + private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId) + where T : class, ILoggerFilterRule + { + // Skip rules with inapplicable log level + if (rule.LogLevel != null && rule.LogLevel < logLevel) + { + return false; + } + + // Skip rules with inapplicable event id + if (rule.EventId != null && rule.EventId != eventId) + { + return false; + } - // TO DO: update the comment and logic - // Filter rule selection: - // 1. Select rules with longest matching categories - // 2. If there is nothing matched by category take all rules without category - // 3. If there is only one rule use it - // 4. If there are multiple rules use last + // Skip rules with inapplicable category + string? categoryName = rule.Category; + if (categoryName != null) + { + const char WildcardChar = '*'; - T? current = null; - foreach (T rule in rules) + int wildcardIndex = categoryName.IndexOf(WildcardChar); + if (wildcardIndex != -1 && + categoryName.IndexOf(WildcardChar, wildcardIndex + 1) != -1) { - if (IsBetter(rule, current, category, logLevel, eventId)) - { - current = rule; - } + throw new InvalidOperationException("Only one wildcard character is allowed in category name."); } - if (current != null) + ReadOnlySpan prefix, suffix; + if (wildcardIndex == -1) { - bestRule = current; + prefix = categoryName.AsSpan(); + suffix = default; + } + else + { + prefix = categoryName.AsSpan(0, wildcardIndex); + suffix = categoryName.AsSpan(wildcardIndex + 1); } - } - private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId) - where T : class, ILoggerFilterRule - { - // Skip rules with inapplicable log level - if (rule.LogLevel != null && rule.LogLevel < logLevel) + if (!category.AsSpan().StartsWith(prefix, StringComparison.OrdinalIgnoreCase) || + !category.AsSpan().EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) { return false; } + } - // Skip rules with inapplicable event id - if (rule.EventId != null && rule.EventId != eventId) + // Decide whose category is better - rule vs current + if (current?.Category != null) + { + if (rule.Category == null) { return false; } - // Skip rules with inapplicable category - string? categoryName = rule.Category; - if (categoryName != null) + if (current.Category.Length > rule.Category.Length) { - const char WildcardChar = '*'; - - int wildcardIndex = categoryName.IndexOf(WildcardChar); - if (wildcardIndex != -1 && - categoryName.IndexOf(WildcardChar, wildcardIndex + 1) != -1) - { - throw new InvalidOperationException("Only one wildcard character is allowed in category name."); - } - - ReadOnlySpan prefix, suffix; - if (wildcardIndex == -1) - { - prefix = categoryName.AsSpan(); - suffix = default; - } - else - { - prefix = categoryName.AsSpan(0, wildcardIndex); - suffix = categoryName.AsSpan(wildcardIndex + 1); - } - - if (!category.AsSpan().StartsWith(prefix, StringComparison.OrdinalIgnoreCase) || - !category.AsSpan().EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + return false; } + } - // Decide whose category is better - rule vs current - if (current?.Category != null) + // Decide whose log level is better - rule vs current + if (current?.LogLevel != null) + { + if (rule.LogLevel == null) { - if (rule.Category == null) - { - return false; - } - - if (current.Category.Length > rule.Category.Length) - { - return false; - } + return false; } - // Decide whose log level is better - rule vs current - if (current?.LogLevel != null) + if (current.LogLevel < rule.LogLevel) { - if (rule.LogLevel == null) - { - return false; - } - - if (current.LogLevel < rule.LogLevel) - { - return false; - } + return false; } + } - // Decide whose event id is better - rule vs current - if (rule.EventId is null) + // Decide whose event id is better - rule vs current + if (rule.EventId is null) + { + if (current?.EventId != null) { - if (current?.EventId != null) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index 7a542350bf4..2d8a110116a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -4,16 +4,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; -#if NET9_0_OR_GREATER using Microsoft.Extensions.Diagnostics.Buffering; -#endif using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Logging; #pragma warning disable CA1031 -// NOTE: This implementation uses thread local storage. As a result, it will fail if formatter code, enricher code, or +// NOTE: This implementation uses thread local storage. As a wasBuffered, it will fail if formatter code, enricher code, or // redactor code calls recursively back into the logger. Don't do that. // // NOTE: Unlike the original logger in dotnet/runtime, this logger eats exceptions thrown from invoked loggers, enrichers, @@ -33,7 +31,6 @@ internal sealed partial class ExtendedLogger : ILogger public MessageLogger[] MessageLoggers { get; set; } = Array.Empty(); public ScopeLogger[] ScopeLoggers { get; set; } = Array.Empty(); -#if NET9_0_OR_GREATER private readonly IBufferManager? _bufferManager; private readonly IBufferSink? _bufferSink; @@ -51,14 +48,6 @@ public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers } } -#else - public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers) - { - _factory = factory; - Loggers = loggers; - } -#endif - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (typeof(TState) == typeof(LoggerMessageState)) @@ -283,38 +272,33 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m RecordException(exception, joiner.EnrichmentTagCollector, config); } -#if NET9_0_OR_GREATER - bool? shouldBuffer = null; -#endif + bool shouldBuffer = true; for (int i = 0; i < loggers.Length; i++) { ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { -#if NET9_0_OR_GREATER - if (shouldBuffer is null or true) + if (shouldBuffer) { if (_bufferManager is not null) { - var result = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + var wasBuffered = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = s.Formatter!; return fmt(s.State!, e); }); - shouldBuffer = result; - // The record was buffered, so we skip logging it for now. - // When a caller needs to flush the buffer and calls IBufferManager.Flush(), - // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. - continue; - - } - else - { - shouldBuffer = false; + if (wasBuffered) + { + // The record was buffered, so we skip logging it here and for all other loggers. + // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. + break; + } } + + shouldBuffer = false; } -#endif try { @@ -394,38 +378,35 @@ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state { RecordException(exception, joiner.EnrichmentTagCollector, config); } -#if NET9_0_OR_GREATER - bool? shouldBuffer = null; -#endif + + bool shouldBuffer = true; for (int i = 0; i < loggers.Length; i++) { ref readonly MessageLogger loggerInfo = ref loggers[i]; if (loggerInfo.IsNotFilteredOut(logLevel)) { -#if NET9_0_OR_GREATER - if (shouldBuffer is null or true) + if (shouldBuffer) { if (_bufferManager is not null) { - var result = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + bool wasBuffered = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = (Func)s.Formatter!; return fmt((TState)s.State!, e); }); - shouldBuffer = result; - // The record was buffered, so we skip logging it for now. - // When a caller needs to flush the buffer and calls IBufferManager.Flush(), - // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. - continue; + if (wasBuffered) + { + // The record was buffered, so we skip logging it here and for all other loggers. + // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. + break; + } } - else - { - shouldBuffer = false; - } + + shouldBuffer = false; } -#endif try { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 8d2b4923d4c..4f2d8376f88 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -7,9 +7,7 @@ using System.Linq; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; -#if NET9_0_OR_GREATER using Microsoft.Extensions.Diagnostics.Buffering; -#endif using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; @@ -26,9 +24,7 @@ internal sealed class ExtendedLoggerFactory : ILoggerFactory private readonly IDisposable? _enrichmentOptionsChangeTokenRegistration; private readonly IDisposable? _redactionOptionsChangeTokenRegistration; private readonly Action[] _enrichers; -#if NET9_0_OR_GREATER private readonly IBufferManager? _bufferManager; -#endif private readonly KeyValuePair[] _staticTags; private readonly Func _redactorProvider; private volatile bool _disposed; @@ -46,18 +42,12 @@ public ExtendedLoggerFactory( IExternalScopeProvider? scopeProvider = null, IOptionsMonitor? enrichmentOptions = null, IOptionsMonitor? redactionOptions = null, -#if NET9_0_OR_GREATER IRedactorProvider? redactorProvider = null, IBufferManager? bufferManager = null) -#else - IRedactorProvider? redactorProvider = null) -#endif #pragma warning restore S107 // Methods should not have too many parameters { _scopeProvider = scopeProvider; -#if NET9_0_OR_GREATER _bufferManager = bufferManager; -#endif _factoryOptions = factoryOptions == null || factoryOptions.Value == null ? new LoggerFactoryOptions() : factoryOptions.Value; @@ -304,12 +294,8 @@ private LoggerConfig ComputeConfig(LoggerEnrichmentOptions? enrichmentOptions, L enrichmentOptions.IncludeExceptionMessage, enrichmentOptions.MaxStackTraceLength, _redactorProvider, -#if NET9_0_OR_GREATER redactionOptions.ApplyDiscriminator, _bufferManager); -#else - redactionOptions.ApplyDiscriminator); -#endif } private void UpdateEnrichmentOptions(LoggerEnrichmentOptions enrichmentOptions) => Config = ComputeConfig(enrichmentOptions, null); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs index c84ba772e86..1870049c38c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; -#if NET9_0_OR_GREATER using Microsoft.Extensions.Diagnostics.Buffering; -#endif using Microsoft.Extensions.Diagnostics.Enrichment; namespace Microsoft.Extensions.Logging; @@ -23,12 +21,8 @@ public LoggerConfig( bool includeExceptionMessage, int maxStackTraceLength, Func getRedactor, -#if NET9_0_OR_GREATER bool addRedactionDiscriminator, IBufferManager? bufferManager) -#else - bool addRedactionDiscriminator) -#endif { #pragma warning restore S107 // Methods should not have too many parameters StaticTags = staticTags; @@ -39,9 +33,7 @@ public LoggerConfig( IncludeExceptionMessage = includeExceptionMessage; GetRedactor = getRedactor; AddRedactionDiscriminator = addRedactionDiscriminator; -#if NET9_0_OR_GREATER BufferManager = bufferManager; -#endif } public KeyValuePair[] StaticTags { get; } @@ -52,7 +44,5 @@ public LoggerConfig( public int MaxStackTraceLength { get; } public Func GetRedactor { get; } public bool AddRedactionDiscriminator { get; } -#if NET9_0_OR_GREATER public IBufferManager? BufferManager { get; } -#endif } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index f303698ed82..5d3c484569b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -6,6 +6,7 @@ + true true true true @@ -16,9 +17,11 @@ true true true + true true true - + $(NoWarn);IL2026 + normal @@ -34,8 +37,10 @@ + + diff --git a/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs b/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs new file mode 100644 index 00000000000..d9ee1bf01a3 --- /dev/null +++ b/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs @@ -0,0 +1,15 @@ +// 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.Text.Json.Serialization; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.ExceptionJsonConverter; +#pragma warning restore CA1716 + +[JsonSerializable(typeof(Exception))] +[JsonSourceGenerationOptions(Converters = new Type[] { typeof(ExceptionJsonConverter) })] +internal sealed partial class ExceptionJsonContext : JsonSerializerContext +{ +} diff --git a/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs b/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs new file mode 100644 index 00000000000..d272c55f2a8 --- /dev/null +++ b/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs @@ -0,0 +1,163 @@ +// 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.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.ExceptionJsonConverter; +#pragma warning restore CA1716 + +internal sealed class ExceptionJsonConverter : JsonConverter +{ +#pragma warning disable CA2201 // Do not raise reserved exception types + private static Exception _failedDeserialization = new Exception("Failed to deserialize the exception object."); +#pragma warning restore CA2201 // Do not raise reserved exception types + + public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + return _failedDeserialization; + } + + return DeserializeException(ref reader); + } + + public override void Write(Utf8JsonWriter writer, Exception exception, JsonSerializerOptions options) + { + HandleException(writer, exception); + } + + public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); + + private static Exception DeserializeException(ref Utf8JsonReader reader) + { + string? type = null; + string? message = null; + string? source = null; + Exception? innerException = null; + List? innerExceptions = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + _ = reader.Read(); + + switch (propertyName) + { + case "Type": + type = reader.GetString(); + break; + case "Message": + message = reader.GetString(); + break; + case "Source": + source = reader.GetString(); + break; + case "InnerException": + if (reader.TokenType == JsonTokenType.StartObject) + { + innerException = DeserializeException(ref reader); + } + + break; + case "InnerExceptions": + innerExceptions = new List(); + if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.StartObject) + { + var innerEx = DeserializeException(ref reader); + innerExceptions.Add(innerEx); + } + } + } + + break; + } + } + else if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + } + + if (type is null) + { + return _failedDeserialization; + } + +#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + Type? deserializedType = Type.GetType(type); +#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. + if (deserializedType is null) + { + return _failedDeserialization; + } + + Exception? exception; + + if (innerExceptions != null && innerExceptions.Count > 0) + { + if (deserializedType == typeof(AggregateException)) + { + exception = new AggregateException(message, innerExceptions); + } + else + { + exception = Activator.CreateInstance(deserializedType, message, innerExceptions.First()) as Exception; + } + } + else if (innerException != null) + { + exception = Activator.CreateInstance(deserializedType, message, innerException) as Exception; + } + else + { + exception = Activator.CreateInstance(deserializedType, message) as Exception; + } + + if (exception == null) + { + return _failedDeserialization; + } + + exception.Source = source; + + return exception; + } + + private static void HandleException(Utf8JsonWriter writer, Exception exception) + { + writer.WriteStartObject(); + writer.WriteString("Type", exception.GetType().FullName); + writer.WriteString("Message", exception.Message); + writer.WriteString("Source", exception.Source); + if (exception is AggregateException aggregateException) + { + writer.WritePropertyName("InnerExceptions"); + writer.WriteStartArray(); + foreach (var ex in aggregateException.InnerExceptions) + { + HandleException(writer, ex); + } + + writer.WriteEndArray(); + } + else if (exception.InnerException != null) + { + writer.WritePropertyName("InnerException"); + HandleException(writer, exception.InnerException); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 439c3788557..e35848d918c 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -6,6 +6,7 @@ + $(NoWarn);IL2026 $(NetCoreTargetFrameworks)$(ConditionalNet462) false $(DefineConstants);SHARED_PROJECT diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs index 76162297cf7..04e57bb24f5 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Generic; using Microsoft.Extensions.Configuration; @@ -11,7 +10,7 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Diagnostics.Logging.Test; +namespace Microsoft.AspNetCore.Diagnostics.Buffering.Test; public class HttpRequestBufferLoggerBuilderExtensionsTests { @@ -64,4 +63,3 @@ public void AddHttpRequestBufferConfiguration_RegistersInDI() Assert.Equivalent(expectedData, options.CurrentValue.Rules); } } -#endif diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index 5bb7f99cc43..07c0c9099e4 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -4,7 +4,6 @@ #if NET8_0_OR_GREATER using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Http; @@ -12,6 +11,7 @@ using System.Net.Mime; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.Buffering; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpLogging; @@ -25,9 +25,6 @@ using Microsoft.Extensions.Http.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -#if NET9_0_OR_GREATER -using Microsoft.Extensions.Diagnostics.Buffering; -#endif using Microsoft.Extensions.Time.Testing; using Microsoft.Net.Http.Headers; using Microsoft.Shared.Text; @@ -59,7 +56,6 @@ public static void Configure(IApplicationBuilder app) app.UseRouting(); app.UseHttpLogging(); -#if NET9_0_OR_GREATER app.Map("/flushrequestlogs", static x => x.Run(static async context => { @@ -83,7 +79,7 @@ public static void Configure(IApplicationBuilder app) bufferManager.Flush(); } })); -#endif + app.Map("/error", static x => x.Run(static async context => { @@ -743,7 +739,6 @@ await RunAsync( }); } -#if NET9_0_OR_GREATER [Fact] public async Task HttpRequestBuffering() { @@ -753,9 +748,14 @@ await RunAsync( .AddLogging(builder => { // enable Microsoft.AspNetCore.Routing.Matching.DfaMatcher debug logs - // which we are produced by ASP.NET Core within HTTP context: + // which are produced by ASP.NET Core within HTTP context. + // This is what is going to be buffered and tested. builder.AddFilter("Microsoft.AspNetCore.Routing.Matching.DfaMatcher", LogLevel.Debug); + // Disable HTTP logging middleware, otherwise even though they are not buffered, + // they will be logged as usual and contaminate test results: + builder.AddFilter("Microsoft.AspNetCore.HttpLogging", LogLevel.None); + builder.AddHttpRequestBuffering(LogLevel.Debug); }), async (logCollector, client, sp) => @@ -782,7 +782,6 @@ await RunAsync( Assert.Equal("test", logCollector.LatestRecord.Category); }); } -#endif [Fact] public async Task HttpLogging_LogRecordIsNotCreated_If_Disabled() diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj index 8675bdcf2f4..c42d3043106 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj @@ -5,6 +5,7 @@ + true $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 true diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj index 5f6ca415e12..6f9488a33d8 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj @@ -2,6 +2,7 @@ Microsoft.Extensions.Diagnostics.Probes.Test Unit tests for Microsoft.Extensions.Diagnostics.Probes + true diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj index 4bc20735577..cf7192a9df5 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj @@ -2,6 +2,7 @@ Microsoft.Extensions.Http.Diagnostics.Test Unit tests for Microsoft.Extensions.Http.Diagnostics. + true diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj index d440b8820db..a2f9e84a160 100644 --- a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj @@ -2,6 +2,7 @@ Microsoft.Extensions.Options.Contextual.Test Unit tests for Microsoft.Extensions.Options.Contextual + true diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj index 163b01082d1..02ad31cde93 100644 --- a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj @@ -2,6 +2,7 @@ Microsoft.Extensions.Resilience.Test Unit tests for Microsoft.Extensions.Resilience + true diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index e28b8b246b8..d99a080a2b3 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET9_0_OR_GREATER using System; using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Buffering; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; @@ -64,4 +62,3 @@ public void AddGlobalBufferConfiguration_RegistersInDI() Assert.Equivalent(expectedData, options.CurrentValue.Rules); } } -#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index 8b7cb598d1f..bb3c0fb5771 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -6,12 +6,10 @@ using System.Linq; using Microsoft.Extensions.Compliance.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Diagnostics.Enrichment; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; -#if NET9_0_OR_GREATER -using Microsoft.Extensions.Diagnostics.Buffering; -#endif using Moq; using Xunit; @@ -122,7 +120,6 @@ public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment } } -#if NET9_0_OR_GREATER [Fact] public static void GlobalBuffering_CanonicalUsecase() { @@ -150,7 +147,6 @@ public static void GlobalBuffering_CanonicalUsecase() // 2 log records emitted because the buffer has been flushed Assert.Equal(2, provider.Logger!.Collector.Count); } -#endif [Theory] [CombinatorialData] diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj index 7273b05c6c7..522fe8a9991 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj @@ -5,6 +5,7 @@ + true false false false diff --git a/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs b/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs new file mode 100644 index 00000000000..37bff0974ce --- /dev/null +++ b/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs @@ -0,0 +1,78 @@ +// 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.Text.Json; +using Xunit; + +namespace Microsoft.Shared.ExceptionJsonConverter.Test; + +public class ExceptionJsonConverterTests +{ + [Fact] + public void SerializeAndDeserialize_SimpleException() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); + var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Equal(exception.Message, deserializedException.Message); + } + + [Fact] + public void SerializeAndDeserialize_ExceptionWithInnerException() + { + // Arrange + var innerException = new ArgumentNullException("paramName", "Inner exception message"); + var exception = new InvalidOperationException("Test exception with inner exception", innerException); + + // Act + var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); + var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Equal(exception.Message, deserializedException.Message); + + Assert.NotNull(deserializedException.InnerException); + Assert.IsType(deserializedException.InnerException); + Assert.Contains(innerException.Message, deserializedException.InnerException.Message); + } + + [Fact] + public void SerializeAndDeserialize_AggregateException() + { + // Arrange + var innerException1 = new ArgumentException("First inner exception"); +#pragma warning disable CA2201 // Do not raise reserved exception types + var innerException2 = new NullReferenceException("Second inner exception"); +#pragma warning restore CA2201 // Do not raise reserved exception types + var exception = new AggregateException("Aggregate exception message", innerException1, innerException2); + + // Act + var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); + var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); + + // Assert + Assert.NotNull(deserializedException); + Assert.IsType(deserializedException); + Assert.Contains(exception.Message, deserializedException.Message); + + var aggregateException = (AggregateException)deserializedException; + Assert.NotNull(aggregateException.InnerExceptions); + Assert.Equal(2, aggregateException.InnerExceptions.Count); + + Assert.IsType(aggregateException.InnerExceptions[0]); + Assert.Equal(innerException1.Message, aggregateException.InnerExceptions[0].Message); + + Assert.IsType(aggregateException.InnerExceptions[1]); + Assert.Equal(innerException2.Message, aggregateException.InnerExceptions[1].Message); + } +} From a371d9c34775b8c32deaf2070b6223d555d3f293 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:01:58 +0100 Subject: [PATCH 05/15] Fix build --- .../Microsoft.Extensions.AI.AzureAIInference.csproj | 1 + .../Microsoft.Extensions.AI.Integration.Tests.csproj | 1 + .../Microsoft.Extensions.AI.Ollama.Tests.csproj | 1 + .../Microsoft.Extensions.AI.OpenAI.Tests.csproj | 1 + .../Microsoft.Extensions.AotCompatibility.TestApp.csproj | 7 +++++++ 5 files changed, 11 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index 919fa9b751f..7e2c1245e4f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -15,6 +15,7 @@ $(TargetFrameworks);netstandard2.0 + true $(NoWarn);CA1063;CA2227;SA1316;S1067;S1121;S3358 true true diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index dc7703a8eb0..bc7533bf049 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -8,6 +8,7 @@ $(NoWarn);CA1063;CA1861;SA1130;VSTHRD003 true + true diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj index 5db789e3b6b..1b485b302ca 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj @@ -5,6 +5,7 @@ + true true diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 66412bfeace..eae3a41d364 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -6,6 +6,7 @@ + true true true diff --git a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj index 07bf93e044c..c5548646574 100644 --- a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj +++ b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj @@ -25,6 +25,13 @@ + + + + + + + From d7661a6170749110c60df5c611d19c490f91556a Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:14:57 +0100 Subject: [PATCH 06/15] Slight design changes with interfaces as per PR comments --- .../Buffering/HttpRequestBuffer.cs | 37 +++++++- .../Buffering/HttpRequestBufferManager.cs | 13 +-- .../Buffering/IHttpRequestBufferManager.cs | 5 ++ .../Buffering/BufferSink.cs | 90 ------------------- .../Buffering/BufferedLoggerProxy.cs | 47 ++++++++++ .../Buffering/GlobalBuffer.cs | 39 ++++++-- .../GlobalBufferLoggerBuilderExtensions.cs | 1 + .../Buffering/GlobalBufferManager.cs | 11 +-- .../Buffering/IBufferManager.cs | 12 +-- .../Buffering/IBufferSink.cs | 24 ----- .../Buffering/IGlobalBufferManager.cs | 15 ++++ .../Buffering/ILoggingBuffer.cs | 2 +- .../Buffering/ISerializedLogRecord.cs | 47 ---------- .../Buffering/SerializedLogRecord.cs | 10 +-- .../ILoggerFilterRule.cs | 2 +- .../Logging/ExtendedLogger.cs | 13 ++- .../Logging/ExtendedLoggerFactory.cs | 1 - .../Logging/AcceptanceTests.cs | 2 +- .../Logging/ExtendedLoggerTests.cs | 2 +- 19 files changed, 167 insertions(+), 206 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 7efd2706e5f..d804d02935d 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -3,10 +3,14 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.AspNetCore.Diagnostics.Buffering; @@ -17,18 +21,19 @@ internal sealed class HttpRequestBuffer : ILoggingBuffer private readonly IOptionsMonitor _globalOptions; private readonly ConcurrentQueue _buffer; private readonly TimeProvider _timeProvider = TimeProvider.System; - private readonly IBufferSink _bufferSink; + private readonly IBufferedLogger _bufferedLogger; private readonly object _bufferCapacityLocker = new(); + private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); private DateTimeOffset _truncateAfter; private DateTimeOffset _lastFlushTimestamp; - public HttpRequestBuffer(IBufferSink bufferSink, + public HttpRequestBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor options, IOptionsMonitor globalOptions) { _options = options; _globalOptions = globalOptions; - _bufferSink = bufferSink; + _bufferedLogger = bufferedLogger; _buffer = new ConcurrentQueue(); _truncateAfter = _timeProvider.GetUtcNow(); @@ -82,7 +87,31 @@ public void Flush() _lastFlushTimestamp = _timeProvider.GetUtcNow(); - _bufferSink.LogRecords(result); + List? pooledList = null; + try + { + pooledList = _logRecordPool.Get(); + foreach (var serializedRecord in result) + { + pooledList.Add( + new PooledLogRecord( + serializedRecord.Timestamp, + serializedRecord.LogLevel, + serializedRecord.EventId, + serializedRecord.Exception, + serializedRecord.FormattedMessage, + serializedRecord.Attributes)); + } + + _bufferedLogger.LogRecords(pooledList); + } + finally + { + if (pooledList is not null) + { + _logRecordPool.Return(pooledList); + } + } } public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index 578364277f8..d32c886c725 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Diagnostics.Buffering; @@ -28,17 +29,17 @@ public HttpRequestBufferManager( _globalOptions = globalOptions; } - public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) + public ILoggingBuffer CreateBuffer(IBufferedLogger bufferedLogger, string category) { var httpContext = _httpContextAccessor.HttpContext; if (httpContext is null) { - return _globalBufferManager.CreateBuffer(bufferSink, category); + return _globalBufferManager.CreateBuffer(bufferedLogger, category); } if (!httpContext.Items.TryGetValue(category, out var buffer)) { - var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions, _globalOptions); + var httpRequestBuffer = new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions); httpContext.Items[category] = httpRequestBuffer; return httpRequestBuffer; } @@ -51,7 +52,7 @@ public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) return loggingBuffer; } - public void Flush() => _globalBufferManager.Flush(); + public void FlushNonRequestLogs() => _globalBufferManager.Flush(); public void FlushCurrentRequestLogs() { @@ -68,7 +69,7 @@ public void FlushCurrentRequestLogs() } public bool TryEnqueue( - IBufferSink bufferSink, + IBufferedLogger bufferedLogger, LogLevel logLevel, string category, EventId eventId, @@ -76,7 +77,7 @@ public bool TryEnqueue( Exception? exception, Func formatter) { - var buffer = CreateBuffer(bufferSink, category); + var buffer = CreateBuffer(bufferedLogger, category); return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs index 4bc77040fbe..fc5036f913e 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs @@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public interface IHttpRequestBufferManager : IBufferManager { + /// + /// Flushes the buffer and emits non-request logs. + /// + void FlushNonRequestLogs(); + /// /// Flushes the buffer and emits buffered logs for the current request. /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs deleted file mode 100644 index 9b1708bf55a..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferSink.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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.Linq; -using System.Text.Json; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Shared.ExceptionJsonConverter; -using Microsoft.Shared.Pools; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -internal sealed class BufferSink : IBufferSink -{ - private readonly ExtendedLoggerFactory _factory; - private readonly string _category; - private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); - - public BufferSink(ExtendedLoggerFactory factory, string category) - { - _factory = factory; - _category = category; - } - - public void LogRecords(IEnumerable serializedRecords) - where T : ISerializedLogRecord - { - var providers = _factory.ProviderRegistrations; - - List? pooledList = null; - try - { - foreach (var provider in providers) - { - var logger = provider.Provider.CreateLogger(_category); - - if (logger is IBufferedLogger bufferedLogger) - { - if (pooledList is null) - { - pooledList = _logRecordPool.Get(); - - foreach (var serializedRecord in serializedRecords) - { - pooledList.Add( - new PooledLogRecord( - serializedRecord.Timestamp, - serializedRecord.LogLevel, - serializedRecord.EventId, - serializedRecord.Exception, - serializedRecord.FormattedMessage, - serializedRecord.Attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)).ToArray())); - } - } - - bufferedLogger.LogRecords(pooledList); - - } - else - { - foreach (var serializedRecord in serializedRecords) - { - Exception? exception = null; - if (serializedRecord.Exception is not null) - { - exception = JsonSerializer.Deserialize(serializedRecord.Exception, ExceptionJsonContext.Default.Exception); - } - - logger.Log( - serializedRecord.LogLevel, - serializedRecord.EventId, - serializedRecord.Attributes, - exception, - (_, _) => serializedRecord.FormattedMessage ?? string.Empty); - } - } - } - } - finally - { - if (pooledList is not null) - { - _logRecordPool.Return(pooledList); - } - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs new file mode 100644 index 00000000000..d2e23177624 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs @@ -0,0 +1,47 @@ +// 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.Diagnostics.Buffering; + +internal sealed class BufferedLoggerProxy : IBufferedLogger +{ + private readonly ExtendedLogger _parentLogger; + + public BufferedLoggerProxy(ExtendedLogger parentLogger) + { + _parentLogger = parentLogger; + } + + public void LogRecords(IEnumerable records) + { + LoggerInformation[] loggerInformations = _parentLogger.Loggers; + foreach (LoggerInformation loggerInformation in loggerInformations) + { + ILogger iLogger = loggerInformation.Logger; + if (iLogger is IBufferedLogger bufferedLogger) + { + bufferedLogger.LogRecords(records); + return; + } + else + { + foreach (BufferedLogRecord record in records) + { +#pragma warning disable CA2201 // Do not raise reserved exception types + iLogger.Log( + record.LogLevel, + record.EventId, + record.Attributes, + record.Exception is not null ? new Exception(record.Exception) : null, + (_, _) => record.FormattedMessage ?? string.Empty); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 6873a63cd00..19a12b343d9 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -3,9 +3,13 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -14,19 +18,20 @@ internal sealed class GlobalBuffer : ILoggingBuffer { private readonly IOptionsMonitor _options; private readonly ConcurrentQueue _buffer; - private readonly IBufferSink _bufferSink; + private readonly IBufferedLogger _bufferedLogger; private readonly TimeProvider _timeProvider; + private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); private DateTimeOffset _lastFlushTimestamp; #if NETFRAMEWORK private object _netfxBufferLocker = new(); #endif - public GlobalBuffer(IBufferSink bufferSink, IOptionsMonitor options, TimeProvider timeProvider) + public GlobalBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor options, TimeProvider timeProvider) { _options = options; _timeProvider = timeProvider; _buffer = new ConcurrentQueue(); - _bufferSink = bufferSink; + _bufferedLogger = bufferedLogger; } public bool TryEnqueue( @@ -62,6 +67,8 @@ public bool TryEnqueue( public void Flush() { + _lastFlushTimestamp = _timeProvider.GetUtcNow(); + var result = _buffer.ToArray(); #if NETFRAMEWORK @@ -76,9 +83,31 @@ public void Flush() _buffer.Clear(); #endif - _lastFlushTimestamp = _timeProvider.GetUtcNow(); + List? pooledList = null; + try + { + pooledList = _logRecordPool.Get(); + foreach (var serializedRecord in result) + { + pooledList.Add( + new PooledLogRecord( + serializedRecord.Timestamp, + serializedRecord.LogLevel, + serializedRecord.EventId, + serializedRecord.Exception, + serializedRecord.FormattedMessage, + serializedRecord.Attributes)); + } - _bufferSink.LogRecords(result); + _bufferedLogger.LogRecords(pooledList); + } + finally + { + if (pooledList is not null) + { + _logRecordPool.Return(pooledList); + } + } } public void TruncateOverlimit() diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs index c553b9cd472..0670841247a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -70,6 +70,7 @@ internal static ILoggingBuilder AddGlobalBufferManager(this ILoggingBuilder buil builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index 549d641afa9..aa2fdd0f440 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -7,11 +7,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.Buffering; -internal sealed class GlobalBufferManager : BackgroundService, IBufferManager +internal sealed class GlobalBufferManager : BackgroundService, IGlobalBufferManager { internal readonly ConcurrentDictionary Buffers = []; private readonly IOptionsMonitor _options; @@ -28,8 +29,8 @@ internal GlobalBufferManager(IOptionsMonitor options, TimeP _options = options; } - public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category) - => Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferSink, _options, _timeProvider)); + public ILoggingBuffer CreateBuffer(IBufferedLogger bufferedLogger, string category) + => Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); public void Flush() { @@ -40,14 +41,14 @@ public void Flush() } public bool TryEnqueue( - IBufferSink bufferSink, + IBufferedLogger bufferedLogger, LogLevel logLevel, string category, EventId eventId, TState attributes, Exception? exception, Func formatter) { - var buffer = CreateBuffer(bufferSink, category); + var buffer = CreateBuffer(bufferedLogger, category); return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs index 42024e13d1f..eda0f16327e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs @@ -4,25 +4,21 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// -/// Interface for a global buffer manager. +/// Interface for a buffer manager. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] public interface IBufferManager { - /// - /// Flushes the buffer and emits all buffered logs. - /// - void Flush(); - /// /// Enqueues a log record in the underlying buffer. /// - /// Buffer sink. + /// A logger capable of logging buffered log records. /// Log level. /// Category. /// Event ID. @@ -32,7 +28,7 @@ public interface IBufferManager /// Type of the instance. /// if the log record was buffered; otherwise, . bool TryEnqueue( - IBufferSink bufferSink, + IBufferedLogger bufferedLoger, LogLevel logLevel, string category, EventId eventId, diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs deleted file mode 100644 index 812f5f02906..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferSink.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -/// -/// Represents a sink for buffered log records of all categories which can be forwarded -/// to all currently registered loggers. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface IBufferSink -{ - /// - /// Forwards the to all currently registered loggers. - /// - /// serialized log records. - /// Type of the log records. - void LogRecords(IEnumerable serializedRecords) - where T : ISerializedLogRecord; -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs new file mode 100644 index 00000000000..63ba009e7f8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Buffering; + +/// +/// Interface for a global buffer manager. +/// +internal interface IGlobalBufferManager : IBufferManager +{ + /// + /// Flushes the buffer and emits all buffered logs. + /// + void Flush(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index e8a642cf6de..f2f016443f9 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal interface ILoggingBuffer { /// - /// Enqueues a log record in the underlying buffer.. + /// Enqueues a log record in the underlying buffer. /// /// Log level. /// Category. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs deleted file mode 100644 index 672f11266f3..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ISerializedLogRecord.cs +++ /dev/null @@ -1,47 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -/// -/// Represents a buffered log record that has been serialized. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface ISerializedLogRecord -{ - /// - /// Gets the time when the log record was first created. - /// - public DateTimeOffset Timestamp { get; } - - /// - /// Gets the record's logging severity level. - /// - public LogLevel LogLevel { get; } - - /// - /// Gets the record's event id. - /// - public EventId EventId { get; } - - /// - /// Gets an exception string for this record. - /// - public string? Exception { get; } - - /// - /// Gets the formatted log message. - /// - public string? FormattedMessage { get; } - - /// - /// Gets the variable set of name/value pairs associated with the record. - /// - public IReadOnlyList> Attributes { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index b122f859852..f704aabff08 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; -internal readonly struct SerializedLogRecord : ISerializedLogRecord +internal readonly struct SerializedLogRecord { public SerializedLogRecord( LogLevel logLevel, @@ -23,13 +23,13 @@ public SerializedLogRecord( EventId = eventId; Timestamp = timestamp; - var serializedAttributes = new List>(attributes.Count); + var serializedAttributes = new List>(attributes.Count); for (int i = 0; i < attributes.Count; i++) { #if NETFRAMEWORK - serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key.ToCharArray()), attributes[i].Value?.ToString() ?? string.Empty)); + serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key.ToCharArray()), attributes[i].Value?.ToString() ?? string.Empty)); #else - serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); + serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); #endif } @@ -41,7 +41,7 @@ public SerializedLogRecord( FormattedMessage = formattedMessage; } - public IReadOnlyList> Attributes { get; } + public IReadOnlyList> Attributes { get; } public string? FormattedMessage { get; } public string? Exception { get; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs index 9dd56a1da1c..77eb81c8ff6 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -19,7 +19,7 @@ internal interface ILoggerFilterRule public LogLevel? LogLevel { get; } /// - /// Gets the maximum of messages where this rule applies to. + /// Gets the of messages where this rule applies to. /// public int? EventId { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index 2d8a110116a..c21cd228e4d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -5,13 +5,14 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.Extensions.Diagnostics.Buffering; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Logging; #pragma warning disable CA1031 -// NOTE: This implementation uses thread local storage. As a wasBuffered, it will fail if formatter code, enricher code, or +// NOTE: This implementation uses thread local storage. As a result, it will fail if formatter code, enricher code, or // redactor code calls recursively back into the logger. Don't do that. // // NOTE: Unlike the original logger in dotnet/runtime, this logger eats exceptions thrown from invoked loggers, enrichers, @@ -32,7 +33,7 @@ internal sealed partial class ExtendedLogger : ILogger public ScopeLogger[] ScopeLoggers { get; set; } = Array.Empty(); private readonly IBufferManager? _bufferManager; - private readonly IBufferSink? _bufferSink; + private readonly IBufferedLogger? _bufferedLogger; public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers) { @@ -42,9 +43,7 @@ public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers _bufferManager = _factory.Config.BufferManager; if (_bufferManager is not null) { - Debug.Assert(loggers.Length > 0, "There should be at least one logger provider."); - - _bufferSink = new BufferSink(factory, loggers[0].Category); + _bufferedLogger = new BufferedLoggerProxy(this); } } @@ -282,7 +281,7 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m { if (_bufferManager is not null) { - var wasBuffered = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + var wasBuffered = _bufferManager.TryEnqueue(_bufferedLogger!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = s.Formatter!; return fmt(s.State!, e); @@ -389,7 +388,7 @@ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state { if (_bufferManager is not null) { - bool wasBuffered = _bufferManager.TryEnqueue(_bufferSink!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + bool wasBuffered = _bufferManager.TryEnqueue(_bufferedLogger!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = (Func)s.Formatter!; return fmt((TState)s.State!, e); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 4f2d8376f88..105fa487ebd 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -30,7 +30,6 @@ internal sealed class ExtendedLoggerFactory : ILoggerFactory private volatile bool _disposed; private LoggerFilterOptions _filterOptions; private IExternalScopeProvider? _scopeProvider; - public IReadOnlyCollection ProviderRegistrations => _providerRegistrations; #pragma warning disable S107 // Methods should not have too many parameters public ExtendedLoggerFactory( diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index 07c0c9099e4..bb9aebec280 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -76,7 +76,7 @@ public static void Configure(IApplicationBuilder app) if (bufferManager is not null) { bufferManager.FlushCurrentRequestLogs(); - bufferManager.Flush(); + bufferManager.FlushNonRequestLogs(); } })); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index bb3c0fb5771..fa2dafbe8ce 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -140,7 +140,7 @@ public static void GlobalBuffering_CanonicalUsecase() // instead of this, users would get IBufferManager from DI and call Flush on it var dlf = (Utils.DisposingLoggerFactory)factory; - var bufferManager = dlf.ServiceProvider.GetRequiredService(); + var bufferManager = dlf.ServiceProvider.GetRequiredService(); bufferManager.Flush(); From 9d13ab0ae8d48dd244d493c87cde98e0b0dd9f82 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:49:17 +0100 Subject: [PATCH 07/15] Drop json serialization --- eng/MSBuild/Shared.props | 4 - .../Logging/FakeLogger.cs | 7 +- ...soft.Extensions.Diagnostics.Testing.csproj | 1 - .../Buffering/SerializedLogRecord.cs | 16 +- .../Microsoft.Extensions.Telemetry.csproj | 2 +- .../ExceptionJsonContext.cs | 15 -- .../ExceptionJsonConverter.cs | 163 ------------------ 7 files changed, 12 insertions(+), 196 deletions(-) delete mode 100644 src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs delete mode 100644 src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index 517491c000c..dee583f7e39 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -46,8 +46,4 @@ - - - - diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs index 04758493df0..19d91bb8e27 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs @@ -7,11 +7,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.ExceptionJsonConverter; namespace Microsoft.Extensions.Logging.Testing; @@ -120,8 +118,9 @@ public void LogRecords(IEnumerable records) foreach (var rec in records) { - var exception = rec.Exception is null ? - null : JsonSerializer.Deserialize(rec.Exception, ExceptionJsonContext.Default.Exception); +#pragma warning disable CA2201 // Do not raise reserved exception types + var exception = rec.Exception is not null ? new Exception(rec.Exception) : null; +#pragma warning restore CA2201 // Do not raise reserved exception types var record = new FakeLogRecord(rec.LogLevel, rec.EventId, ConsumeTState(rec.Attributes), exception, rec.FormattedMessage ?? string.Empty, l.ToArray(), Category, !_disabledLevels.ContainsKey(rec.LogLevel), rec.Timestamp); Collector.AddRecord(record); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj index 690755328d5..6a30cf38e8f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Microsoft.Extensions.Diagnostics.Testing.csproj @@ -12,7 +12,6 @@ true true true - true $(NoWarn);IL2026;SYSLIB1100;SYSLIB1101 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index f704aabff08..0c97d893756 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Text.Json; using Microsoft.Extensions.Logging; -using Microsoft.Shared.ExceptionJsonConverter; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -24,20 +22,22 @@ public SerializedLogRecord( Timestamp = timestamp; var serializedAttributes = new List>(attributes.Count); +#if NETFRAMEWORK for (int i = 0; i < attributes.Count; i++) { -#if NETFRAMEWORK serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key.ToCharArray()), attributes[i].Value?.ToString() ?? string.Empty)); + } + + Exception = new string(exception?.Message.ToCharArray()); #else + for (int i = 0; i < attributes.Count; i++) + { serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); -#endif } + Exception = new string(exception?.Message); +#endif Attributes = serializedAttributes; - - // Serialize without StackTrace, which is already optionally available in the log attributes via the ExtendedLogger. - Exception = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); - FormattedMessage = formattedMessage; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index 5d3c484569b..c72fea7449a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -17,7 +17,6 @@ true true true - true true true $(NoWarn);IL2026 @@ -50,6 +49,7 @@ + diff --git a/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs b/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs deleted file mode 100644 index d9ee1bf01a3..00000000000 --- a/src/Shared/ExceptionJsonConverter/ExceptionJsonContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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.Text.Json.Serialization; - -#pragma warning disable CA1716 -namespace Microsoft.Shared.ExceptionJsonConverter; -#pragma warning restore CA1716 - -[JsonSerializable(typeof(Exception))] -[JsonSourceGenerationOptions(Converters = new Type[] { typeof(ExceptionJsonConverter) })] -internal sealed partial class ExceptionJsonContext : JsonSerializerContext -{ -} diff --git a/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs b/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs deleted file mode 100644 index d272c55f2a8..00000000000 --- a/src/Shared/ExceptionJsonConverter/ExceptionJsonConverter.cs +++ /dev/null @@ -1,163 +0,0 @@ -// 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.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; - -#pragma warning disable CA1716 -namespace Microsoft.Shared.ExceptionJsonConverter; -#pragma warning restore CA1716 - -internal sealed class ExceptionJsonConverter : JsonConverter -{ -#pragma warning disable CA2201 // Do not raise reserved exception types - private static Exception _failedDeserialization = new Exception("Failed to deserialize the exception object."); -#pragma warning restore CA2201 // Do not raise reserved exception types - - public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - return _failedDeserialization; - } - - return DeserializeException(ref reader); - } - - public override void Write(Utf8JsonWriter writer, Exception exception, JsonSerializerOptions options) - { - HandleException(writer, exception); - } - - public override bool CanConvert(Type typeToConvert) => typeof(Exception).IsAssignableFrom(typeToConvert); - - private static Exception DeserializeException(ref Utf8JsonReader reader) - { - string? type = null; - string? message = null; - string? source = null; - Exception? innerException = null; - List? innerExceptions = null; - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.PropertyName) - { - string? propertyName = reader.GetString(); - _ = reader.Read(); - - switch (propertyName) - { - case "Type": - type = reader.GetString(); - break; - case "Message": - message = reader.GetString(); - break; - case "Source": - source = reader.GetString(); - break; - case "InnerException": - if (reader.TokenType == JsonTokenType.StartObject) - { - innerException = DeserializeException(ref reader); - } - - break; - case "InnerExceptions": - innerExceptions = new List(); - if (reader.TokenType == JsonTokenType.StartArray) - { - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.StartObject) - { - var innerEx = DeserializeException(ref reader); - innerExceptions.Add(innerEx); - } - } - } - - break; - } - } - else if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - } - - if (type is null) - { - return _failedDeserialization; - } - -#pragma warning disable IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - Type? deserializedType = Type.GetType(type); -#pragma warning restore IL2057 // Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. - if (deserializedType is null) - { - return _failedDeserialization; - } - - Exception? exception; - - if (innerExceptions != null && innerExceptions.Count > 0) - { - if (deserializedType == typeof(AggregateException)) - { - exception = new AggregateException(message, innerExceptions); - } - else - { - exception = Activator.CreateInstance(deserializedType, message, innerExceptions.First()) as Exception; - } - } - else if (innerException != null) - { - exception = Activator.CreateInstance(deserializedType, message, innerException) as Exception; - } - else - { - exception = Activator.CreateInstance(deserializedType, message) as Exception; - } - - if (exception == null) - { - return _failedDeserialization; - } - - exception.Source = source; - - return exception; - } - - private static void HandleException(Utf8JsonWriter writer, Exception exception) - { - writer.WriteStartObject(); - writer.WriteString("Type", exception.GetType().FullName); - writer.WriteString("Message", exception.Message); - writer.WriteString("Source", exception.Source); - if (exception is AggregateException aggregateException) - { - writer.WritePropertyName("InnerExceptions"); - writer.WriteStartArray(); - foreach (var ex in aggregateException.InnerExceptions) - { - HandleException(writer, ex); - } - - writer.WriteEndArray(); - } - else if (exception.InnerException != null) - { - writer.WritePropertyName("InnerException"); - HandleException(writer, exception.InnerException); - } - - writer.WriteEndObject(); - } -} From fe0065883fe1d21ecc21758aadfcc4731a07570e Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:05:23 +0100 Subject: [PATCH 08/15] Add log record size estimation and limit buffer size in bytes Remove pooling for now --- .../Buffering/HttpRequestBuffer.cs | 98 ++++++++----------- ...ttpRequestBufferLoggerBuilderExtensions.cs | 12 +-- .../Buffering/HttpRequestBufferManager.cs | 48 ++++----- .../Buffering/HttpRequestBufferOptions.cs | 11 +-- .../Buffering/IHttpRequestBufferManager.cs | 2 +- .../Buffering/BufferedLoggerProxy.cs | 1 - ...dLogRecord.cs => DeserializedLogRecord.cs} | 56 +++++------ .../Buffering/GlobalBuffer.cs | 85 ++++++++-------- .../GlobalBufferLoggerBuilderExtensions.cs | 6 +- .../Buffering/GlobalBufferManager.cs | 22 +---- .../Buffering/GlobalBufferOptions.cs | 10 +- .../Buffering/IGlobalBufferManager.cs | 6 +- .../Buffering/ILoggingBuffer.cs | 10 +- .../Buffering/SerializedLogRecord.cs | 66 ++++++++++--- .../ILoggerFilterRule.cs | 6 +- .../LoggerFilterRuleSelector.cs | 26 ++++- .../Logging/LoggingEnrichmentExtensions.cs | 29 +++++- .../Logging/LoggingRedactionExtensions.cs | 11 ++- .../Microsoft.Extensions.Telemetry.csproj | 1 - src/Shared/Shared.csproj | 1 - .../ExceptionJsonConverterTests.cs | 78 --------------- 21 files changed, 266 insertions(+), 319 deletions(-) rename src/Libraries/Microsoft.Extensions.Telemetry/Buffering/{PooledLogRecord.cs => DeserializedLogRecord.cs} (56%) delete mode 100644 test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index d804d02935d..5df7aecbde1 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -4,13 +4,12 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.Pools; using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.AspNetCore.Diagnostics.Buffering; @@ -22,10 +21,9 @@ internal sealed class HttpRequestBuffer : ILoggingBuffer private readonly ConcurrentQueue _buffer; private readonly TimeProvider _timeProvider = TimeProvider.System; private readonly IBufferedLogger _bufferedLogger; - private readonly object _bufferCapacityLocker = new(); - private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); - private DateTimeOffset _truncateAfter; + private DateTimeOffset _lastFlushTimestamp; + private int _bufferSize; public HttpRequestBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor options, @@ -35,8 +33,6 @@ public HttpRequestBuffer(IBufferedLogger bufferedLogger, _globalOptions = globalOptions; _bufferedLogger = bufferedLogger; _buffer = new ConcurrentQueue(); - - _truncateAfter = _timeProvider.GetUtcNow(); } public bool TryEnqueue( @@ -52,66 +48,57 @@ public bool TryEnqueue( return false; } - switch (attributes) + SerializedLogRecord serializedLogRecord = default; + if (attributes is ModernTagJoiner modernTagJoiner) { - case ModernTagJoiner modernTagJoiner: - _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, - ((Func)(object)formatter)(modernTagJoiner, exception))); - break; - case LegacyTagJoiner legacyTagJoiner: - _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, - ((Func)(object)formatter)(legacyTagJoiner, exception))); - break; - default: - Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}"); - break; + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, + ((Func)(object)formatter)(modernTagJoiner, exception)); + } + else if (attributes is LegacyTagJoiner legacyTagJoiner) + { + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, + ((Func)(object)formatter)(legacyTagJoiner, exception)); + } + else + { + Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}"); } - var now = _timeProvider.GetUtcNow(); - lock (_bufferCapacityLocker) + if (serializedLogRecord.SizeInBytes > _globalOptions.CurrentValue.LogRecordSizeInBytes) { - if (now >= _truncateAfter) - { - _truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration); - TruncateOverlimit(); - } + return false; } + _buffer.Enqueue(serializedLogRecord); + _ = Interlocked.Add(ref _bufferSize, serializedLogRecord.SizeInBytes); + + Trim(); + return true; } public void Flush() { - var result = _buffer.ToArray(); - _buffer.Clear(); - _lastFlushTimestamp = _timeProvider.GetUtcNow(); - List? pooledList = null; - try - { - pooledList = _logRecordPool.Get(); - foreach (var serializedRecord in result) - { - pooledList.Add( - new PooledLogRecord( - serializedRecord.Timestamp, - serializedRecord.LogLevel, - serializedRecord.EventId, - serializedRecord.Exception, - serializedRecord.FormattedMessage, - serializedRecord.Attributes)); - } - - _bufferedLogger.LogRecords(pooledList); - } - finally + SerializedLogRecord[] bufferedRecords = _buffer.ToArray(); + + _buffer.Clear(); + + var deserializedLogRecords = new List(bufferedRecords.Length); + foreach (var bufferedRecord in bufferedRecords) { - if (pooledList is not null) - { - _logRecordPool.Return(pooledList); - } + deserializedLogRecords.Add( + new DeserializedLogRecord( + bufferedRecord.Timestamp, + bufferedRecord.LogLevel, + bufferedRecord.EventId, + bufferedRecord.Exception, + bufferedRecord.FormattedMessage, + bufferedRecord.Attributes)); } + + _bufferedLogger.LogRecords(deserializedLogRecords); } public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) @@ -126,12 +113,11 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) return rule is not null; } - public void TruncateOverlimit() + private void Trim() { - // Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments. - while (_buffer.Count > _options.CurrentValue.PerRequestCapacity) + while (_bufferSize > _options.CurrentValue.PerRequestBufferSizeInBytes && _buffer.TryDequeue(out var item)) { - _ = _buffer.TryDequeue(out _); + _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes); } } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs index dd82ee079b8..b95db41fa8b 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -38,8 +38,7 @@ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder build return builder .AddHttpRequestBufferConfiguration(configuration) .AddHttpRequestBufferManager() - .AddGlobalBufferConfiguration(configuration) - .AddGlobalBufferManager(); + .AddGlobalBuffer(configuration); } /// @@ -61,8 +60,7 @@ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder build return builder .AddHttpRequestBufferManager() - .AddGlobalBuffer(level) - .AddGlobalBufferManager(); + .AddGlobalBuffer(level); } /// @@ -75,11 +73,9 @@ internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder { _ = Throw.IfNull(builder); - builder.Services.TryAddSingleton(); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + _ = builder.Services.AddExtendedLoggerFeactory(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index d32c886c725..d49795403be 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -12,13 +12,13 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager { - private readonly GlobalBufferManager _globalBufferManager; + private readonly IGlobalBufferManager _globalBufferManager; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IOptionsMonitor _requestOptions; private readonly IOptionsMonitor _globalOptions; public HttpRequestBufferManager( - GlobalBufferManager globalBufferManager, + IGlobalBufferManager globalBufferManager, IHttpContextAccessor httpContextAccessor, IOptionsMonitor requestOptions, IOptionsMonitor globalOptions) @@ -29,29 +29,6 @@ public HttpRequestBufferManager( _globalOptions = globalOptions; } - public ILoggingBuffer CreateBuffer(IBufferedLogger bufferedLogger, string category) - { - var httpContext = _httpContextAccessor.HttpContext; - if (httpContext is null) - { - return _globalBufferManager.CreateBuffer(bufferedLogger, category); - } - - if (!httpContext.Items.TryGetValue(category, out var buffer)) - { - var httpRequestBuffer = new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions); - httpContext.Items[category] = httpRequestBuffer; - return httpRequestBuffer; - } - - if (buffer is not ILoggingBuffer loggingBuffer) - { - throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}"); - } - - return loggingBuffer; - } - public void FlushNonRequestLogs() => _globalBufferManager.Flush(); public void FlushCurrentRequestLogs() @@ -77,7 +54,24 @@ public bool TryEnqueue( Exception? exception, Func formatter) { - var buffer = CreateBuffer(bufferedLogger, category); - return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext is null) + { + return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, attributes, exception, formatter); + } + + if (!httpContext.Items.TryGetValue(category, out var buffer)) + { + var httpRequestBuffer = new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions); + httpContext.Items[category] = httpRequestBuffer; + buffer = httpRequestBuffer; + } + + if (buffer is not ILoggingBuffer loggingBuffer) + { + throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}"); + } + + return loggingBuffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs index a61f5c8b3cf..e95876299ab 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs @@ -1,7 +1,6 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Diagnostics.Buffering; @@ -16,14 +15,10 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; public class HttpRequestBufferOptions { /// - /// Gets or sets the duration to check and remove the buffered items exceeding the . + /// Gets or sets the size in bytes of the buffer for a request. If the buffer size exceeds this limit, the oldest buffered log records will be dropped. /// - public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// Gets or sets the size of the buffer for a request. - /// - public int PerRequestCapacity { get; set; } = 1_000; + /// TO DO: add validation. + public int PerRequestBufferSizeInBytes { get; set; } = 5_000_000; #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() #pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs index fc5036f913e..c6951b5042a 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs @@ -21,5 +21,5 @@ public interface IHttpRequestBufferManager : IBufferManager /// /// Flushes the buffer and emits buffered logs for the current request. /// - public void FlushCurrentRequestLogs(); + void FlushCurrentRequestLogs(); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs index d2e23177624..9cda54b51b5 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferedLoggerProxy.cs @@ -26,7 +26,6 @@ public void LogRecords(IEnumerable records) if (iLogger is IBufferedLogger bufferedLogger) { bufferedLogger.LogRecords(records); - return; } else { diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs similarity index 56% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs index 41776f37fc5..cf760364e71 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/PooledLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs @@ -3,15 +3,29 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; -internal sealed class PooledLogRecord : BufferedLogRecord, IResettable + +/// +/// Represents a log record deserialized from somewhere, such as buffer. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public sealed class DeserializedLogRecord : BufferedLogRecord { - public PooledLogRecord( + /// + /// Initializes a new instance of the class. + /// + /// The time when the log record was first created. + /// Logging severity level. + /// Event ID. + /// An exception string for this record. + /// The formatted log message. + /// The set of name/value pairs associated with the record. + public DeserializedLogRecord( DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, @@ -27,49 +41,27 @@ public PooledLogRecord( _attributes = attributes; } + /// public override DateTimeOffset Timestamp => _timestamp; private DateTimeOffset _timestamp; + /// public override LogLevel LogLevel => _logLevel; private LogLevel _logLevel; + /// public override EventId EventId => _eventId; private EventId _eventId; + /// public override string? Exception => _exception; private string? _exception; - public override ActivitySpanId? ActivitySpanId => _activitySpanId; - private ActivitySpanId? _activitySpanId; - - public override ActivityTraceId? ActivityTraceId => _activityTraceId; - private ActivityTraceId? _activityTraceId; - - public override int? ManagedThreadId => _managedThreadId; - private int? _managedThreadId; - + /// public override string? FormattedMessage => _formattedMessage; private string? _formattedMessage; - public override string? MessageTemplate => _messageTemplate; - private string? _messageTemplate; - + /// public override IReadOnlyList> Attributes => _attributes; private IReadOnlyList> _attributes; - - public bool TryReset() - { - _timestamp = default; - _logLevel = default; - _eventId = default; - _exception = default; - _activitySpanId = default; - _activityTraceId = default; - _managedThreadId = default; - _formattedMessage = default; - _messageTemplate = default; - _attributes = []; - - return true; - } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 19a12b343d9..ba87ac2330c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -4,12 +4,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.Pools; using static Microsoft.Extensions.Logging.ExtendedLogger; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -20,8 +19,9 @@ internal sealed class GlobalBuffer : ILoggingBuffer private readonly ConcurrentQueue _buffer; private readonly IBufferedLogger _bufferedLogger; private readonly TimeProvider _timeProvider; - private readonly ObjectPool> _logRecordPool = PoolFactory.CreateListPool(); private DateTimeOffset _lastFlushTimestamp; + + private int _bufferSize; #if NETFRAMEWORK private object _netfxBufferLocker = new(); #endif @@ -47,21 +47,32 @@ public bool TryEnqueue( return false; } - switch (attributes) + SerializedLogRecord serializedLogRecord = default; + if (attributes is ModernTagJoiner modernTagJoiner) + { + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, + ((Func)(object)formatter)(modernTagJoiner, exception)); + } + else if (attributes is LegacyTagJoiner legacyTagJoiner) + { + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, + ((Func)(object)formatter)(legacyTagJoiner, exception)); + } + else + { + Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(T)}"); + } + + if (serializedLogRecord.SizeInBytes > _options.CurrentValue.LogRecordSizeInBytes) { - case ModernTagJoiner modernTagJoiner: - _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, - ((Func)(object)formatter)(modernTagJoiner, exception))); - break; - case LegacyTagJoiner legacyTagJoiner: - _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, - ((Func)(object)formatter)(legacyTagJoiner, exception))); - break; - default: - Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(T)}"); - break; + return false; } + _buffer.Enqueue(serializedLogRecord); + _ = Interlocked.Add(ref _bufferSize, serializedLogRecord.SizeInBytes); + + Trim(); + return true; } @@ -69,7 +80,7 @@ public void Flush() { _lastFlushTimestamp = _timeProvider.GetUtcNow(); - var result = _buffer.ToArray(); + SerializedLogRecord[] bufferedRecords = _buffer.ToArray(); #if NETFRAMEWORK lock (_netfxBufferLocker) @@ -83,39 +94,27 @@ public void Flush() _buffer.Clear(); #endif - List? pooledList = null; - try + var deserializedLogRecords = new List(bufferedRecords.Length); + foreach (var bufferedRecord in bufferedRecords) { - pooledList = _logRecordPool.Get(); - foreach (var serializedRecord in result) - { - pooledList.Add( - new PooledLogRecord( - serializedRecord.Timestamp, - serializedRecord.LogLevel, - serializedRecord.EventId, - serializedRecord.Exception, - serializedRecord.FormattedMessage, - serializedRecord.Attributes)); - } - - _bufferedLogger.LogRecords(pooledList); - } - finally - { - if (pooledList is not null) - { - _logRecordPool.Return(pooledList); - } + deserializedLogRecords.Add( + new DeserializedLogRecord( + bufferedRecord.Timestamp, + bufferedRecord.LogLevel, + bufferedRecord.EventId, + bufferedRecord.Exception, + bufferedRecord.FormattedMessage, + bufferedRecord.Attributes)); } + + _bufferedLogger.LogRecords(deserializedLogRecords); } - public void TruncateOverlimit() + private void Trim() { - // Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments. - while (_buffer.Count > _options.CurrentValue.Capacity) + while (_bufferSize > _options.CurrentValue.BufferSizeInBytes && _buffer.TryDequeue(out var item)) { - _ = _buffer.TryDequeue(out _); + _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs index 0670841247a..3137645557a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.Buffering; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; @@ -68,13 +67,12 @@ internal static ILoggingBuilder AddGlobalBufferManager(this ILoggingBuilder buil { _ = Throw.IfNull(builder); + _ = builder.Services.AddExtendedLoggerFeactory(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); - return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index aa2fdd0f440..d2f9eef254a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -3,16 +3,13 @@ using System; using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.Buffering; -internal sealed class GlobalBufferManager : BackgroundService, IGlobalBufferManager +internal sealed class GlobalBufferManager : IGlobalBufferManager { internal readonly ConcurrentDictionary Buffers = []; private readonly IOptionsMonitor _options; @@ -29,9 +26,6 @@ internal GlobalBufferManager(IOptionsMonitor options, TimeP _options = options; } - public ILoggingBuffer CreateBuffer(IBufferedLogger bufferedLogger, string category) - => Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); - public void Flush() { foreach (var buffer in Buffers.Values) @@ -48,19 +42,7 @@ public bool TryEnqueue( Exception? exception, Func formatter) { - var buffer = CreateBuffer(bufferedLogger, category); + var buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); } - - protected override async Task ExecuteAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - await _timeProvider.Delay(_options.CurrentValue.Duration, cancellationToken).ConfigureAwait(false); - foreach (var buffer in Buffers.Values) - { - buffer.TruncateOverlimit(); - } - } - } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs index 22df6eb3de2..b55ec90e308 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -24,14 +24,16 @@ public class GlobalBufferOptions public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); /// - /// Gets or sets the duration to check and remove the buffered items exceeding the . + /// Gets or sets the maxiumum size of each individual log record in bytes. If the size of a log record exceeds this limit, it won't be buffered. /// - public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(30); + /// TO DO: add validation. + public int LogRecordSizeInBytes { get; set; } = 50_000; /// - /// Gets or sets the size of the buffer. + /// Gets or sets the maximum size of the buffer in bytes. If the buffer size exceeds this limit, the oldest buffered log records will be dropped. /// - public int Capacity { get; set; } = 10_000; + /// TO DO: add validation. + public int BufferSizeInBytes { get; set; } = 500_000_000; #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() #pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs index 63ba009e7f8..5a68f76c6a2 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs @@ -1,12 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Interface for a global buffer manager. /// -internal interface IGlobalBufferManager : IBufferManager +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface IGlobalBufferManager : IBufferManager { /// /// Flushes the buffer and emits all buffered logs. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index f2f016443f9..c23835f2ffc 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -2,14 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Interface for a logging buffer. /// -internal interface ILoggingBuffer +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface ILoggingBuffer { /// /// Enqueues a log record in the underlying buffer. @@ -34,9 +37,4 @@ bool TryEnqueue( /// Flushes the buffer. /// void Flush(); - - /// - /// Removes items exceeding the buffer limit. - /// - void TruncateOverlimit(); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index 0c97d893756..47744fedcf2 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -3,12 +3,29 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; -internal readonly struct SerializedLogRecord +/// +/// Represents a log record that has been serialized for purposes of buffering or similar. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public readonly struct SerializedLogRecord +#pragma warning restore CA1815 // Override equals and operator equals on value types { + /// + /// Initializes a new instance of the struct. + /// + /// Logging severity level. + /// Event ID. + /// The time when the log record was first created. + /// The set of name/value pairs associated with the record. + /// An exception string for this record. + /// The formatted log message. public SerializedLogRecord( LogLevel logLevel, EventId eventId, @@ -21,33 +38,56 @@ public SerializedLogRecord( EventId = eventId; Timestamp = timestamp; - var serializedAttributes = new List>(attributes.Count); -#if NETFRAMEWORK - for (int i = 0; i < attributes.Count; i++) + List> serializedAttributes = []; + if (attributes is not null) { - serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key.ToCharArray()), attributes[i].Value?.ToString() ?? string.Empty)); - } + serializedAttributes = new List>(attributes.Count); + for (int i = 0; i < attributes.Count; i++) + { + string value = attributes[i].Value?.ToString() ?? string.Empty; + serializedAttributes.Add(new KeyValuePair(attributes[i].Key, value)); + SizeInBytes += value.Length * sizeof(char); + } - Exception = new string(exception?.Message.ToCharArray()); -#else - for (int i = 0; i < attributes.Count; i++) - { - serializedAttributes.Add(new KeyValuePair(new string(attributes[i].Key), attributes[i].Value?.ToString() ?? string.Empty)); + Exception = exception?.Message; } - Exception = new string(exception?.Message); -#endif Attributes = serializedAttributes; FormattedMessage = formattedMessage; } + /// + /// Gets the variable set of name/value pairs associated with the record. + /// public IReadOnlyList> Attributes { get; } + + /// + /// Gets the formatted log message. + /// public string? FormattedMessage { get; } + + /// + /// Gets an exception string for this record. + /// public string? Exception { get; } + /// + /// Gets the time when the log record was first created. + /// public DateTimeOffset Timestamp { get; } + /// + /// Gets the record's logging severity level. + /// public LogLevel LogLevel { get; } + /// + /// Gets the record's event ID. + /// public EventId EventId { get; } + + /// + /// Gets the approximate size of the serialized log record in bytes. + /// + public int SizeInBytes { get; init; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs index 77eb81c8ff6..6751250b63c 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -1,12 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + namespace Microsoft.Extensions.Logging; /// /// Represents a rule used for filtering log messages for purposes of log sampling and buffering. /// -internal interface ILoggerFilterRule +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public interface ILoggerFilterRule { /// /// Gets the logger category this rule applies to. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs index a9f739cadfd..ff78457ce6e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs @@ -7,11 +7,26 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Logging; -internal static class LoggerFilterRuleSelector +/// +/// Selects the best rule from the list of rules for a given log event. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public static class LoggerFilterRuleSelector { + /// + /// Selects the best rule from the list of rules for a given log event. + /// + /// The type of the rules. + /// The list of rules to select from. + /// The category of the log event. + /// The log level of the log event. + /// The event id of the log event. + /// The best rule that matches the log event. public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, out T? bestRule) where T : class, ILoggerFilterRule @@ -26,11 +41,14 @@ public static void Select(IList rules, string category, LogLevel logLevel, // 4. If there are multiple rules use last T? current = null; - foreach (T rule in rules) + if (rules is not null) { - if (IsBetter(rule, current, category, logLevel, eventId)) + foreach (T rule in rules) { - current = rule; + if (IsBetter(rule, current, category, logLevel, eventId)) + { + current = rule; + } } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs index 1d4262b454e..cf68d2ea10b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging; @@ -34,9 +36,10 @@ public static ILoggingBuilder EnableEnrichment(this ILoggingBuilder builder, Act _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - _ = builder.Services.Configure(configure); - _ = builder.Services.AddOptionsWithValidateOnStart(); + _ = builder.Services + .AddExtendedLoggerFeactory() + .Configure(configure) + .AddOptionsWithValidateOnStart(); return builder; } @@ -52,9 +55,25 @@ public static ILoggingBuilder EnableEnrichment(this ILoggingBuilder builder, ICo _ = Throw.IfNull(builder); _ = Throw.IfNull(section); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - _ = builder.Services.AddOptionsWithValidateOnStart().Bind(section); + _ = builder.Services + .AddExtendedLoggerFeactory() + .AddOptionsWithValidateOnStart().Bind(section); return builder; } + + /// + /// Adds a default implementation of the to the service collection. + /// + /// The . + /// The value of . + [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] + public static IServiceCollection AddExtendedLoggerFeactory(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services; + } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingRedactionExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingRedactionExtensions.cs index 4ec3ea9ef3e..284e24f10ea 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingRedactionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingRedactionExtensions.cs @@ -4,7 +4,6 @@ using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; @@ -34,8 +33,9 @@ public static ILoggingBuilder EnableRedaction(this ILoggingBuilder builder, Acti _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - _ = builder.Services.Configure(configure); + _ = builder.Services + .AddExtendedLoggerFeactory() + .Configure(configure); return builder; } @@ -51,8 +51,9 @@ public static ILoggingBuilder EnableRedaction(this ILoggingBuilder builder, ICon _ = Throw.IfNull(builder); _ = Throw.IfNull(section); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - _ = builder.Services.AddOptions().Bind(section); + _ = builder.Services + .AddExtendedLoggerFeactory() + .AddOptions().Bind(section); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index c72fea7449a..174cb10fadc 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -49,7 +49,6 @@ - diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index e35848d918c..439c3788557 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -6,7 +6,6 @@ - $(NoWarn);IL2026 $(NetCoreTargetFrameworks)$(ConditionalNet462) false $(DefineConstants);SHARED_PROJECT diff --git a/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs b/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs deleted file mode 100644 index 37bff0974ce..00000000000 --- a/test/Shared/ExceptionJsonConverter/ExceptionJsonConverterTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// 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.Text.Json; -using Xunit; - -namespace Microsoft.Shared.ExceptionJsonConverter.Test; - -public class ExceptionJsonConverterTests -{ - [Fact] - public void SerializeAndDeserialize_SimpleException() - { - // Arrange - var exception = new InvalidOperationException("Test exception"); - - // Act - var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); - var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Equal(exception.Message, deserializedException.Message); - } - - [Fact] - public void SerializeAndDeserialize_ExceptionWithInnerException() - { - // Arrange - var innerException = new ArgumentNullException("paramName", "Inner exception message"); - var exception = new InvalidOperationException("Test exception with inner exception", innerException); - - // Act - var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); - var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Equal(exception.Message, deserializedException.Message); - - Assert.NotNull(deserializedException.InnerException); - Assert.IsType(deserializedException.InnerException); - Assert.Contains(innerException.Message, deserializedException.InnerException.Message); - } - - [Fact] - public void SerializeAndDeserialize_AggregateException() - { - // Arrange - var innerException1 = new ArgumentException("First inner exception"); -#pragma warning disable CA2201 // Do not raise reserved exception types - var innerException2 = new NullReferenceException("Second inner exception"); -#pragma warning restore CA2201 // Do not raise reserved exception types - var exception = new AggregateException("Aggregate exception message", innerException1, innerException2); - - // Act - var json = JsonSerializer.Serialize(exception, ExceptionJsonContext.Default.Exception); - var deserializedException = JsonSerializer.Deserialize(json, ExceptionJsonContext.Default.Exception); - - // Assert - Assert.NotNull(deserializedException); - Assert.IsType(deserializedException); - Assert.Contains(exception.Message, deserializedException.Message); - - var aggregateException = (AggregateException)deserializedException; - Assert.NotNull(aggregateException.InnerExceptions); - Assert.Equal(2, aggregateException.InnerExceptions.Count); - - Assert.IsType(aggregateException.InnerExceptions[0]); - Assert.Equal(innerException1.Message, aggregateException.InnerExceptions[0].Message); - - Assert.IsType(aggregateException.InnerExceptions[1]); - Assert.Equal(innerException2.Message, aggregateException.InnerExceptions[1].Message); - } -} From 5fc421c6a8c29f455f42f6f18352831b5fdf6522 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:40:37 +0100 Subject: [PATCH 09/15] Add filtering by attributes --- .../Buffering/HttpRequestBuffer.cs | 19 +++-- ...ttpRequestBufferLoggerBuilderExtensions.cs | 2 +- ...soft.Extensions.AI.AzureAIInference.csproj | 1 - .../Buffering/BufferFilterRule.cs | 12 +++- .../Buffering/GlobalBuffer.cs | 19 +++-- .../GlobalBufferLoggerBuilderExtensions.cs | 2 +- .../ILoggerFilterRule.cs | 13 +++- .../LoggerFilterRuleSelector.cs | 3 +- ...questBufferLoggerBuilderExtensionsTests.cs | 4 +- ...Extensions.AotCompatibility.TestApp.csproj | 7 -- ...lobalBufferLoggerBuilderExtensionsTests.cs | 4 +- .../LoggerFilterRuleSelectorTests.cs | 69 +++++++++++++++++++ 12 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 5df7aecbde1..307cd908a17 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -43,19 +43,24 @@ public bool TryEnqueue( Exception? exception, Func formatter) { - if (!IsEnabled(category, logLevel, eventId)) - { - return false; - } - SerializedLogRecord serializedLogRecord = default; if (attributes is ModernTagJoiner modernTagJoiner) { + if (!IsEnabled(category, logLevel, eventId, modernTagJoiner)) + { + return false; + } + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, ((Func)(object)formatter)(modernTagJoiner, exception)); } else if (attributes is LegacyTagJoiner legacyTagJoiner) { + if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner)) + { + return false; + } + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, ((Func)(object)formatter)(legacyTagJoiner, exception)); } @@ -101,7 +106,7 @@ public void Flush() _bufferedLogger.LogRecords(deserializedLogRecords); } - public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) + public bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IReadOnlyList> attributes) { if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration) { @@ -110,7 +115,7 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); - return rule is not null; + return rule is not null && rule.Filter(category, logLevel, eventId, attributes); } private void Trim() diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs index b95db41fa8b..bf0a2b37a69 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -55,7 +55,7 @@ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder build _ = Throw.IfNull(builder); _ = builder.Services - .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) + .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null, null))) .Configure(configure ?? new Action(_ => { })); return builder diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index 7e2c1245e4f..919fa9b751f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -15,7 +15,6 @@ $(TargetFrameworks);netstandard2.0 - true $(NoWarn);CA1063;CA2227;SA1316;S1067;S1121;S3358 true true diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs index b69d133a3f6..9cb555d7c9b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs @@ -1,6 +1,8 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; @@ -17,7 +19,7 @@ public class BufferFilterRule : ILoggerFilterRule /// Initializes a new instance of the class. /// public BufferFilterRule() - : this(null, null, null) + : this(null, null, null, null) { } @@ -27,11 +29,14 @@ public BufferFilterRule() /// The category name to use in this filter rule. /// The to use in this filter rule. /// The to use in this filter rule. - public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId) + /// The optional filter delegate to use if a log message passes other filters. + public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId, + Func>, bool>? filter = null) { Category = categoryName; LogLevel = logLevel; EventId = eventId; + Filter = filter ?? ((_, _, _, _) => true); } /// @@ -42,4 +47,7 @@ public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId) /// public int? EventId { get; set; } + + /// + public Func>, bool> Filter { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index ba87ac2330c..49c8706640b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -42,19 +42,24 @@ public bool TryEnqueue( Exception? exception, Func formatter) { - if (!IsEnabled(category, logLevel, eventId)) - { - return false; - } - SerializedLogRecord serializedLogRecord = default; if (attributes is ModernTagJoiner modernTagJoiner) { + if (!IsEnabled(category, logLevel, eventId, modernTagJoiner)) + { + return false; + } + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, ((Func)(object)formatter)(modernTagJoiner, exception)); } else if (attributes is LegacyTagJoiner legacyTagJoiner) { + if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner)) + { + return false; + } + serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, ((Func)(object)formatter)(legacyTagJoiner, exception)); } @@ -118,7 +123,7 @@ private void Trim() } } - private bool IsEnabled(string category, LogLevel logLevel, EventId eventId) + private bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IReadOnlyList> attributes) { if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration) { @@ -127,6 +132,6 @@ private bool IsEnabled(string category, LogLevel logLevel, EventId eventId) LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); - return rule is not null; + return rule is not null && rule.Filter(category, logLevel, eventId, attributes); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs index 3137645557a..26d034e7993 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs @@ -52,7 +52,7 @@ public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, LogL _ = Throw.IfNull(builder); _ = builder.Services - .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null))) + .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null, null))) .Configure(configure ?? new Action(_ => { })); return builder.AddGlobalBufferManager(); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs index 6751250b63c..59d32c992c0 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -1,6 +1,8 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; @@ -15,15 +17,20 @@ public interface ILoggerFilterRule /// /// Gets the logger category this rule applies to. /// - public string? Category { get; } + string? Category { get; } /// /// Gets the maximum of messages. /// - public LogLevel? LogLevel { get; } + LogLevel? LogLevel { get; } /// /// Gets the of messages where this rule applies to. /// - public int? EventId { get; } + int? EventId { get; } + + /// + /// Gets the filter delegate that would be additionally applied to messages that passed the , , and filters. + /// + Func>, bool> Filter { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs index ff78457ce6e..94397b4e9b2 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs @@ -27,8 +27,7 @@ public static class LoggerFilterRuleSelector /// The log level of the log event. /// The event id of the log event. /// The best rule that matches the log event. - public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, - out T? bestRule) + public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, out T? bestRule) where T : class, ILoggerFilterRule { bestRule = null; diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs index 04e57bb24f5..929b97b97b2 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs @@ -45,8 +45,8 @@ public void AddHttpRequestBufferConfiguration_RegistersInDI() { List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1), - new BufferFilterRule(null, LogLevel.Information, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Information, null, null), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); configBuilder.AddJsonFile("appsettings.json"); diff --git a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj index c5548646574..07bf93e044c 100644 --- a/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj +++ b/test/Libraries/Microsoft.Extensions.AotCompatibility.TestApp/Microsoft.Extensions.AotCompatibility.TestApp.csproj @@ -25,13 +25,6 @@ - - - - - - - diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index d99a080a2b3..6ff9c6c274b 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -44,8 +44,8 @@ public void AddGlobalBufferConfiguration_RegistersInDI() { List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1), - new BufferFilterRule(null, LogLevel.Information, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Information, null, null), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); configBuilder.AddJsonFile("appsettings.json"); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs new file mode 100644 index 00000000000..86a2f14ad7a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Diagnostics.Buffering; +using Xunit; + +namespace Microsoft.Extensions.Logging.Test; +public class LoggerFilterRuleSelectorTests +{ + [Fact] + public void SelectsRightRule() + { + // Arrange + var rules = new List + { + new BufferFilterRule(null, null, null, null), + new BufferFilterRule(null, null, 1, null), + new BufferFilterRule(null, LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Warning, null, null), + new BufferFilterRule(null, LogLevel.Warning, 2, null), + new BufferFilterRule(null, LogLevel.Warning, 1, null), + new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1), // the best rule + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 2, null), + new BufferFilterRule("Program.MyLogger", null, 1, null), + new BufferFilterRule(null, LogLevel.Warning, 1, null), + new BufferFilterRule("Program", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, null, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Error, 1, null), + }; + + // Act + LoggerFilterRuleSelector.Select( + rules, "Program.MyLogger", LogLevel.Warning, 1, out var actualResult); + + // Assert + Assert.Same(rules[9], actualResult); + } + + [Fact] + public void WhenManyRuleApply_SelectsLast() + { + // Arrange + var rules = new List + { + new BufferFilterRule(null, LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Information, 1, null), + new BufferFilterRule(null, LogLevel.Warning, null, null), + new BufferFilterRule(null, LogLevel.Warning, 2, null), + new BufferFilterRule(null, LogLevel.Warning, 1, null), + new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1), // the best rule + new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1), // same as the best, but last and should be selected + }; + + // Act + LoggerFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, out var actualResult); + + // Assert + Assert.Same(rules.Last(), actualResult); + } +} From 70cfc7c5851fccb960762486bcfe8eab814d4980 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:26:22 +0100 Subject: [PATCH 10/15] Use attributes directly instead of Func delegate --- .../Buffering/HttpRequestBuffer.cs | 4 +-- .../Buffering/HttpRequestBufferOptions.cs | 6 ++-- .../Buffering/BufferFilterRule.cs | 9 +++-- .../Buffering/GlobalBuffer.cs | 4 +-- .../Buffering/GlobalBufferConfigureOptions.cs | 10 ++++++ .../Buffering/GlobalBufferOptions.cs | 6 ++-- .../ILoggerFilterRule.cs | 5 ++- .../LoggerFilterRuleSelector.cs | 35 +++++++++++++++++-- ...lobalBufferLoggerBuilderExtensionsTests.cs | 13 +++++-- .../LoggerFilterRuleSelectorTests.cs | 14 ++++---- .../appsettings.json | 10 +++++- 11 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 307cd908a17..ef5fb6db567 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -113,9 +113,9 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRead return false; } - LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); + LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); - return rule is not null && rule.Filter(category, logLevel, eventId, attributes); + return rule is not null; } private void Trim() diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs index e95876299ab..e97dc38f260 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs @@ -21,11 +21,9 @@ public class HttpRequestBufferOptions public int PerRequestBufferSizeInBytes { get; set; } = 5_000_000; #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() -#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// - /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. + /// Gets the collection of used for filtering log messages for the purpose of further buffering. /// - public List Rules { get; set; } = []; -#pragma warning restore CA2227 // Collection properties should be read only + public List Rules { get; } = []; #pragma warning restore CA1002 // Do not expose generic lists } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs index 9cb555d7c9b..139d27e081e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs @@ -1,7 +1,6 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -29,14 +28,14 @@ public BufferFilterRule() /// The category name to use in this filter rule. /// The to use in this filter rule. /// The to use in this filter rule. - /// The optional filter delegate to use if a log message passes other filters. + /// The optional attributes to use if a log message passes other filters. public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId, - Func>, bool>? filter = null) + IReadOnlyList>? attributes = null) { Category = categoryName; LogLevel = logLevel; EventId = eventId; - Filter = filter ?? ((_, _, _, _) => true); + Attributes = attributes ?? []; } /// @@ -49,5 +48,5 @@ public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId, public int? EventId { get; set; } /// - public Func>, bool> Filter { get; } + public IReadOnlyList> Attributes { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 49c8706640b..4c2861c47d9 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -130,8 +130,8 @@ private bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRea return false; } - LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); + LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); - return rule is not null && rule.Filter(category, logLevel, eventId, attributes); + return rule is not null; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs index d847f8a59d9..d1cea3e2adb 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs @@ -35,6 +35,16 @@ public void Configure(GlobalBufferOptions options) return; } + if (parsedOptions.LogRecordSizeInBytes > 0) + { + options.LogRecordSizeInBytes = parsedOptions.LogRecordSizeInBytes; + } + + if (parsedOptions.BufferSizeInBytes > 0) + { + options.BufferSizeInBytes = parsedOptions.BufferSizeInBytes; + } + options.Rules.AddRange(parsedOptions.Rules); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs index b55ec90e308..61be85f88c3 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -36,11 +36,9 @@ public class GlobalBufferOptions public int BufferSizeInBytes { get; set; } = 500_000_000; #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() -#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// - /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. + /// Gets the collection of used for filtering log messages for the purpose of further buffering. /// - public List Rules { get; set; } = []; -#pragma warning restore CA2227 // Collection properties should be read only + public List Rules { get; } = []; #pragma warning restore CA1002 // Do not expose generic lists } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs index 59d32c992c0..07a4bb11f13 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs @@ -1,7 +1,6 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; @@ -30,7 +29,7 @@ public interface ILoggerFilterRule int? EventId { get; } /// - /// Gets the filter delegate that would be additionally applied to messages that passed the , , and filters. + /// Gets the log state attributes of messages where this rules applies to. /// - Func>, bool> Filter { get; } + IReadOnlyList> Attributes { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs index 94397b4e9b2..6801986a587 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Logging; @@ -26,8 +27,10 @@ public static class LoggerFilterRuleSelector /// The category of the log event. /// The log level of the log event. /// The event id of the log event. + /// The log state attributes of the log event. /// The best rule that matches the log event. - public static void Select(IList rules, string category, LogLevel logLevel, EventId eventId, out T? bestRule) + public static void Select(IList rules, string category, LogLevel logLevel, + EventId eventId, IReadOnlyList>? attributes, out T? bestRule) where T : class, ILoggerFilterRule { bestRule = null; @@ -44,7 +47,7 @@ public static void Select(IList rules, string category, LogLevel logLevel, { foreach (T rule in rules) { - if (IsBetter(rule, current, category, logLevel, eventId)) + if (IsBetter(rule, current, category, logLevel, eventId, attributes)) { current = rule; } @@ -57,7 +60,7 @@ public static void Select(IList rules, string category, LogLevel logLevel, } } - private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId) + private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId, IReadOnlyList>? attributes) where T : class, ILoggerFilterRule { // Skip rules with inapplicable log level @@ -104,6 +107,18 @@ private static bool IsBetter(T rule, T? current, string category, LogLevel lo } } + // Skip rules with inapplicable attributes + if (rule.Attributes.Count > 0 && attributes?.Count > 0) + { + foreach (KeyValuePair ruleAttribute in rule.Attributes) + { + if (!attributes.Contains(ruleAttribute)) + { + return false; + } + } + } + // Decide whose category is better - rule vs current if (current?.Category != null) { @@ -141,6 +156,20 @@ private static bool IsBetter(T rule, T? current, string category, LogLevel lo } } + // Decide whose attributes are better - rule vs current + if (current?.Attributes.Count > 0) + { + if (rule?.Attributes.Count == 0) + { + return false; + } + + if (rule?.Attributes.Count < current.Attributes.Count) + { + return false; + } + } + return true; } } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index 6ff9c6c274b..88dc5f51559 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -40,11 +40,11 @@ public void WhenArgumentNull_Throws() } [Fact] - public void AddGlobalBufferConfiguration_RegistersInDI() + public void AddGlobalBuffer_WithConfiguration_RegistersInDI() { List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, [new("region", "westus2")]), new BufferFilterRule(null, LogLevel.Information, null, null), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); @@ -53,12 +53,19 @@ public void AddGlobalBufferConfiguration_RegistersInDI() var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { - builder.AddGlobalBufferConfiguration(configuration); + builder.AddGlobalBuffer(configuration); + builder.Services.Configure(options => + { + options.LogRecordSizeInBytes = 33; + }); }); var serviceProvider = serviceCollection.BuildServiceProvider(); var options = serviceProvider.GetService>(); Assert.NotNull(options); Assert.NotNull(options.CurrentValue); + Assert.Equal(33, options.CurrentValue.LogRecordSizeInBytes); // value comes from the Configure() call + Assert.Equal(1000, options.CurrentValue.BufferSizeInBytes); // value comes from appsettings.json + Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.SuspendAfterFlushDuration); // value comes from default Assert.Equivalent(expectedData, options.CurrentValue.Rules); } } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs index 86a2f14ad7a..2b1551c8e88 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -24,7 +24,9 @@ public void SelectsRightRule() new BufferFilterRule(null, LogLevel.Warning, 1, null), new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1), // the best rule + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region2", "westus2")]), // inapplicable key + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus3")]), // inapplicable value + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule - [11] new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 2, null), new BufferFilterRule("Program.MyLogger", null, 1, null), new BufferFilterRule(null, LogLevel.Warning, 1, null), @@ -35,10 +37,10 @@ public void SelectsRightRule() // Act LoggerFilterRuleSelector.Select( - rules, "Program.MyLogger", LogLevel.Warning, 1, out var actualResult); + rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert - Assert.Same(rules[9], actualResult); + Assert.Same(rules[11], actualResult); } [Fact] @@ -56,12 +58,12 @@ public void WhenManyRuleApply_SelectsLast() new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1), // the best rule - new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1), // same as the best, but last and should be selected + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule + new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, [new("region", "westus2")]), // same as the best, but last and should be selected }; // Act - LoggerFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, out var actualResult); + LoggerFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert Assert.Same(rules.Last(), actualResult); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json index 9cbf9c7b85c..620729a11c2 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -18,11 +18,19 @@ } }, "Buffering": { + "LogRecordSizeInBytes": 100, + "BufferSizeInBytes": 1000, "Rules": [ { "Category": "Program.MyLogger", "LogLevel": "Information", - "EventId": 1 + "EventId": 1, + "Attributes": [ + { + "key": "region", + "value": "westus2" + } + ] }, { "LogLevel": "Information" From e96277f1d4c1d7de669bafc3670161e909a88cbf Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:56:12 +0100 Subject: [PATCH 11/15] Add http buffer holder PR comments --- .../Buffering/HttpRequestBuffer.cs | 12 +-- .../Buffering/HttpRequestBufferHolder.cs | 24 +++++ ...ttpRequestBufferLoggerBuilderExtensions.cs | 3 +- .../Buffering/HttpRequestBufferManager.cs | 32 +++---- .../Buffering/SerializedLogRecord.cs | 90 +++++++++++++++++++ .../Buffering/BufferFilterRule.cs | 18 ++-- .../BufferFilterRuleSelector.cs} | 28 +++--- .../Buffering/GlobalBuffer.cs | 4 +- .../Buffering/GlobalBufferConfigureOptions.cs | 4 +- .../Buffering/GlobalBufferManager.cs | 4 +- .../Buffering/GlobalBufferOptions.cs | 5 +- .../Buffering/IBufferManager.cs | 6 +- .../Buffering/ILoggingBuffer.cs | 6 +- .../Buffering/SerializedLogRecord.cs | 5 +- .../ILoggerFilterRule.cs | 35 -------- .../Logging/LoggingEnrichmentExtensions.cs | 5 +- ...lobalBufferLoggerBuilderExtensionsTests.cs | 4 +- .../LoggerFilterRuleSelectorTests.cs | 8 +- .../appsettings.json | 2 +- 19 files changed, 180 insertions(+), 115 deletions(-) create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs rename src/Libraries/Microsoft.Extensions.Telemetry/{LoggerFilterRuleSelector.cs => Buffering/BufferFilterRuleSelector.cs} (82%) delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index ef5fb6db567..35a1893e43b 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -39,12 +39,12 @@ public bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - TState attributes, + TState state, Exception? exception, Func formatter) { SerializedLogRecord serializedLogRecord = default; - if (attributes is ModernTagJoiner modernTagJoiner) + if (state is ModernTagJoiner modernTagJoiner) { if (!IsEnabled(category, logLevel, eventId, modernTagJoiner)) { @@ -54,7 +54,7 @@ public bool TryEnqueue( serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, ((Func)(object)formatter)(modernTagJoiner, exception)); } - else if (attributes is LegacyTagJoiner legacyTagJoiner) + else if (state is LegacyTagJoiner legacyTagJoiner) { if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner)) { @@ -66,10 +66,10 @@ public bool TryEnqueue( } else { - Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}"); + Throw.ArgumentException(nameof(state), $"Unsupported type of the log state object detected: {typeof(TState)}"); } - if (serializedLogRecord.SizeInBytes > _globalOptions.CurrentValue.LogRecordSizeInBytes) + if (serializedLogRecord.SizeInBytes > _globalOptions.CurrentValue.MaxLogRecordSizeInBytes) { return false; } @@ -113,7 +113,7 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRead return false; } - LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); + BufferFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); return rule is not null; } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs new file mode 100644 index 00000000000..4200b5e5494 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs @@ -0,0 +1,24 @@ +// 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.Concurrent; +using Microsoft.Extensions.Diagnostics.Buffering; + +namespace Microsoft.AspNetCore.Diagnostics.Buffering; + +internal sealed class HttpRequestBufferHolder +{ + private readonly ConcurrentDictionary _buffers = new(); + + public ILoggingBuffer GetOrAdd(string category, Func valueFactory) => + _buffers.GetOrAdd(category, valueFactory); + + public void Flush() + { + foreach (var buffer in _buffers.Values) + { + buffer.Flush(); + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs index bf0a2b37a69..8c53955220f 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs @@ -73,8 +73,7 @@ internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder { _ = Throw.IfNull(builder); - _ = builder.Services.AddExtendedLoggerFeactory(); - + builder.Services.TryAddScoped(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index d49795403be..e0c48bf691a 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Buffering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -33,16 +34,7 @@ public HttpRequestBufferManager( public void FlushCurrentRequestLogs() { - if (_httpContextAccessor.HttpContext is not null) - { - foreach (var kvp in _httpContextAccessor.HttpContext!.Items) - { - if (kvp.Value is ILoggingBuffer buffer) - { - buffer.Flush(); - } - } - } + _httpContextAccessor.HttpContext?.RequestServices.GetService()?.Flush(); } public bool TryEnqueue( @@ -50,28 +42,24 @@ public bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - TState attributes, + TState state, Exception? exception, Func formatter) { - var httpContext = _httpContextAccessor.HttpContext; + HttpContext? httpContext = _httpContextAccessor.HttpContext; if (httpContext is null) { - return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, attributes, exception, formatter); + return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter); } - if (!httpContext.Items.TryGetValue(category, out var buffer)) - { - var httpRequestBuffer = new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions); - httpContext.Items[category] = httpRequestBuffer; - buffer = httpRequestBuffer; - } + HttpRequestBufferHolder? bufferHolder = httpContext.RequestServices.GetService(); + ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!); - if (buffer is not ILoggingBuffer loggingBuffer) + if (buffer is null) { - throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}"); + return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter); } - return loggingBuffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); + return buffer.TryEnqueue(logLevel, category, eventId, state, exception, formatter); } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs new file mode 100644 index 00000000000..2ec9b051399 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs @@ -0,0 +1,90 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Diagnostics.Buffering; + +/// +/// Represents a log record that has been serialized for purposes of buffering or similar. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +internal readonly struct SerializedLogRecord +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + /// + /// Initializes a new instance of the struct. + /// + /// Logging severity level. + /// Event ID. + /// The time when the log record was first created. + /// The set of name/value pairs associated with the record. + /// An exception string for this record. + /// The formatted log message. + public SerializedLogRecord( + LogLevel logLevel, + EventId eventId, + DateTimeOffset timestamp, + IReadOnlyList> attributes, + Exception? exception, + string formattedMessage) + { + LogLevel = logLevel; + EventId = eventId; + Timestamp = timestamp; + + List> serializedAttributes = []; + if (attributes is not null) + { + serializedAttributes = new List>(attributes.Count); + for (int i = 0; i < attributes.Count; i++) + { + string value = attributes[i].Value?.ToString() ?? string.Empty; + serializedAttributes.Add(new KeyValuePair(attributes[i].Key, value)); + SizeInBytes += value.Length * sizeof(char); + } + + Exception = exception?.Message; + } + + Attributes = serializedAttributes; + FormattedMessage = formattedMessage; + } + + /// + /// Gets the variable set of name/value pairs associated with the record. + /// + public IReadOnlyList> Attributes { get; } + + /// + /// Gets the formatted log message. + /// + public string? FormattedMessage { get; } + + /// + /// Gets an exception string for this record. + /// + public string? Exception { get; } + + /// + /// Gets the time when the log record was first created. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Gets the record's logging severity level. + /// + public LogLevel LogLevel { get; } + + /// + /// Gets the record's event ID. + /// + public EventId EventId { get; } + + /// + /// Gets the approximate size of the serialized log record in bytes. + /// + public int SizeInBytes { get; init; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs index 139d27e081e..63d16241232 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// Defines a rule used to filter log messages for purposes of futher buffering. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public class BufferFilterRule : ILoggerFilterRule +public class BufferFilterRule { /// /// Initializes a new instance of the class. @@ -38,15 +38,23 @@ public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId, Attributes = attributes ?? []; } - /// + /// + /// Gets or sets the logger category this rule applies to. + /// public string? Category { get; set; } - /// + /// + /// Gets or sets the maximum of messages. + /// public LogLevel? LogLevel { get; set; } - /// + /// + /// Gets or sets the of messages where this rule applies to. + /// public int? EventId { get; set; } - /// + /// + /// Gets or sets the log state attributes of messages where this rules applies to. + /// public IReadOnlyList> Attributes { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs index 6801986a587..b8b23265b86 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/LoggerFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs @@ -7,31 +7,27 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.Shared.DiagnosticIds; +using Microsoft.Extensions.Logging; -namespace Microsoft.Extensions.Logging; +namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Selects the best rule from the list of rules for a given log event. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public static class LoggerFilterRuleSelector +internal static class BufferFilterRuleSelector { /// - /// Selects the best rule from the list of rules for a given log event. + /// Selects the best rule from the list of rules for a given log event. /// - /// The type of the rules. /// The list of rules to select from. /// The category of the log event. /// The log level of the log event. /// The event id of the log event. /// The log state attributes of the log event. /// The best rule that matches the log event. - public static void Select(IList rules, string category, LogLevel logLevel, - EventId eventId, IReadOnlyList>? attributes, out T? bestRule) - where T : class, ILoggerFilterRule + public static void Select(IList rules, string category, LogLevel logLevel, + EventId eventId, IReadOnlyList>? attributes, out BufferFilterRule? bestRule) { bestRule = null; @@ -42,10 +38,10 @@ public static void Select(IList rules, string category, LogLevel logLevel, // 3. If there is only one rule use it // 4. If there are multiple rules use last - T? current = null; + BufferFilterRule? current = null; if (rules is not null) { - foreach (T rule in rules) + foreach (BufferFilterRule rule in rules) { if (IsBetter(rule, current, category, logLevel, eventId, attributes)) { @@ -60,8 +56,8 @@ public static void Select(IList rules, string category, LogLevel logLevel, } } - private static bool IsBetter(T rule, T? current, string category, LogLevel logLevel, EventId eventId, IReadOnlyList>? attributes) - where T : class, ILoggerFilterRule + private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, string category, + LogLevel logLevel, EventId eventId, IReadOnlyList>? attributes) { // Skip rules with inapplicable log level if (rule.LogLevel != null && rule.LogLevel < logLevel) @@ -159,12 +155,12 @@ private static bool IsBetter(T rule, T? current, string category, LogLevel lo // Decide whose attributes are better - rule vs current if (current?.Attributes.Count > 0) { - if (rule?.Attributes.Count == 0) + if (rule.Attributes.Count == 0) { return false; } - if (rule?.Attributes.Count < current.Attributes.Count) + if (rule.Attributes.Count < current.Attributes.Count) { return false; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index 4c2861c47d9..b828a04ba2a 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -68,7 +68,7 @@ public bool TryEnqueue( Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(T)}"); } - if (serializedLogRecord.SizeInBytes > _options.CurrentValue.LogRecordSizeInBytes) + if (serializedLogRecord.SizeInBytes > _options.CurrentValue.MaxLogRecordSizeInBytes) { return false; } @@ -130,7 +130,7 @@ private bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRea return false; } - LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); + BufferFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); return rule is not null; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs index d1cea3e2adb..f32955c246e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs @@ -35,9 +35,9 @@ public void Configure(GlobalBufferOptions options) return; } - if (parsedOptions.LogRecordSizeInBytes > 0) + if (parsedOptions.MaxLogRecordSizeInBytes > 0) { - options.LogRecordSizeInBytes = parsedOptions.LogRecordSizeInBytes; + options.MaxLogRecordSizeInBytes = parsedOptions.MaxLogRecordSizeInBytes; } if (parsedOptions.BufferSizeInBytes > 0) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index d2f9eef254a..3f1ea4e402d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -38,11 +38,11 @@ public bool TryEnqueue( IBufferedLogger bufferedLogger, LogLevel logLevel, string category, - EventId eventId, TState attributes, + EventId eventId, TState state, Exception? exception, Func formatter) { var buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); - return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); + return buffer.TryEnqueue(logLevel, category, eventId, state, exception, formatter); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs index 61be85f88c3..79fc3691d8f 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs @@ -27,10 +27,11 @@ public class GlobalBufferOptions /// Gets or sets the maxiumum size of each individual log record in bytes. If the size of a log record exceeds this limit, it won't be buffered. /// /// TO DO: add validation. - public int LogRecordSizeInBytes { get; set; } = 50_000; + public int MaxLogRecordSizeInBytes { get; set; } = 50_000; /// - /// Gets or sets the maximum size of the buffer in bytes. If the buffer size exceeds this limit, the oldest buffered log records will be dropped. + /// Gets or sets the maximum size of the buffer in bytes. If adding a new log entry would cause the buffer size to exceed this limit, + /// the oldest buffered log records will be dropped to make room. /// /// TO DO: add validation. public int BufferSizeInBytes { get; set; } = 500_000_000; diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs index eda0f16327e..da48c9b20fc 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs @@ -22,17 +22,17 @@ public interface IBufferManager /// Log level. /// Category. /// Event ID. - /// Attributes. + /// Log state attributes. /// Exception. /// Formatter delegate. - /// Type of the instance. + /// Type of the instance. /// if the log record was buffered; otherwise, . bool TryEnqueue( IBufferedLogger bufferedLoger, LogLevel logLevel, string category, EventId eventId, - TState attributes, + TState state, Exception? exception, Func formatter); } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index c23835f2ffc..b20c37d15b8 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -20,16 +20,16 @@ public interface ILoggingBuffer /// Log level. /// Category. /// Event ID. - /// Attributes. + /// Log state attributes. /// Exception. /// Formatter delegate. - /// Type of the instance. + /// Type of the instance. /// if the log record was buffered; otherwise, . bool TryEnqueue( LogLevel logLevel, string category, EventId eventId, - TState attributes, + TState state, Exception? exception, Func formatter); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs index 47744fedcf2..3592d734632 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -13,8 +11,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// Represents a log record that has been serialized for purposes of buffering or similar. /// #pragma warning disable CA1815 // Override equals and operator equals on value types -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public readonly struct SerializedLogRecord +internal readonly struct SerializedLogRecord #pragma warning restore CA1815 // Override equals and operator equals on value types { /// diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs deleted file mode 100644 index 07a4bb11f13..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/ILoggerFilterRule.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Logging; - -/// -/// Represents a rule used for filtering log messages for purposes of log sampling and buffering. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface ILoggerFilterRule -{ - /// - /// Gets the logger category this rule applies to. - /// - string? Category { get; } - - /// - /// Gets the maximum of messages. - /// - LogLevel? LogLevel { get; } - - /// - /// Gets the of messages where this rule applies to. - /// - int? EventId { get; } - - /// - /// Gets the log state attributes of messages where this rules applies to. - /// - IReadOnlyList> Attributes { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs index cf68d2ea10b..87563067283 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEnrichmentExtensions.cs @@ -2,12 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Logging; @@ -67,8 +65,7 @@ public static ILoggingBuilder EnableEnrichment(this ILoggingBuilder builder, ICo /// /// The . /// The value of . - [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] - public static IServiceCollection AddExtendedLoggerFeactory(this IServiceCollection services) + internal static IServiceCollection AddExtendedLoggerFeactory(this IServiceCollection services) { _ = Throw.IfNull(services); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index 88dc5f51559..278cf007904 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -56,14 +56,14 @@ public void AddGlobalBuffer_WithConfiguration_RegistersInDI() builder.AddGlobalBuffer(configuration); builder.Services.Configure(options => { - options.LogRecordSizeInBytes = 33; + options.MaxLogRecordSizeInBytes = 33; }); }); var serviceProvider = serviceCollection.BuildServiceProvider(); var options = serviceProvider.GetService>(); Assert.NotNull(options); Assert.NotNull(options.CurrentValue); - Assert.Equal(33, options.CurrentValue.LogRecordSizeInBytes); // value comes from the Configure() call + Assert.Equal(33, options.CurrentValue.MaxLogRecordSizeInBytes); // value comes from the Configure() call Assert.Equal(1000, options.CurrentValue.BufferSizeInBytes); // value comes from appsettings.json Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.SuspendAfterFlushDuration); // value comes from default Assert.Equivalent(expectedData, options.CurrentValue.Rules); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs index 2b1551c8e88..90c4e1b4ad0 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -13,7 +13,7 @@ public class LoggerFilterRuleSelectorTests public void SelectsRightRule() { // Arrange - var rules = new List + var rules = new List { new BufferFilterRule(null, null, null, null), new BufferFilterRule(null, null, 1, null), @@ -36,7 +36,7 @@ public void SelectsRightRule() }; // Act - LoggerFilterRuleSelector.Select( + BufferFilterRuleSelector.Select( rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert @@ -47,7 +47,7 @@ public void SelectsRightRule() public void WhenManyRuleApply_SelectsLast() { // Arrange - var rules = new List + var rules = new List { new BufferFilterRule(null, LogLevel.Information, 1, null), new BufferFilterRule(null, LogLevel.Information, 1, null), @@ -63,7 +63,7 @@ public void WhenManyRuleApply_SelectsLast() }; // Act - LoggerFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); + BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert Assert.Same(rules.Last(), actualResult); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json index 620729a11c2..1fa37a89e54 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -18,7 +18,7 @@ } }, "Buffering": { - "LogRecordSizeInBytes": 100, + "MaxLogRecordSizeInBytes": 100, "BufferSizeInBytes": 1000, "Rules": [ { From a79fcbfda0b4501a1e155cfe5453578b0d397f8c Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:32:05 +0100 Subject: [PATCH 12/15] Make ILoggingBuffer and DeserializedLogRecord types internal --- .../Buffering/DeserializedLogRecord.cs | 64 +++++++++++++++++++ .../Buffering/HttpRequestBufferHolder.cs | 1 - .../Buffering/ILoggingBuffer.cs | 37 +++++++++++ .../Buffering/DeserializedLogRecord.cs | 5 +- .../Buffering/ILoggingBuffer.cs | 5 +- 5 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs new file mode 100644 index 00000000000..e9a7db6a852 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs @@ -0,0 +1,64 @@ +// 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Diagnostics.Buffering; + +/// +/// Represents a log record deserialized from somewhere, such as buffer. +/// +internal sealed class DeserializedLogRecord : BufferedLogRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The time when the log record was first created. + /// Logging severity level. + /// Event ID. + /// An exception string for this record. + /// The formatted log message. + /// The set of name/value pairs associated with the record. + public DeserializedLogRecord( + DateTimeOffset timestamp, + LogLevel logLevel, + EventId eventId, + string? exception, + string? formattedMessage, + IReadOnlyList> attributes) + { + _timestamp = timestamp; + _logLevel = logLevel; + _eventId = eventId; + _exception = exception; + _formattedMessage = formattedMessage; + _attributes = attributes; + } + + /// + public override DateTimeOffset Timestamp => _timestamp; + private DateTimeOffset _timestamp; + + /// + public override LogLevel LogLevel => _logLevel; + private LogLevel _logLevel; + + /// + public override EventId EventId => _eventId; + private EventId _eventId; + + /// + public override string? Exception => _exception; + private string? _exception; + + /// + public override string? FormattedMessage => _formattedMessage; + private string? _formattedMessage; + + /// + public override IReadOnlyList> Attributes => _attributes; + private IReadOnlyList> _attributes; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs index 4200b5e5494..2c5184ba608 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Concurrent; -using Microsoft.Extensions.Diagnostics.Buffering; namespace Microsoft.AspNetCore.Diagnostics.Buffering; diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs new file mode 100644 index 00000000000..5f25ce0f729 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.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 Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Diagnostics.Buffering; + +/// +/// Interface for a logging buffer. +/// +internal interface ILoggingBuffer +{ + /// + /// Enqueues a log record in the underlying buffer. + /// + /// Log level. + /// Category. + /// Event ID. + /// Log state attributes. + /// Exception. + /// Formatter delegate. + /// Type of the instance. + /// if the log record was buffered; otherwise, . + bool TryEnqueue( + LogLevel logLevel, + string category, + EventId eventId, + TState state, + Exception? exception, + Func formatter); + + /// + /// Flushes the buffer. + /// + void Flush(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs index cf760364e71..e7d6bf8f345 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs @@ -3,18 +3,15 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Represents a log record deserialized from somewhere, such as buffer. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class DeserializedLogRecord : BufferedLogRecord +internal sealed class DeserializedLogRecord : BufferedLogRecord { /// /// Initializes a new instance of the class. diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs index b20c37d15b8..8cc1b1f0d90 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs @@ -2,17 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Interface for a logging buffer. /// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface ILoggingBuffer +internal interface ILoggingBuffer { /// /// Enqueues a log record in the underlying buffer. From 8a91c1529b7662fdb1969a5cca1ef725fd047b32 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:09:38 +0100 Subject: [PATCH 13/15] Move shared files to Shared project and add more tests --- eng/MSBuild/Shared.props | 4 + .../Buffering/DeserializedLogRecord.cs | 64 ------------- .../Buffering/HttpRequestBufferHolder.cs | 1 + .../Buffering/ILoggingBuffer.cs | 37 -------- .../Buffering/SerializedLogRecord.cs | 90 ------------------- ...t.AspNetCore.Diagnostics.Middleware.csproj | 6 +- .../Microsoft.Extensions.Telemetry.csproj | 9 +- .../DeserializedLogRecord.cs | 3 +- .../LoggingBuffering}/ILoggingBuffer.cs | 0 .../LoggingBuffering}/SerializedLogRecord.cs | 21 +++-- src/Shared/Shared.csproj | 1 + ...lobalBufferLoggerBuilderExtensionsTests.cs | 2 +- .../LoggerFilterRuleSelectorTests.cs | 18 ++++ .../appsettings.json | 4 + 14 files changed, 55 insertions(+), 205 deletions(-) delete mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs delete mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs delete mode 100644 src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs rename src/{Libraries/Microsoft.Extensions.Telemetry/Buffering => Shared/LoggingBuffering}/DeserializedLogRecord.cs (97%) rename src/{Libraries/Microsoft.Extensions.Telemetry/Buffering => Shared/LoggingBuffering}/ILoggingBuffer.cs (100%) rename src/{Libraries/Microsoft.Extensions.Telemetry/Buffering => Shared/LoggingBuffering}/SerializedLogRecord.cs (86%) diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props index dee583f7e39..6bf0fb6abc4 100644 --- a/eng/MSBuild/Shared.props +++ b/eng/MSBuild/Shared.props @@ -46,4 +46,8 @@ + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs deleted file mode 100644 index e9a7db6a852..00000000000 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/DeserializedLogRecord.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Diagnostics.Buffering; - -/// -/// Represents a log record deserialized from somewhere, such as buffer. -/// -internal sealed class DeserializedLogRecord : BufferedLogRecord -{ - /// - /// Initializes a new instance of the class. - /// - /// The time when the log record was first created. - /// Logging severity level. - /// Event ID. - /// An exception string for this record. - /// The formatted log message. - /// The set of name/value pairs associated with the record. - public DeserializedLogRecord( - DateTimeOffset timestamp, - LogLevel logLevel, - EventId eventId, - string? exception, - string? formattedMessage, - IReadOnlyList> attributes) - { - _timestamp = timestamp; - _logLevel = logLevel; - _eventId = eventId; - _exception = exception; - _formattedMessage = formattedMessage; - _attributes = attributes; - } - - /// - public override DateTimeOffset Timestamp => _timestamp; - private DateTimeOffset _timestamp; - - /// - public override LogLevel LogLevel => _logLevel; - private LogLevel _logLevel; - - /// - public override EventId EventId => _eventId; - private EventId _eventId; - - /// - public override string? Exception => _exception; - private string? _exception; - - /// - public override string? FormattedMessage => _formattedMessage; - private string? _formattedMessage; - - /// - public override IReadOnlyList> Attributes => _attributes; - private IReadOnlyList> _attributes; -} diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs index 2c5184ba608..4200b5e5494 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; +using Microsoft.Extensions.Diagnostics.Buffering; namespace Microsoft.AspNetCore.Diagnostics.Buffering; diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs deleted file mode 100644 index 5f25ce0f729..00000000000 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/ILoggingBuffer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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 Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Diagnostics.Buffering; - -/// -/// Interface for a logging buffer. -/// -internal interface ILoggingBuffer -{ - /// - /// Enqueues a log record in the underlying buffer. - /// - /// Log level. - /// Category. - /// Event ID. - /// Log state attributes. - /// Exception. - /// Formatter delegate. - /// Type of the instance. - /// if the log record was buffered; otherwise, . - bool TryEnqueue( - LogLevel logLevel, - string category, - EventId eventId, - TState state, - Exception? exception, - Func formatter); - - /// - /// Flushes the buffer. - /// - void Flush(); -} diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs deleted file mode 100644 index 2ec9b051399..00000000000 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/SerializedLogRecord.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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 Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Diagnostics.Buffering; - -/// -/// Represents a log record that has been serialized for purposes of buffering or similar. -/// -#pragma warning disable CA1815 // Override equals and operator equals on value types -internal readonly struct SerializedLogRecord -#pragma warning restore CA1815 // Override equals and operator equals on value types -{ - /// - /// Initializes a new instance of the struct. - /// - /// Logging severity level. - /// Event ID. - /// The time when the log record was first created. - /// The set of name/value pairs associated with the record. - /// An exception string for this record. - /// The formatted log message. - public SerializedLogRecord( - LogLevel logLevel, - EventId eventId, - DateTimeOffset timestamp, - IReadOnlyList> attributes, - Exception? exception, - string formattedMessage) - { - LogLevel = logLevel; - EventId = eventId; - Timestamp = timestamp; - - List> serializedAttributes = []; - if (attributes is not null) - { - serializedAttributes = new List>(attributes.Count); - for (int i = 0; i < attributes.Count; i++) - { - string value = attributes[i].Value?.ToString() ?? string.Empty; - serializedAttributes.Add(new KeyValuePair(attributes[i].Key, value)); - SizeInBytes += value.Length * sizeof(char); - } - - Exception = exception?.Message; - } - - Attributes = serializedAttributes; - FormattedMessage = formattedMessage; - } - - /// - /// Gets the variable set of name/value pairs associated with the record. - /// - public IReadOnlyList> Attributes { get; } - - /// - /// Gets the formatted log message. - /// - public string? FormattedMessage { get; } - - /// - /// Gets an exception string for this record. - /// - public string? Exception { get; } - - /// - /// Gets the time when the log record was first created. - /// - public DateTimeOffset Timestamp { get; } - - /// - /// Gets the record's logging severity level. - /// - public LogLevel LogLevel { get; } - - /// - /// Gets the record's event ID. - /// - public EventId EventId { get; } - - /// - /// Gets the approximate size of the serialized log record in bytes. - /// - public int SizeInBytes { get; init; } -} diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj index 75b3604fc63..0c42f2575ec 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj @@ -11,14 +11,14 @@ true true true - false - false + true true false false true false - true + false + false diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj index 174cb10fadc..6c19aab939e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -8,15 +8,16 @@ true true + true true - true - true true + true + true + true true true true - true - true + true true true $(NoWarn);IL2026 diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs b/src/Shared/LoggingBuffering/DeserializedLogRecord.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs rename to src/Shared/LoggingBuffering/DeserializedLogRecord.cs index e7d6bf8f345..02a5cf1712d 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/DeserializedLogRecord.cs +++ b/src/Shared/LoggingBuffering/DeserializedLogRecord.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - +#if !SHARED_PROJECT || NET9_0_OR_GREATER using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; @@ -62,3 +62,4 @@ public DeserializedLogRecord( public override IReadOnlyList> Attributes => _attributes; private IReadOnlyList> _attributes; } +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs b/src/Shared/LoggingBuffering/ILoggingBuffer.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBuffer.cs rename to src/Shared/LoggingBuffering/ILoggingBuffer.cs diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs b/src/Shared/LoggingBuffering/SerializedLogRecord.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs rename to src/Shared/LoggingBuffering/SerializedLogRecord.cs index 3592d734632..561220e2094 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/SerializedLogRecord.cs +++ b/src/Shared/LoggingBuffering/SerializedLogRecord.cs @@ -10,9 +10,8 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Represents a log record that has been serialized for purposes of buffering or similar. /// -#pragma warning disable CA1815 // Override equals and operator equals on value types +#pragma warning disable CA1815 // Override equals and operator equals on value types - not used for this struct, would be dead code internal readonly struct SerializedLogRecord -#pragma warning restore CA1815 // Override equals and operator equals on value types { /// /// Initializes a new instance of the struct. @@ -41,16 +40,28 @@ public SerializedLogRecord( serializedAttributes = new List>(attributes.Count); for (int i = 0; i < attributes.Count; i++) { + string key = attributes[i].Key; string value = attributes[i].Value?.ToString() ?? string.Empty; - serializedAttributes.Add(new KeyValuePair(attributes[i].Key, value)); + serializedAttributes.Add(new KeyValuePair(key, value)); + + SizeInBytes += key.Length * sizeof(char); SizeInBytes += value.Length * sizeof(char); } - - Exception = exception?.Message; } Attributes = serializedAttributes; + + Exception = exception?.Message; + if (Exception is not null) + { + SizeInBytes += Exception.Length * sizeof(char); + } + FormattedMessage = formattedMessage; + if (FormattedMessage is not null) + { + SizeInBytes += FormattedMessage.Length * sizeof(char); + } } /// diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 439c3788557..d25c011a05f 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -29,6 +29,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index 278cf007904..9a02d8f254c 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -44,7 +44,7 @@ public void AddGlobalBuffer_WithConfiguration_RegistersInDI() { List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, [new("region", "westus2")]), + new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, [new("region", "westus2"), new ("priority", 1)]), new BufferFilterRule(null, LogLevel.Information, null, null), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs index 90c4e1b4ad0..9279a778713 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -68,4 +68,22 @@ public void WhenManyRuleApply_SelectsLast() // Assert Assert.Same(rules.Last(), actualResult); } + + [Fact] + public void CanWorkWithValueTypeAttributes() + { + // Arrange + var rules = new List + { + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)]), // the best rule + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 3)]), + new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), + }; + + // Act + BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)], out var actualResult); + + // Assert + Assert.Same(rules.First(), actualResult); + } } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json index 1fa37a89e54..d70df4596ba 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -29,6 +29,10 @@ { "key": "region", "value": "westus2" + }, + { + "key": "priority", + "value": 1 } ] }, From 4f524ebbdc597801c5b99bfbb46fed7febc2bb67 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:48:12 +0100 Subject: [PATCH 14/15] Add custom equality comparer --- .../Buffering/BufferFilterRuleSelector.cs | 4 ++- .../Buffering/StringifyComprarer.cs | 34 +++++++++++++++++++ .../LoggerFilterRuleSelectorTests.cs | 7 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs index b8b23265b86..237a7e2b242 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs @@ -17,6 +17,8 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// internal static class BufferFilterRuleSelector { + private static readonly IEqualityComparer> _stringifyComparer = new StringifyComprarer(); + /// /// Selects the best rule from the list of rules for a given log event. /// @@ -108,7 +110,7 @@ private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, s { foreach (KeyValuePair ruleAttribute in rule.Attributes) { - if (!attributes.Contains(ruleAttribute)) + if (!attributes.Contains(ruleAttribute, _stringifyComparer)) { return false; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs new file mode 100644 index 00000000000..dc6980333a2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Microsoft.Extensions.Diagnostics.Buffering; +internal sealed class StringifyComprarer : IEqualityComparer> +{ + public bool Equals(KeyValuePair x, KeyValuePair y) + { + if (x.Key != y.Key) + { + return false; + } + + if (x.Value is null && y.Value is null) + { + return true; + } + + if (x.Value is null || y.Value is null) + { + return false; + } + + return x.Value.ToString() == y.Value.ToString(); + } + + public int GetHashCode(KeyValuePair obj) + { + return HashCode.Combine(obj.Key, obj.Value?.ToString()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs index 9279a778713..041408f7bd1 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -75,15 +75,16 @@ public void CanWorkWithValueTypeAttributes() // Arrange var rules = new List { + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 1)]), new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)]), // the best rule new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 3)]), - new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), + new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), }; // Act - BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)], out var actualResult); + BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", "2")], out var actualResult); // Assert - Assert.Same(rules.First(), actualResult); + Assert.Same(rules[1], actualResult); } } From b2b6e563f15b78727269fd787341666016376042 Mon Sep 17 00:00:00 2001 From: evgenyfedorov2 <25526458+evgenyfedorov2@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:35:52 +0100 Subject: [PATCH 15/15] Address API Review feedback --- .../Buffering/HttpRequestBuffer.cs | 38 ++++----- .../Buffering/HttpRequestBufferManager.cs | 39 ++++------ ...questBufferingLoggingBuilderExtensions.cs} | 77 +++++++++--------- ...fferManager.cs => HttpRequestLogBuffer.cs} | 13 +--- ...ttpRequestLogBufferingConfigureOptions.cs} | 14 ++-- ...s.cs => HttpRequestLogBufferingOptions.cs} | 14 ++-- .../Buffering/BufferFilterRule.cs | 60 -------------- .../Buffering/GlobalBuffer.cs | 36 ++++----- .../Buffering/GlobalBufferManager.cs | 23 ++---- ...lobalBufferingLoggingBuilderExtensions.cs} | 65 ++++++++-------- ... => GlobalLogBufferingConfigureOptions.cs} | 17 ++-- ...ptions.cs => GlobalLogBufferingOptions.cs} | 14 ++-- .../Buffering/IBufferManager.cs | 38 --------- .../Buffering/IGlobalBufferManager.cs | 19 ----- .../Buffering/LogBuffer.cs | 31 ++++++++ .../Buffering/LogBufferingFilterRule.cs | 63 +++++++++++++++ ...r.cs => LogBufferingFilterRuleSelector.cs} | 26 +++---- .../Buffering/StringifyComprarer.cs | 1 + .../Logging/ExtendedLogger.cs | 20 ++--- .../Logging/ExtendedLoggerFactory.cs | 8 +- .../Logging/LoggerConfig.cs | 6 +- src/Shared/LoggingBuffering/ILoggingBuffer.cs | 19 +---- ...questBufferLoggerBuilderExtensionsTests.cs | 15 ++-- .../Logging/AcceptanceTests.cs | 10 +-- .../appsettings.json | 5 +- ...lobalBufferLoggerBuilderExtensionsTests.cs | 24 +++--- .../LoggerFilterRuleSelectorTests.cs | 78 +++++++++---------- .../Logging/ExtendedLoggerTests.cs | 6 +- .../appsettings.json | 5 +- 29 files changed, 364 insertions(+), 420 deletions(-) rename src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/{HttpRequestBufferLoggerBuilderExtensions.cs => HttpRequestBufferingLoggingBuilderExtensions.cs} (51%) rename src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/{IHttpRequestBufferManager.cs => HttpRequestLogBuffer.cs} (58%) rename src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/{HttpRequestBufferConfigureOptions.cs => HttpRequestLogBufferingConfigureOptions.cs} (62%) rename src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/{HttpRequestBufferOptions.cs => HttpRequestLogBufferingOptions.cs} (58%) delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs rename src/Libraries/Microsoft.Extensions.Telemetry/Buffering/{GlobalBufferLoggerBuilderExtensions.cs => GlobalBufferingLoggingBuilderExtensions.cs} (55%) rename src/Libraries/Microsoft.Extensions.Telemetry/Buffering/{GlobalBufferConfigureOptions.cs => GlobalLogBufferingConfigureOptions.cs} (62%) rename src/Libraries/Microsoft.Extensions.Telemetry/Buffering/{GlobalBufferOptions.cs => GlobalLogBufferingOptions.cs} (75%) delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBuffer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs rename src/Libraries/Microsoft.Extensions.Telemetry/Buffering/{BufferFilterRuleSelector.cs => LogBufferingFilterRuleSelector.cs} (85%) diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs index 35a1893e43b..40c84f6850c 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs @@ -16,8 +16,8 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; internal sealed class HttpRequestBuffer : ILoggingBuffer { - private readonly IOptionsMonitor _options; - private readonly IOptionsMonitor _globalOptions; + private readonly IOptionsMonitor _options; + private readonly IOptionsMonitor _globalOptions; private readonly ConcurrentQueue _buffer; private readonly TimeProvider _timeProvider = TimeProvider.System; private readonly IBufferedLogger _bufferedLogger; @@ -26,8 +26,8 @@ internal sealed class HttpRequestBuffer : ILoggingBuffer private int _bufferSize; public HttpRequestBuffer(IBufferedLogger bufferedLogger, - IOptionsMonitor options, - IOptionsMonitor globalOptions) + IOptionsMonitor options, + IOptionsMonitor globalOptions) { _options = options; _globalOptions = globalOptions; @@ -35,38 +35,32 @@ public HttpRequestBuffer(IBufferedLogger bufferedLogger, _buffer = new ConcurrentQueue(); } - public bool TryEnqueue( - LogLevel logLevel, - string category, - EventId eventId, - TState state, - Exception? exception, - Func formatter) + public bool TryEnqueue(LogEntry logEntry) { SerializedLogRecord serializedLogRecord = default; - if (state is ModernTagJoiner modernTagJoiner) + if (logEntry.State is ModernTagJoiner modernTagJoiner) { - if (!IsEnabled(category, logLevel, eventId, modernTagJoiner)) + if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, modernTagJoiner)) { return false; } - serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, - ((Func)(object)formatter)(modernTagJoiner, exception)); + serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), modernTagJoiner, logEntry.Exception, + ((Func)(object)logEntry.Formatter)(modernTagJoiner, logEntry.Exception)); } - else if (state is LegacyTagJoiner legacyTagJoiner) + else if (logEntry.State is LegacyTagJoiner legacyTagJoiner) { - if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner)) + if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, legacyTagJoiner)) { return false; } - serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, - ((Func)(object)formatter)(legacyTagJoiner, exception)); + serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), legacyTagJoiner, logEntry.Exception, + ((Func)(object)logEntry.Formatter)(legacyTagJoiner, logEntry.Exception)); } else { - Throw.ArgumentException(nameof(state), $"Unsupported type of the log state object detected: {typeof(TState)}"); + Throw.InvalidOperationException($"Unsupported type of the log state object detected: {typeof(TState)}"); } if (serializedLogRecord.SizeInBytes > _globalOptions.CurrentValue.MaxLogRecordSizeInBytes) @@ -113,14 +107,14 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRead return false; } - BufferFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); + LogBufferingFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out LogBufferingFilterRule? rule); return rule is not null; } private void Trim() { - while (_bufferSize > _options.CurrentValue.PerRequestBufferSizeInBytes && _buffer.TryDequeue(out var item)) + while (_bufferSize > _options.CurrentValue.MaxPerRequestBufferSizeInBytes && _buffer.TryDequeue(out var item)) { _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes); } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs index e0c48bf691a..bc9a469cbb9 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs @@ -1,65 +1,56 @@ // 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 Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Buffering; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Diagnostics.Buffering; -internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager +internal sealed class HttpRequestBufferManager : HttpRequestLogBuffer { - private readonly IGlobalBufferManager _globalBufferManager; + private readonly LogBuffer _globalBuffer; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IOptionsMonitor _requestOptions; - private readonly IOptionsMonitor _globalOptions; + private readonly IOptionsMonitor _requestOptions; + private readonly IOptionsMonitor _globalOptions; public HttpRequestBufferManager( - IGlobalBufferManager globalBufferManager, + LogBuffer globalBuffer, IHttpContextAccessor httpContextAccessor, - IOptionsMonitor requestOptions, - IOptionsMonitor globalOptions) + IOptionsMonitor requestOptions, + IOptionsMonitor globalOptions) { - _globalBufferManager = globalBufferManager; + _globalBuffer = globalBuffer; _httpContextAccessor = httpContextAccessor; _requestOptions = requestOptions; _globalOptions = globalOptions; } - public void FlushNonRequestLogs() => _globalBufferManager.Flush(); + public override void Flush() => _globalBuffer.Flush(); - public void FlushCurrentRequestLogs() + public override void FlushCurrentRequestLogs() { _httpContextAccessor.HttpContext?.RequestServices.GetService()?.Flush(); } - public bool TryEnqueue( - IBufferedLogger bufferedLogger, - LogLevel logLevel, - string category, - EventId eventId, - TState state, - Exception? exception, - Func formatter) + public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry) { HttpContext? httpContext = _httpContextAccessor.HttpContext; if (httpContext is null) { - return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter); + return _globalBuffer.TryEnqueue(bufferedLogger, logEntry); } HttpRequestBufferHolder? bufferHolder = httpContext.RequestServices.GetService(); - ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!); + ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(logEntry.Category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!); if (buffer is null) { - return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter); + return _globalBuffer.TryEnqueue(bufferedLogger, logEntry); } - return buffer.TryEnqueue(logLevel, category, eventId, state, exception, formatter); + return buffer.TryEnqueue(logEntry); } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs similarity index 51% rename from src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs index 8c53955220f..8c61553e78c 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs @@ -17,83 +17,90 @@ namespace Microsoft.Extensions.Logging; /// -/// Lets you register log buffers in a dependency injection container. +/// Lets you register HTTP request log buffering in a dependency injection container. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public static class HttpRequestBufferLoggerBuilderExtensions +public static class HttpRequestBufferingLoggingBuilderExtensions { /// - /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in - /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. + /// Adds HTTP request log buffering to the logging infrastructure. /// /// The . /// The to add. /// The value of . /// is . + /// + /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime. + /// public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration) { _ = Throw.IfNull(builder); _ = Throw.IfNull(configuration); + _ = builder.Services.AddSingleton>(new HttpRequestLogBufferingConfigureOptions(configuration)); + return builder - .AddHttpRequestBufferConfiguration(configuration) .AddHttpRequestBufferManager() - .AddGlobalBuffer(configuration); + .AddGlobalBuffering(configuration); } /// - /// Adds HTTP request-aware buffering to the logging infrastructure. Matched logs will be buffered in - /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. + /// Adds HTTP request log buffering to the logging infrastructure. /// /// The . - /// The log level (and below) to apply the buffer to. - /// The buffer configuration options. + /// The buffer configuration delegate. /// The value of . /// is . - public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) + /// + /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime. + /// + public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, Action configure) { _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.Configure(configure); - _ = builder.Services - .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null, null))) - .Configure(configure ?? new Action(_ => { })); + HttpRequestLogBufferingOptions options = new HttpRequestLogBufferingOptions(); + configure(options); return builder .AddHttpRequestBufferManager() - .AddGlobalBuffer(level); + .AddGlobalBuffering(opts => opts.Rules = options.Rules); } /// - /// Adds HTTP request buffer provider to the logging infrastructure. + /// Adds HTTP request log buffering to the logging infrastructure. /// /// The . - /// The so that additional calls can be chained. + /// The log level (and below) to apply the buffer to. + /// The value of . /// is . - internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder) + /// + /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime. + /// + public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? logLevel = null) { _ = Throw.IfNull(builder); - builder.Services.TryAddScoped(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + _ = builder.Services.Configure(options => options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel))); - return builder; + return builder + .AddHttpRequestBufferManager() + .AddGlobalBuffering(logLevel); } - /// - /// Configures from an instance of . - /// - /// The . - /// The to add. - /// The value of . - /// is . - internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) + internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder) { - _ = Throw.IfNull(builder); - - _ = builder.Services.AddSingleton>(new HttpRequestBufferConfigureOptions(configuration)); + builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(sp => + { + var globalBufferManager = sp.GetRequiredService(); + return ActivatorUtilities.CreateInstance(sp, globalBufferManager); + }); + builder.Services.TryAddSingleton(sp => sp.GetRequiredService()); + builder.Services.TryAddSingleton(sp => sp.GetRequiredService()); return builder; } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs similarity index 58% rename from src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs index c6951b5042a..6fca3fa5b82 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs @@ -8,18 +8,13 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; /// -/// Interface for an HTTP request buffer manager. +/// Buffers HTTP request logs into circular buffers and drops them after some time if not flushed. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface IHttpRequestBufferManager : IBufferManager +public abstract class HttpRequestLogBuffer : LogBuffer { /// - /// Flushes the buffer and emits non-request logs. + /// Flushes buffers and emits buffered logs for the current HTTP request. /// - void FlushNonRequestLogs(); - - /// - /// Flushes the buffer and emits buffered logs for the current request. - /// - void FlushCurrentRequestLogs(); + public abstract void FlushCurrentRequestLogs(); } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs similarity index 62% rename from src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs index 81cc67d4e22..004fc1d89d5 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs @@ -1,23 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Diagnostics.Buffering; -internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions +internal sealed class HttpRequestLogBufferingConfigureOptions : IConfigureOptions { private const string BufferingKey = "Buffering"; private readonly IConfiguration _configuration; - public HttpRequestBufferConfigureOptions(IConfiguration configuration) + public HttpRequestLogBufferingConfigureOptions(IConfiguration configuration) { _configuration = configuration; } - public void Configure(HttpRequestBufferOptions options) + public void Configure(HttpRequestLogBufferingOptions options) { if (_configuration == null) { @@ -30,12 +29,15 @@ public void Configure(HttpRequestBufferOptions options) return; } - var parsedOptions = section.Get(); + var parsedOptions = section.Get(); if (parsedOptions is null) { return; } - options.Rules.AddRange(parsedOptions.Rules); + foreach (var rule in parsedOptions.Rules) + { + options.Rules.Add(rule); + } } } diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.cs similarity index 58% rename from src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.cs index e97dc38f260..4b42320a4af 100644 --- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs +++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.cs @@ -9,21 +9,21 @@ namespace Microsoft.AspNetCore.Diagnostics.Buffering; /// -/// The options for LoggerBuffer. +/// The options for HTTP request log buffering. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public class HttpRequestBufferOptions +public class HttpRequestLogBufferingOptions { /// /// Gets or sets the size in bytes of the buffer for a request. If the buffer size exceeds this limit, the oldest buffered log records will be dropped. /// /// TO DO: add validation. - public int PerRequestBufferSizeInBytes { get; set; } = 5_000_000; + public int MaxPerRequestBufferSizeInBytes { get; set; } = 5_000_000; -#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() +#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// - /// Gets the collection of used for filtering log messages for the purpose of further buffering. + /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. /// - public List Rules { get; } = []; -#pragma warning restore CA1002 // Do not expose generic lists + public IList Rules { get; set; } = []; +#pragma warning restore CA2227 } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs deleted file mode 100644 index 63d16241232..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRule.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -/// -/// Defines a rule used to filter log messages for purposes of futher buffering. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public class BufferFilterRule -{ - /// - /// Initializes a new instance of the class. - /// - public BufferFilterRule() - : this(null, null, null, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The category name to use in this filter rule. - /// The to use in this filter rule. - /// The to use in this filter rule. - /// The optional attributes to use if a log message passes other filters. - public BufferFilterRule(string? categoryName, LogLevel? logLevel, int? eventId, - IReadOnlyList>? attributes = null) - { - Category = categoryName; - LogLevel = logLevel; - EventId = eventId; - Attributes = attributes ?? []; - } - - /// - /// Gets or sets the logger category this rule applies to. - /// - public string? Category { get; set; } - - /// - /// Gets or sets the maximum of messages. - /// - public LogLevel? LogLevel { get; set; } - - /// - /// Gets or sets the of messages where this rule applies to. - /// - public int? EventId { get; set; } - - /// - /// Gets or sets the log state attributes of messages where this rules applies to. - /// - public IReadOnlyList> Attributes { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs index b828a04ba2a..b4fddaf8b65 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBuffer.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; internal sealed class GlobalBuffer : ILoggingBuffer { - private readonly IOptionsMonitor _options; + private readonly IOptionsMonitor _options; private readonly ConcurrentQueue _buffer; private readonly IBufferedLogger _bufferedLogger; private readonly TimeProvider _timeProvider; @@ -26,7 +26,7 @@ internal sealed class GlobalBuffer : ILoggingBuffer private object _netfxBufferLocker = new(); #endif - public GlobalBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor options, TimeProvider timeProvider) + public GlobalBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor options, TimeProvider timeProvider) { _options = options; _timeProvider = timeProvider; @@ -34,38 +34,32 @@ public GlobalBuffer(IBufferedLogger bufferedLogger, IOptionsMonitor( - LogLevel logLevel, - string category, - EventId eventId, - T attributes, - Exception? exception, - Func formatter) + public bool TryEnqueue(LogEntry logEntry) { SerializedLogRecord serializedLogRecord = default; - if (attributes is ModernTagJoiner modernTagJoiner) + if (logEntry.State is ModernTagJoiner modernTagJoiner) { - if (!IsEnabled(category, logLevel, eventId, modernTagJoiner)) + if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, modernTagJoiner)) { return false; } - serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, - ((Func)(object)formatter)(modernTagJoiner, exception)); + serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), modernTagJoiner, logEntry.Exception, + ((Func)(object)logEntry.Formatter)(modernTagJoiner, logEntry.Exception)); } - else if (attributes is LegacyTagJoiner legacyTagJoiner) + else if (logEntry.State is LegacyTagJoiner legacyTagJoiner) { - if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner)) + if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, legacyTagJoiner)) { return false; } - serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, - ((Func)(object)formatter)(legacyTagJoiner, exception)); + serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), legacyTagJoiner, logEntry.Exception, + ((Func)(object)logEntry.Formatter)(legacyTagJoiner, logEntry.Exception)); } else { - Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(T)}"); + Throw.InvalidOperationException($"Unsupported type of the log state detected: {typeof(TState)}"); } if (serializedLogRecord.SizeInBytes > _options.CurrentValue.MaxLogRecordSizeInBytes) @@ -113,11 +107,13 @@ public void Flush() } _bufferedLogger.LogRecords(deserializedLogRecords); + + // TO DO: adjust _buffersize after flushing. } private void Trim() { - while (_bufferSize > _options.CurrentValue.BufferSizeInBytes && _buffer.TryDequeue(out var item)) + while (_bufferSize > _options.CurrentValue.MaxBufferSizeInBytes && _buffer.TryDequeue(out var item)) { _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes); } @@ -130,7 +126,7 @@ private bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IRea return false; } - BufferFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule); + LogBufferingFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out LogBufferingFilterRule? rule); return rule is not null; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs index 3f1ea4e402d..f1a76ebb06b 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferManager.cs @@ -3,30 +3,29 @@ using System; using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Diagnostics.Buffering; -internal sealed class GlobalBufferManager : IGlobalBufferManager +internal sealed class GlobalBufferManager : LogBuffer { internal readonly ConcurrentDictionary Buffers = []; - private readonly IOptionsMonitor _options; + private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider = TimeProvider.System; - public GlobalBufferManager(IOptionsMonitor options) + public GlobalBufferManager(IOptionsMonitor options) { _options = options; } - internal GlobalBufferManager(IOptionsMonitor options, TimeProvider timeProvider) + internal GlobalBufferManager(IOptionsMonitor options, TimeProvider timeProvider) { _timeProvider = timeProvider; _options = options; } - public void Flush() + public override void Flush() { foreach (var buffer in Buffers.Values) { @@ -34,15 +33,9 @@ public void Flush() } } - public bool TryEnqueue( - IBufferedLogger bufferedLogger, - LogLevel logLevel, - string category, - EventId eventId, TState state, - Exception? exception, - Func formatter) + public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry) { - var buffer = Buffers.GetOrAdd(category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); - return buffer.TryEnqueue(logLevel, category, eventId, state, exception, formatter); + var buffer = Buffers.GetOrAdd(logEntry.Category, _ => new GlobalBuffer(bufferedLogger, _options, _timeProvider)); + return buffer.TryEnqueue(logEntry); } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferingLoggingBuilderExtensions.cs similarity index 55% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferingLoggingBuilderExtensions.cs index 26d034e7993..cb48c3fe74e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferLoggerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferingLoggingBuilderExtensions.cs @@ -15,79 +15,76 @@ namespace Microsoft.Extensions.Logging; /// -/// Lets you register log buffers in a dependency injection container. +/// Lets you register log buffering in a dependency injection container. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public static class GlobalBufferLoggerBuilderExtensions +public static class GlobalBufferingLoggingBuilderExtensions { /// - /// Adds global buffer to the logging infrastructure. - /// Matched logs will be buffered and can optionally be flushed and emitted./>. + /// Adds global log buffering to the logging infrastructure. /// /// The . /// The to add. /// The value of . /// is . - public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, IConfiguration configuration) + /// + /// Matched logs will be buffered and can optionally be flushed and emitted. + /// + public static ILoggingBuilder AddGlobalBuffering(this ILoggingBuilder builder, IConfiguration configuration) { _ = Throw.IfNull(builder); _ = Throw.IfNull(configuration); - return builder - .AddGlobalBufferConfiguration(configuration) - .AddGlobalBufferManager(); + _ = builder.Services.AddSingleton>(new GlobalLogBufferingConfigureOptions(configuration)); + + return builder.AddGlobalBufferManager(); } /// - /// Adds global buffer to the logging infrastructure. - /// Matched logs will be buffered and can optionally be flushed and emitted./>. + /// Adds global log buffering to the logging infrastructure. /// /// The . - /// The log level (and below) to apply the buffer to. /// Configure buffer options. /// The value of . /// is . - public static ILoggingBuilder AddGlobalBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null) + /// + /// Matched logs will be buffered and can optionally be flushed and emitted. + /// + public static ILoggingBuilder AddGlobalBuffering(this ILoggingBuilder builder, Action configure) { _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); - _ = builder.Services - .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null, null))) - .Configure(configure ?? new Action(_ => { })); + _ = builder.Services.Configure(configure); return builder.AddGlobalBufferManager(); } /// - /// Adds global logging buffer manager. + /// Adds global log buffering to the logging infrastructure. /// /// The . - /// The so that additional calls can be chained. - internal static ILoggingBuilder AddGlobalBufferManager(this ILoggingBuilder builder) + /// The log level (and below) to apply the buffer to. + /// The value of . + /// is . + /// + /// Matched logs will be buffered and can optionally be flushed and emitted. + /// + public static ILoggingBuilder AddGlobalBuffering(this ILoggingBuilder builder, LogLevel? logLevel = null) { _ = Throw.IfNull(builder); - _ = builder.Services.AddExtendedLoggerFeactory(); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); - builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); + _ = builder.Services.Configure(options => options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel))); - return builder; + return builder.AddGlobalBufferManager(); } - /// - /// Configures from an instance of . - /// - /// The . - /// The to add. - /// The value of . - /// is . - internal static ILoggingBuilder AddGlobalBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) + internal static ILoggingBuilder AddGlobalBufferManager(this ILoggingBuilder builder) { - _ = Throw.IfNull(builder); + _ = builder.Services.AddExtendedLoggerFeactory(); - _ = builder.Services.AddSingleton>(new GlobalBufferConfigureOptions(configuration)); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(static sp => sp.GetRequiredService()); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingConfigureOptions.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingConfigureOptions.cs index f32955c246e..f595567bdee 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferConfigureOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingConfigureOptions.cs @@ -6,17 +6,17 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; -internal sealed class GlobalBufferConfigureOptions : IConfigureOptions +internal sealed class GlobalLogBufferingConfigureOptions : IConfigureOptions { private const string BufferingKey = "Buffering"; private readonly IConfiguration _configuration; - public GlobalBufferConfigureOptions(IConfiguration configuration) + public GlobalLogBufferingConfigureOptions(IConfiguration configuration) { _configuration = configuration; } - public void Configure(GlobalBufferOptions options) + public void Configure(GlobalLogBufferingOptions options) { if (_configuration == null) { @@ -29,7 +29,7 @@ public void Configure(GlobalBufferOptions options) return; } - var parsedOptions = section.Get(); + var parsedOptions = section.Get(); if (parsedOptions is null) { return; @@ -40,11 +40,14 @@ public void Configure(GlobalBufferOptions options) options.MaxLogRecordSizeInBytes = parsedOptions.MaxLogRecordSizeInBytes; } - if (parsedOptions.BufferSizeInBytes > 0) + if (parsedOptions.MaxBufferSizeInBytes > 0) { - options.BufferSizeInBytes = parsedOptions.BufferSizeInBytes; + options.MaxBufferSizeInBytes = parsedOptions.MaxBufferSizeInBytes; } - options.Rules.AddRange(parsedOptions.Rules); + foreach (var rule in parsedOptions.Rules) + { + options.Rules.Add(rule); + } } } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs index 79fc3691d8f..6d96908341e 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalBufferOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/GlobalLogBufferingOptions.cs @@ -9,10 +9,10 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// -/// The options for LoggerBuffer. +/// The options for global log buffering. /// [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public class GlobalBufferOptions +public class GlobalLogBufferingOptions { /// /// Gets or sets the time to suspend the buffering after flushing. @@ -34,12 +34,12 @@ public class GlobalBufferOptions /// the oldest buffered log records will be dropped to make room. /// /// TO DO: add validation. - public int BufferSizeInBytes { get; set; } = 500_000_000; + public int MaxBufferSizeInBytes { get; set; } = 500_000_000; -#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() +#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern /// - /// Gets the collection of used for filtering log messages for the purpose of further buffering. + /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering. /// - public List Rules { get; } = []; -#pragma warning restore CA1002 // Do not expose generic lists + public IList Rules { get; set; } = []; +#pragma warning restore CA2227 } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs deleted file mode 100644 index da48c9b20fc..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IBufferManager.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -/// -/// Interface for a buffer manager. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface IBufferManager -{ - /// - /// Enqueues a log record in the underlying buffer. - /// - /// A logger capable of logging buffered log records. - /// Log level. - /// Category. - /// Event ID. - /// Log state attributes. - /// Exception. - /// Formatter delegate. - /// Type of the instance. - /// if the log record was buffered; otherwise, . - bool TryEnqueue( - IBufferedLogger bufferedLoger, - LogLevel logLevel, - string category, - EventId eventId, - TState state, - Exception? exception, - Func formatter); -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs deleted file mode 100644 index 5a68f76c6a2..00000000000 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/IGlobalBufferManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.Diagnostics.Buffering; - -/// -/// Interface for a global buffer manager. -/// -[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] -public interface IGlobalBufferManager : IBufferManager -{ - /// - /// Flushes the buffer and emits all buffered logs. - /// - void Flush(); -} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBuffer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBuffer.cs new file mode 100644 index 00000000000..343662a0958 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBuffer.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Diagnostics.Buffering; + +/// +/// Buffers logs into circular buffers and drops them after some time if not flushed. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods +public abstract class LogBuffer +#pragma warning restore S1694 // An abstract class should have both abstract and concrete methods +{ + /// + /// Flushes the buffer and emits all buffered logs. + /// + public abstract void Flush(); + + /// + /// Enqueues a log record in the underlying buffer, if available. + /// + /// A logger capable of logging buffered log records. + /// A log entry to be buffered. + /// Type of the log state in the instance. + /// if the log record was buffered; otherwise, . + public abstract bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs new file mode 100644 index 00000000000..affc131bf40 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRule.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Extensions.Diagnostics.Buffering; + +/// +/// Defines a rule used to filter log messages for purposes of further buffering. +/// +[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] +public class LogBufferingFilterRule +{ + /// + /// Initializes a new instance of the class. + /// + /// The category name to use in this filter rule. + /// The to use in this filter rule. + /// The event ID to use in this filter rule. + /// The event name to use in this filter rule. + /// The log state attributes to use in this filter tule. + public LogBufferingFilterRule( + string? categoryName = null, + LogLevel? logLevel = null, + int? eventId = null, + string? eventName = null, + IReadOnlyList>? attributes = null) + { + CategoryName = categoryName; + LogLevel = logLevel; + EventId = eventId; + EventName = eventName; + Attributes = attributes; + } + + /// + /// Gets the logger category name this rule applies to. + /// + public string? CategoryName { get; } + + /// + /// Gets the maximum of messages this rule applies to. + /// + public LogLevel? LogLevel { get; } + + /// + /// Gets the evnet ID of messages where this rule applies to. + /// + public int? EventId { get; } + + /// + /// Gets the name of the event this rule applies to. + /// + public string? EventName { get; } + + /// + /// Gets the log state attributes of messages where this rules applies to. + /// + public IReadOnlyList>? Attributes { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRuleSelector.cs similarity index 85% rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs rename to src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRuleSelector.cs index 237a7e2b242..08bf5a88307 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/BufferFilterRuleSelector.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/LogBufferingFilterRuleSelector.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.Diagnostics.Buffering; /// /// Selects the best rule from the list of rules for a given log event. /// -internal static class BufferFilterRuleSelector +internal static class LogBufferingFilterRuleSelector { private static readonly IEqualityComparer> _stringifyComparer = new StringifyComprarer(); @@ -28,8 +28,8 @@ internal static class BufferFilterRuleSelector /// The event id of the log event. /// The log state attributes of the log event. /// The best rule that matches the log event. - public static void Select(IList rules, string category, LogLevel logLevel, - EventId eventId, IReadOnlyList>? attributes, out BufferFilterRule? bestRule) + public static void Select(IList rules, string category, LogLevel logLevel, + EventId eventId, IReadOnlyList>? attributes, out LogBufferingFilterRule? bestRule) { bestRule = null; @@ -40,10 +40,10 @@ public static void Select(IList rules, string category, LogLev // 3. If there is only one rule use it // 4. If there are multiple rules use last - BufferFilterRule? current = null; + LogBufferingFilterRule? current = null; if (rules is not null) { - foreach (BufferFilterRule rule in rules) + foreach (LogBufferingFilterRule rule in rules) { if (IsBetter(rule, current, category, logLevel, eventId, attributes)) { @@ -58,7 +58,7 @@ public static void Select(IList rules, string category, LogLev } } - private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, string category, + private static bool IsBetter(LogBufferingFilterRule rule, LogBufferingFilterRule? current, string category, LogLevel logLevel, EventId eventId, IReadOnlyList>? attributes) { // Skip rules with inapplicable log level @@ -74,7 +74,7 @@ private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, s } // Skip rules with inapplicable category - string? categoryName = rule.Category; + string? categoryName = rule.CategoryName; if (categoryName != null) { const char WildcardChar = '*'; @@ -106,7 +106,7 @@ private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, s } // Skip rules with inapplicable attributes - if (rule.Attributes.Count > 0 && attributes?.Count > 0) + if (rule.Attributes?.Count > 0 && attributes?.Count > 0) { foreach (KeyValuePair ruleAttribute in rule.Attributes) { @@ -118,14 +118,14 @@ private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, s } // Decide whose category is better - rule vs current - if (current?.Category != null) + if (current?.CategoryName != null) { - if (rule.Category == null) + if (rule.CategoryName == null) { return false; } - if (current.Category.Length > rule.Category.Length) + if (current.CategoryName.Length > rule.CategoryName.Length) { return false; } @@ -155,9 +155,9 @@ private static bool IsBetter(BufferFilterRule rule, BufferFilterRule? current, s } // Decide whose attributes are better - rule vs current - if (current?.Attributes.Count > 0) + if (current?.Attributes?.Count > 0) { - if (rule.Attributes.Count == 0) + if (rule.Attributes is null || rule.Attributes.Count == 0) { return false; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs index dc6980333a2..fbec3961880 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/StringifyComprarer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; namespace Microsoft.Extensions.Diagnostics.Buffering; + internal sealed class StringifyComprarer : IEqualityComparer> { public bool Equals(KeyValuePair x, KeyValuePair y) diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs index c21cd228e4d..1c44834b5cf 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLogger.cs @@ -32,7 +32,7 @@ internal sealed partial class ExtendedLogger : ILogger public MessageLogger[] MessageLoggers { get; set; } = Array.Empty(); public ScopeLogger[] ScopeLoggers { get; set; } = Array.Empty(); - private readonly IBufferManager? _bufferManager; + private readonly LogBuffer? _bufferingManager; private readonly IBufferedLogger? _bufferedLogger; public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers) @@ -40,8 +40,8 @@ public ExtendedLogger(ExtendedLoggerFactory factory, LoggerInformation[] loggers _factory = factory; Loggers = loggers; - _bufferManager = _factory.Config.BufferManager; - if (_bufferManager is not null) + _bufferingManager = _factory.Config.BufferingManager; + if (_bufferingManager is not null) { _bufferedLogger = new BufferedLoggerProxy(this); } @@ -279,18 +279,19 @@ private void ModernPath(LogLevel logLevel, EventId eventId, LoggerMessageState m { if (shouldBuffer) { - if (_bufferManager is not null) + if (_bufferingManager is not null) { - var wasBuffered = _bufferManager.TryEnqueue(_bufferedLogger!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + var logEntry = new LogEntry(logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = s.Formatter!; return fmt(s.State!, e); }); + var wasBuffered = _bufferingManager.TryEnqueue(_bufferedLogger!, logEntry); if (wasBuffered) { // The record was buffered, so we skip logging it here and for all other loggers. - // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // When a caller needs to flush the buffer and calls Flush(), // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. break; } @@ -386,18 +387,19 @@ private void LegacyPath(LogLevel logLevel, EventId eventId, TState state { if (shouldBuffer) { - if (_bufferManager is not null) + if (_bufferingManager is not null) { - bool wasBuffered = _bufferManager.TryEnqueue(_bufferedLogger!, logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => + var logEntry = new LogEntry(logLevel, loggerInfo.Category!, eventId, joiner, exception, static (s, e) => { var fmt = (Func)s.Formatter!; return fmt((TState)s.State!, e); }); + bool wasBuffered = _bufferingManager.TryEnqueue(_bufferedLogger!, in logEntry); if (wasBuffered) { // The record was buffered, so we skip logging it here and for all other loggers. - // When a caller needs to flush the buffer and calls IBufferManager.Flush(), + // When a caller needs to flush the buffer and calls Flush(), // the buffer manager will internally call IBufferedLogger.LogRecords to emit log records. break; } diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs index 105fa487ebd..f24cc911f9f 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs @@ -24,7 +24,7 @@ internal sealed class ExtendedLoggerFactory : ILoggerFactory private readonly IDisposable? _enrichmentOptionsChangeTokenRegistration; private readonly IDisposable? _redactionOptionsChangeTokenRegistration; private readonly Action[] _enrichers; - private readonly IBufferManager? _bufferManager; + private readonly LogBuffer? _bufferingManager; private readonly KeyValuePair[] _staticTags; private readonly Func _redactorProvider; private volatile bool _disposed; @@ -42,11 +42,11 @@ public ExtendedLoggerFactory( IOptionsMonitor? enrichmentOptions = null, IOptionsMonitor? redactionOptions = null, IRedactorProvider? redactorProvider = null, - IBufferManager? bufferManager = null) + LogBuffer? bufferingManager = null) #pragma warning restore S107 // Methods should not have too many parameters { _scopeProvider = scopeProvider; - _bufferManager = bufferManager; + _bufferingManager = bufferingManager; _factoryOptions = factoryOptions == null || factoryOptions.Value == null ? new LoggerFactoryOptions() : factoryOptions.Value; @@ -294,7 +294,7 @@ private LoggerConfig ComputeConfig(LoggerEnrichmentOptions? enrichmentOptions, L enrichmentOptions.MaxStackTraceLength, _redactorProvider, redactionOptions.ApplyDiscriminator, - _bufferManager); + _bufferingManager); } private void UpdateEnrichmentOptions(LoggerEnrichmentOptions enrichmentOptions) => Config = ComputeConfig(enrichmentOptions, null); diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs index 1870049c38c..80c953e1335 100644 --- a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerConfig.cs @@ -22,7 +22,7 @@ public LoggerConfig( int maxStackTraceLength, Func getRedactor, bool addRedactionDiscriminator, - IBufferManager? bufferManager) + LogBuffer? bufferingManager) { #pragma warning restore S107 // Methods should not have too many parameters StaticTags = staticTags; @@ -33,7 +33,7 @@ public LoggerConfig( IncludeExceptionMessage = includeExceptionMessage; GetRedactor = getRedactor; AddRedactionDiscriminator = addRedactionDiscriminator; - BufferManager = bufferManager; + BufferingManager = bufferingManager; } public KeyValuePair[] StaticTags { get; } @@ -44,5 +44,5 @@ public LoggerConfig( public int MaxStackTraceLength { get; } public Func GetRedactor { get; } public bool AddRedactionDiscriminator { get; } - public IBufferManager? BufferManager { get; } + public LogBuffer? BufferingManager { get; } } diff --git a/src/Shared/LoggingBuffering/ILoggingBuffer.cs b/src/Shared/LoggingBuffering/ILoggingBuffer.cs index 8cc1b1f0d90..0ff1fb2c872 100644 --- a/src/Shared/LoggingBuffering/ILoggingBuffer.cs +++ b/src/Shared/LoggingBuffering/ILoggingBuffer.cs @@ -1,8 +1,7 @@ // 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Diagnostics.Buffering; @@ -14,21 +13,9 @@ internal interface ILoggingBuffer /// /// Enqueues a log record in the underlying buffer. /// - /// Log level. - /// Category. - /// Event ID. - /// Log state attributes. - /// Exception. - /// Formatter delegate. - /// Type of the instance. + /// Type of the log state in the instance. /// if the log record was buffered; otherwise, . - bool TryEnqueue( - LogLevel logLevel, - string category, - EventId eventId, - TState state, - Exception? exception, - Func formatter); + bool TryEnqueue(LogEntry logEntry); /// /// Flushes the buffer. diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs index 929b97b97b2..52cd5132af9 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs @@ -24,7 +24,7 @@ public void AddHttpRequestBuffering_RegistersInDI() }); var serviceProvider = serviceCollection.BuildServiceProvider(); - var buffer = serviceProvider.GetService(); + var buffer = serviceProvider.GetService(); Assert.NotNull(buffer); Assert.IsAssignableFrom(buffer); @@ -43,21 +43,18 @@ public void WhenArgumentNull_Throws() [Fact] public void AddHttpRequestBufferConfiguration_RegistersInDI() { - List expectedData = + List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, null), - new BufferFilterRule(null, LogLevel.Information, null, null), + new LogBufferingFilterRule(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"), + new LogBufferingFilterRule(logLevel: LogLevel.Information), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); configBuilder.AddJsonFile("appsettings.json"); IConfigurationRoot configuration = configBuilder.Build(); var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(builder => - { - builder.AddHttpRequestBufferConfiguration(configuration); - }); + serviceCollection.AddLogging(b => b.AddHttpRequestBuffering(configuration)); var serviceProvider = serviceCollection.BuildServiceProvider(); - var options = serviceProvider.GetService>(); + var options = serviceProvider.GetService>(); Assert.NotNull(options); Assert.NotNull(options.CurrentValue); Assert.Equivalent(expectedData, options.CurrentValue.Rules); diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs index bb9aebec280..c33a96b1945 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs @@ -61,8 +61,8 @@ public static void Configure(IApplicationBuilder app) { await context.Request.Body.DrainAsync(default); - // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor - var bufferManager = context.RequestServices.GetService(); + // normally, this would be a Middleware and HttpRequestLogBuffer would be injected via constructor + var bufferManager = context.RequestServices.GetService(); bufferManager?.FlushCurrentRequestLogs(); })); @@ -71,12 +71,12 @@ public static void Configure(IApplicationBuilder app) { await context.Request.Body.DrainAsync(default); - // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor - var bufferManager = context.RequestServices.GetService(); + // normally, this would be a Middleware and HttpRequestLogBuffer would be injected via constructor + var bufferManager = context.RequestServices.GetService(); if (bufferManager is not null) { bufferManager.FlushCurrentRequestLogs(); - bufferManager.FlushNonRequestLogs(); + bufferManager.Flush(); } })); diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json index 79676b0e1e9..9597f27c636 100644 --- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json +++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json @@ -21,9 +21,10 @@ "Buffering": { "Rules": [ { - "Category": "Program.MyLogger", + "CategoryName": "Program.MyLogger", "LogLevel": "Information", - "EventId": 1 + "EventId": 1, + "EventName": "number one" }, { "LogLevel": "Information" diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs index 9a02d8f254c..76d62e496f9 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs @@ -19,11 +19,11 @@ public void AddGlobalBuffer_RegistersInDI() var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { - builder.AddGlobalBuffer(LogLevel.Warning); + builder.AddGlobalBuffering(LogLevel.Warning); }); var serviceProvider = serviceCollection.BuildServiceProvider(); - var bufferManager = serviceProvider.GetService(); + var bufferManager = serviceProvider.GetService(); Assert.NotNull(bufferManager); Assert.IsAssignableFrom(bufferManager); @@ -35,17 +35,17 @@ public void WhenArgumentNull_Throws() var builder = null as ILoggingBuilder; var configuration = null as IConfiguration; - Assert.Throws(() => builder!.AddGlobalBuffer(LogLevel.Warning)); - Assert.Throws(() => builder!.AddGlobalBuffer(configuration!)); + Assert.Throws(() => builder!.AddGlobalBuffering(LogLevel.Warning)); + Assert.Throws(() => builder!.AddGlobalBuffering(configuration!)); } [Fact] public void AddGlobalBuffer_WithConfiguration_RegistersInDI() { - List expectedData = + List expectedData = [ - new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, [new("region", "westus2"), new ("priority", 1)]), - new BufferFilterRule(null, LogLevel.Information, null, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Information, 1, "number one", [new("region", "westus2"), new ("priority", 1)]), + new LogBufferingFilterRule(logLevel : LogLevel.Information), ]; ConfigurationBuilder configBuilder = new ConfigurationBuilder(); configBuilder.AddJsonFile("appsettings.json"); @@ -53,18 +53,18 @@ public void AddGlobalBuffer_WithConfiguration_RegistersInDI() var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(builder => { - builder.AddGlobalBuffer(configuration); - builder.Services.Configure(options => + builder.AddGlobalBuffering(configuration); + builder.Services.Configure(options => { options.MaxLogRecordSizeInBytes = 33; }); }); var serviceProvider = serviceCollection.BuildServiceProvider(); - var options = serviceProvider.GetService>(); + var options = serviceProvider.GetService>(); Assert.NotNull(options); Assert.NotNull(options.CurrentValue); - Assert.Equal(33, options.CurrentValue.MaxLogRecordSizeInBytes); // value comes from the Configure() call - Assert.Equal(1000, options.CurrentValue.BufferSizeInBytes); // value comes from appsettings.json + Assert.Equal(33, options.CurrentValue.MaxLogRecordSizeInBytes); // value comes from the Configure() call + Assert.Equal(1000, options.CurrentValue.MaxBufferSizeInBytes); // value comes from appsettings.json Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.SuspendAfterFlushDuration); // value comes from default Assert.Equivalent(expectedData, options.CurrentValue.Rules); } diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs index 041408f7bd1..e41e877bd7c 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs @@ -13,30 +13,30 @@ public class LoggerFilterRuleSelectorTests public void SelectsRightRule() { // Arrange - var rules = new List + var rules = new List { - new BufferFilterRule(null, null, null, null), - new BufferFilterRule(null, null, 1, null), - new BufferFilterRule(null, LogLevel.Information, 1, null), - new BufferFilterRule(null, LogLevel.Information, 1, null), - new BufferFilterRule(null, LogLevel.Warning, null, null), - new BufferFilterRule(null, LogLevel.Warning, 2, null), - new BufferFilterRule(null, LogLevel.Warning, 1, null), - new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region2", "westus2")]), // inapplicable key - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus3")]), // inapplicable value - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule - [11] - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 2, null), - new BufferFilterRule("Program.MyLogger", null, 1, null), - new BufferFilterRule(null, LogLevel.Warning, 1, null), - new BufferFilterRule("Program", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, null, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Error, 1, null), + new LogBufferingFilterRule(null, null, null, null), + new LogBufferingFilterRule(null, null, 1, null), + new LogBufferingFilterRule(null, LogLevel.Information, 1, null), + new LogBufferingFilterRule(null, LogLevel.Information, 1, null), + new LogBufferingFilterRule(null, LogLevel.Warning, null, null), + new LogBufferingFilterRule(null, LogLevel.Warning, 2, null), + new LogBufferingFilterRule(null, LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes: [new("region2", "westus2")]), // inapplicable key + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus3")]), // inapplicable value + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // the best rule - [11] + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 2, null), + new LogBufferingFilterRule("Program.MyLogger", null, 1, null), + new LogBufferingFilterRule(null, LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, null, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Error, 1, null), }; // Act - BufferFilterRuleSelector.Select( + LogBufferingFilterRuleSelector.Select( rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert @@ -47,23 +47,23 @@ public void SelectsRightRule() public void WhenManyRuleApply_SelectsLast() { // Arrange - var rules = new List + var rules = new List { - new BufferFilterRule(null, LogLevel.Information, 1, null), - new BufferFilterRule(null, LogLevel.Information, 1, null), - new BufferFilterRule(null, LogLevel.Warning, null, null), - new BufferFilterRule(null, LogLevel.Warning, 2, null), - new BufferFilterRule(null, LogLevel.Warning, 1, null), - new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule - new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, [new("region", "westus2")]), // same as the best, but last and should be selected + new LogBufferingFilterRule(null, LogLevel.Information, 1, null), + new LogBufferingFilterRule(null, LogLevel.Information, 1, null), + new LogBufferingFilterRule(null, LogLevel.Warning, null, null), + new LogBufferingFilterRule(null, LogLevel.Warning, 2, null), + new LogBufferingFilterRule(null, LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // the best rule + new LogBufferingFilterRule("Program.MyLogger*", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // same as the best, but last and should be selected }; // Act - BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); + LogBufferingFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult); // Assert Assert.Same(rules.Last(), actualResult); @@ -73,16 +73,16 @@ public void WhenManyRuleApply_SelectsLast() public void CanWorkWithValueTypeAttributes() { // Arrange - var rules = new List + var rules = new List { - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 1)]), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)]), // the best rule - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 3)]), - new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 1)]), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 2)]), // the best rule + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 3)]), + new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, null), }; // Act - BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", "2")], out var actualResult); + LogBufferingFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", "2")], out var actualResult); // Assert Assert.Same(rules[1], actualResult); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs index fa2dafbe8ce..27820b1ea24 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs @@ -128,7 +128,7 @@ public static void GlobalBuffering_CanonicalUsecase() builder => { builder.AddProvider(provider); - builder.AddGlobalBuffer(LogLevel.Warning); + builder.AddGlobalBuffering(LogLevel.Warning); }); var logger = factory.CreateLogger("my category"); @@ -138,9 +138,9 @@ public static void GlobalBuffering_CanonicalUsecase() // nothing is logged because the buffer not flushed yet Assert.Equal(0, provider.Logger!.Collector.Count); - // instead of this, users would get IBufferManager from DI and call Flush on it + // instead of this, users would get LogBuffer from DI and call Flush on it var dlf = (Utils.DisposingLoggerFactory)factory; - var bufferManager = dlf.ServiceProvider.GetRequiredService(); + var bufferManager = dlf.ServiceProvider.GetRequiredService(); bufferManager.Flush(); diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json index d70df4596ba..f829c55c4dc 100644 --- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -19,12 +19,13 @@ }, "Buffering": { "MaxLogRecordSizeInBytes": 100, - "BufferSizeInBytes": 1000, + "MaxBufferSizeInBytes": 1000, "Rules": [ { - "Category": "Program.MyLogger", + "CategoryName": "Program.MyLogger", "LogLevel": "Information", "EventId": 1, + "EventName" : "number one", "Attributes": [ { "key": "region",