-
Notifications
You must be signed in to change notification settings - Fork 822
Add logging buffering #5635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
evgenyfedorov2
merged 55 commits into
dotnet:main
from
evgenyfedorov2:evgenyfedorov2/log_buffering
Apr 9, 2025
Merged
Add logging buffering #5635
Changes from 6 commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
988c709
Buffering
evgenyfedorov2 2f1a335
Major update
evgenyfedorov2 2d2412e
Remove Json exception converter
evgenyfedorov2 f7eaab1
Fix namespaces
evgenyfedorov2 1f464df
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 a371d9c
Fix build
evgenyfedorov2 d7661a6
Slight design changes with interfaces as per PR comments
evgenyfedorov2 9d13ab0
Drop json serialization
evgenyfedorov2 fe00658
Add log record size estimation and limit buffer size in bytes
evgenyfedorov2 5fc421c
Add filtering by attributes
evgenyfedorov2 70cfc7c
Use attributes directly instead of Func delegate
evgenyfedorov2 e96277f
Add http buffer holder
evgenyfedorov2 a79fcbf
Make ILoggingBuffer and DeserializedLogRecord types internal
evgenyfedorov2 8a91c15
Move shared files to Shared project and add more tests
evgenyfedorov2 4f524eb
Add custom equality comparer
evgenyfedorov2 b2b6e56
Address API Review feedback
evgenyfedorov2 f3a6b85
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 1370225
merge
evgenyfedorov2 393ce26
API review feedback
evgenyfedorov2 20b5a4c
Remove extra lines
evgenyfedorov2 f5ce71e
Make tests culture agnostic
ce2f7fb
Global log buffering - options validation, rule selector optimization
6ffbacd
Rename to PerIncomingRequest
evgenyfedorov2 1370ccb
Minor updates
56257ca
More renames
73a8678
Rename shared folder LoggingBuffering to LogBuffering
8a42385
Remove per request options refresh because buffer are scoped anyway
7ff16fc
Remove unnecessary casting
35223f3
Add DebuggerDisplay to SerializedLogRecord.cs
bf0b59d
Added pooling of log record attributes lists
715ce25
Moved validation for max one asterisk in log category to options vali…
ccdfaeb
Add size of SerializedLogRecord struct to ballpark size estimation
4e1566b
Added a remark
5171413
Added remarks on buffer filter rules
de83448
Enable log buffering for .NET 8
b137e88
Revert "Enable log buffering for .NET 8"
821ff0b
Fix warnings
406df25
Revert unnecessary changes
evgenyfedorov2 4dc02f4
update
evgenyfedorov2 053d6c1
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 6a26ce8
PR feedback
c93a183
.
542bdd2
Improve string size calculation
6c85897
Refactor TryEnqueue
4248980
Add a double-buffer pattern to avoid race conditions
c824379
Flush in batches and pool lists for emitted records
1bc60c0
Fix racing conditions for buffers
2e2a6a4
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 5e49d69
Use two buffers
f6a2dec
Add more list pools
6e92aec
More tests
1fef67b
Update
51d67d4
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 621578a
PR Comments
ec2ef64
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// 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; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Shared.Diagnostics; | ||
using static Microsoft.Extensions.Logging.ExtendedLogger; | ||
|
||
namespace Microsoft.AspNetCore.Diagnostics.Buffering; | ||
|
||
internal sealed class HttpRequestBuffer : ILoggingBuffer | ||
{ | ||
private readonly IOptionsMonitor<HttpRequestBufferOptions> _options; | ||
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions; | ||
private readonly ConcurrentQueue<SerializedLogRecord> _buffer; | ||
private readonly TimeProvider _timeProvider = TimeProvider.System; | ||
private readonly IBufferSink _bufferSink; | ||
private readonly object _bufferCapacityLocker = new(); | ||
private DateTimeOffset _truncateAfter; | ||
private DateTimeOffset _lastFlushTimestamp; | ||
|
||
public HttpRequestBuffer(IBufferSink bufferSink, | ||
IOptionsMonitor<HttpRequestBufferOptions> options, | ||
IOptionsMonitor<GlobalBufferOptions> globalOptions) | ||
{ | ||
_options = options; | ||
_globalOptions = globalOptions; | ||
_bufferSink = bufferSink; | ||
_buffer = new ConcurrentQueue<SerializedLogRecord>(); | ||
|
||
_truncateAfter = _timeProvider.GetUtcNow(); | ||
} | ||
|
||
public bool TryEnqueue<TState>( | ||
LogLevel logLevel, | ||
string category, | ||
EventId eventId, | ||
TState attributes, | ||
Exception? exception, | ||
Func<TState, Exception?, string> formatter) | ||
{ | ||
if (!IsEnabled(category, logLevel, eventId)) | ||
{ | ||
return false; | ||
} | ||
|
||
switch (attributes) | ||
{ | ||
case ModernTagJoiner modernTagJoiner: | ||
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception, | ||
((Func<ModernTagJoiner, Exception?, string>)(object)formatter)(modernTagJoiner, exception))); | ||
break; | ||
case LegacyTagJoiner legacyTagJoiner: | ||
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception, | ||
((Func<LegacyTagJoiner, Exception?, string>)(object)formatter)(legacyTagJoiner, exception))); | ||
break; | ||
default: | ||
Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}"); | ||
break; | ||
} | ||
|
||
var now = _timeProvider.GetUtcNow(); | ||
lock (_bufferCapacityLocker) | ||
{ | ||
if (now >= _truncateAfter) | ||
{ | ||
_truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration); | ||
TruncateOverlimit(); | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public void Flush() | ||
{ | ||
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 + _globalOptions.CurrentValue.SuspendAfterFlushDuration) | ||
{ | ||
return false; | ||
} | ||
|
||
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 _); | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...icrosoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// 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<HttpRequestBufferOptions> | ||
{ | ||
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<HttpRequestBufferOptions>(); | ||
if (parsedOptions is null) | ||
{ | ||
return; | ||
} | ||
|
||
options.Rules.AddRange(parsedOptions.Rules); | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
...t.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// 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.AspNetCore.Diagnostics.Buffering; | ||
using Microsoft.AspNetCore.Http; | ||
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; | ||
using Microsoft.Shared.Diagnostics; | ||
|
||
namespace Microsoft.Extensions.Logging; | ||
|
||
/// <summary> | ||
/// Lets you register log buffers in a dependency injection container. | ||
/// </summary> | ||
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] | ||
public static class HttpRequestBufferLoggerBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// 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./>. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param> | ||
/// <returns>The value of <paramref name="builder"/>.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
_ = Throw.IfNull(configuration); | ||
|
||
return builder | ||
.AddHttpRequestBufferConfiguration(configuration) | ||
.AddHttpRequestBufferManager() | ||
.AddGlobalBufferConfiguration(configuration) | ||
.AddGlobalBufferManager(); | ||
} | ||
|
||
/// <summary> | ||
/// 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./>. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <param name="level">The log level (and below) to apply the buffer to.</param> | ||
/// <param name="configure">The buffer configuration options.</param> | ||
/// <returns>The value of <paramref name="builder"/>.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action<HttpRequestBufferOptions>? configure = null) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
|
||
_ = builder.Services | ||
.Configure<HttpRequestBufferOptions>(options => options.Rules.Add(new BufferFilterRule(null, level, null))) | ||
.Configure(configure ?? new Action<HttpRequestBufferOptions>(_ => { })); | ||
|
||
return builder | ||
.AddHttpRequestBufferManager() | ||
.AddGlobalBuffer(level) | ||
.AddGlobalBufferManager(); | ||
} | ||
|
||
/// <summary> | ||
/// Adds HTTP request buffer provider to the logging infrastructure. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <returns>The <see cref="ILoggingBuilder"/> so that additional calls can be chained.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
|
||
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||
|
||
builder.Services.TryAddSingleton<ExtendedLoggerFactory>(); | ||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerFactory, ExtendedLoggerFactory>(sp => sp.GetRequiredService<ExtendedLoggerFactory>())); | ||
|
||
builder.Services.TryAddSingleton<HttpRequestBufferManager>(); | ||
builder.Services.TryAddSingleton<IBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>()); | ||
builder.Services.TryAddSingleton<IHttpRequestBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>()); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// Configures <see cref="HttpRequestBufferOptions" /> from an instance of <see cref="IConfiguration" />. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param> | ||
/// <returns>The value of <paramref name="builder"/>.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
|
||
_ = builder.Services.AddSingleton<IConfigureOptions<HttpRequestBufferOptions>>(new HttpRequestBufferConfigureOptions(configuration)); | ||
|
||
return builder; | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
...braries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// 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.Diagnostics.Buffering; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Diagnostics.Buffering; | ||
|
||
internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager | ||
{ | ||
private readonly GlobalBufferManager _globalBufferManager; | ||
private readonly IHttpContextAccessor _httpContextAccessor; | ||
private readonly IOptionsMonitor<HttpRequestBufferOptions> _requestOptions; | ||
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions; | ||
|
||
public HttpRequestBufferManager( | ||
GlobalBufferManager globalBufferManager, | ||
IHttpContextAccessor httpContextAccessor, | ||
IOptionsMonitor<HttpRequestBufferOptions> requestOptions, | ||
IOptionsMonitor<GlobalBufferOptions> 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}"); | ||
} | ||
evgenyfedorov2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return loggingBuffer; | ||
} | ||
|
||
public void Flush() => _globalBufferManager.Flush(); | ||
|
||
public void FlushCurrentRequestLogs() | ||
{ | ||
if (_httpContextAccessor.HttpContext is not null) | ||
{ | ||
foreach (var kvp in _httpContextAccessor.HttpContext!.Items) | ||
evgenyfedorov2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (kvp.Value is ILoggingBuffer buffer) | ||
{ | ||
buffer.Flush(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public bool TryEnqueue<TState>( | ||
IBufferSink bufferSink, | ||
LogLevel logLevel, | ||
string category, | ||
EventId eventId, | ||
TState attributes, | ||
evgenyfedorov2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Exception? exception, | ||
Func<TState, Exception?, string> formatter) | ||
{ | ||
var buffer = CreateBuffer(bufferSink, category); | ||
return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
...braries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// 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; | ||
using Microsoft.Shared.DiagnosticIds; | ||
|
||
namespace Microsoft.AspNetCore.Diagnostics.Buffering; | ||
|
||
/// <summary> | ||
/// The options for LoggerBuffer. | ||
/// </summary> | ||
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] | ||
public class HttpRequestBufferOptions | ||
{ | ||
/// <summary> | ||
/// Gets or sets the duration to check and remove the buffered items exceeding the <see cref="PerRequestCapacity"/>. | ||
/// </summary> | ||
public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10); | ||
|
||
/// <summary> | ||
/// Gets or sets the size of the buffer for a request. | ||
/// </summary> | ||
public int PerRequestCapacity { get; set; } = 1_000; | ||
evgenyfedorov2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#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 | ||
/// <summary> | ||
/// Gets or sets the collection of <see cref="BufferFilterRule"/> used for filtering log messages for the purpose of further buffering. | ||
/// </summary> | ||
public List<BufferFilterRule> Rules { get; set; } = []; | ||
#pragma warning restore CA2227 // Collection properties should be read only | ||
#pragma warning restore CA1002 // Do not expose generic lists | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.