-
Notifications
You must be signed in to change notification settings - Fork 821
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 all 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
164 changes: 164 additions & 0 deletions
164
...braries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBuffer.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,164 @@ | ||
// 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.Linq; | ||
using System.Threading; | ||
using Microsoft.Extensions.Diagnostics.Buffering; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Microsoft.Extensions.ObjectPool; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Shared.Diagnostics; | ||
using Microsoft.Shared.Pools; | ||
|
||
namespace Microsoft.AspNetCore.Diagnostics.Buffering; | ||
|
||
internal sealed class IncomingRequestLogBuffer : IDisposable | ||
{ | ||
private const int MaxBatchSize = 256; | ||
private static readonly ObjectPool<List<DeserializedLogRecord>> _recordsToEmitListPool = | ||
PoolFactory.CreateListPoolWithCapacity<DeserializedLogRecord>(MaxBatchSize); | ||
|
||
private readonly IBufferedLogger _bufferedLogger; | ||
private readonly LogBufferingFilterRuleSelector _ruleSelector; | ||
private readonly IOptionsMonitor<PerRequestLogBufferingOptions> _options; | ||
private readonly TimeProvider _timeProvider = TimeProvider.System; | ||
private readonly LogBufferingFilterRule[] _filterRules; | ||
private readonly Lock _bufferSwapLock = new(); | ||
private volatile bool _disposed; | ||
private ConcurrentQueue<SerializedLogRecord> _activeBuffer = new(); | ||
private ConcurrentQueue<SerializedLogRecord> _standbyBuffer = new(); | ||
private int _activeBufferSize; | ||
private DateTimeOffset _lastFlushTimestamp; | ||
|
||
public IncomingRequestLogBuffer( | ||
IBufferedLogger bufferedLogger, | ||
string category, | ||
LogBufferingFilterRuleSelector ruleSelector, | ||
IOptionsMonitor<PerRequestLogBufferingOptions> options) | ||
{ | ||
_bufferedLogger = bufferedLogger; | ||
_ruleSelector = ruleSelector; | ||
_options = options; | ||
_filterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), category); | ||
} | ||
|
||
public bool TryEnqueue<TState>(LogEntry<TState> logEntry) | ||
{ | ||
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.AutoFlushDuration) | ||
{ | ||
return false; | ||
} | ||
|
||
IReadOnlyList<KeyValuePair<string, object?>>? attributes = logEntry.State as IReadOnlyList<KeyValuePair<string, object?>>; | ||
if (attributes is null) | ||
{ | ||
// we expect state to be either ModernTagJoiner or LegacyTagJoiner | ||
// which both implement IReadOnlyList<KeyValuePair<string, object?>> | ||
// and if not, we throw an exception | ||
Throw.InvalidOperationException( | ||
$"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList<KeyValuePair<string, object?>>"); | ||
} | ||
|
||
if (_ruleSelector.Select(_filterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null) | ||
{ | ||
// buffering is not enabled for this log entry, | ||
// return false to indicate that the log entry should be logged normally. | ||
return false; | ||
} | ||
|
||
SerializedLogRecord serializedLogRecord = SerializedLogRecordFactory.Create( | ||
logEntry.LogLevel, | ||
logEntry.EventId, | ||
_timeProvider.GetUtcNow(), | ||
attributes, | ||
logEntry.Exception, | ||
logEntry.Formatter(logEntry.State, logEntry.Exception)); | ||
|
||
if (serializedLogRecord.SizeInBytes > _options.CurrentValue.MaxLogRecordSizeInBytes) | ||
{ | ||
SerializedLogRecordFactory.Return(serializedLogRecord); | ||
return false; | ||
} | ||
|
||
lock (_bufferSwapLock) | ||
{ | ||
_activeBuffer.Enqueue(serializedLogRecord); | ||
_ = Interlocked.Add(ref _activeBufferSize, serializedLogRecord.SizeInBytes); | ||
|
||
} | ||
|
||
TrimExcessRecords(); | ||
|
||
return true; | ||
} | ||
|
||
public void Flush() | ||
{ | ||
_lastFlushTimestamp = _timeProvider.GetUtcNow(); | ||
|
||
ConcurrentQueue<SerializedLogRecord> tempBuffer; | ||
int numItemsToEmit; | ||
lock (_bufferSwapLock) | ||
{ | ||
tempBuffer = _activeBuffer; | ||
_activeBuffer = _standbyBuffer; | ||
_standbyBuffer = tempBuffer; | ||
|
||
numItemsToEmit = tempBuffer.Count; | ||
|
||
_ = Interlocked.Exchange(ref _activeBufferSize, 0); | ||
} | ||
|
||
for (int offset = 0; offset < numItemsToEmit && !tempBuffer.IsEmpty; offset += MaxBatchSize) | ||
{ | ||
int currentBatchSize = Math.Min(MaxBatchSize, numItemsToEmit - offset); | ||
List<DeserializedLogRecord> recordsToEmit = _recordsToEmitListPool.Get(); | ||
try | ||
{ | ||
for (int i = 0; i < currentBatchSize && tempBuffer.TryDequeue(out SerializedLogRecord bufferedRecord); i++) | ||
{ | ||
recordsToEmit.Add(new DeserializedLogRecord( | ||
bufferedRecord.Timestamp, | ||
bufferedRecord.LogLevel, | ||
bufferedRecord.EventId, | ||
bufferedRecord.Exception, | ||
bufferedRecord.FormattedMessage, | ||
bufferedRecord.Attributes)); | ||
} | ||
|
||
_bufferedLogger.LogRecords(recordsToEmit); | ||
} | ||
finally | ||
{ | ||
_recordsToEmitListPool.Return(recordsToEmit); | ||
} | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
if (_disposed) | ||
{ | ||
return; | ||
} | ||
|
||
_disposed = true; | ||
|
||
_ruleSelector.InvalidateCache(); | ||
} | ||
|
||
private void TrimExcessRecords() | ||
{ | ||
while (_activeBufferSize > _options.CurrentValue.MaxPerRequestBufferSizeInBytes && | ||
_activeBuffer.TryDequeue(out SerializedLogRecord item)) | ||
{ | ||
_ = Interlocked.Add(ref _activeBufferSize, -item.SizeInBytes); | ||
SerializedLogRecordFactory.Return(item); | ||
} | ||
} | ||
} | ||
#endif |
40 changes: 40 additions & 0 deletions
40
...s/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBufferHolder.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,40 @@ | ||
// 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; | ||
|
||
namespace Microsoft.AspNetCore.Diagnostics.Buffering; | ||
|
||
internal sealed class IncomingRequestLogBufferHolder : IDisposable | ||
{ | ||
private readonly ConcurrentDictionary<string, IncomingRequestLogBuffer> _buffers = new(); | ||
private bool _disposed; | ||
|
||
public IncomingRequestLogBuffer GetOrAdd(string category, Func<string, IncomingRequestLogBuffer> valueFactory) => | ||
_buffers.GetOrAdd(category, valueFactory); | ||
|
||
public void Flush() | ||
{ | ||
foreach (IncomingRequestLogBuffer buffer in _buffers.Values) | ||
{ | ||
buffer.Flush(); | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
if (_disposed) | ||
{ | ||
return; | ||
} | ||
|
||
_disposed = true; | ||
|
||
foreach (IncomingRequestLogBuffer buffer in _buffers.Values) | ||
{ | ||
buffer.Dispose(); | ||
} | ||
} | ||
} | ||
#endif |
124 changes: 124 additions & 0 deletions
124
...AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.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,124 @@ | ||
// 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.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.Options; | ||
using Microsoft.Shared.DiagnosticIds; | ||
using Microsoft.Shared.Diagnostics; | ||
|
||
namespace Microsoft.Extensions.Logging; | ||
|
||
/// <summary> | ||
/// Lets you register per incoming request log buffering in a dependency injection container. | ||
/// </summary> | ||
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] | ||
public static class PerIncomingRequestLoggingBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds per incoming request log buffering to the logging infrastructure. | ||
/// </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"/> or <paramref name="configuration"/> is <see langword="null"/>.</exception> | ||
/// <remarks> | ||
/// Matched logs will be buffered in a buffer specific to each incoming request | ||
/// and can optionally be flushed and emitted during the request lifetime. | ||
/// </remarks> | ||
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
_ = Throw.IfNull(configuration); | ||
|
||
_ = builder.Services | ||
.AddSingleton<IConfigureOptions<PerRequestLogBufferingOptions>>(new PerRequestLogBufferingConfigureOptions(configuration)) | ||
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>() | ||
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>(); | ||
|
||
return builder | ||
.AddPerRequestBufferManager() | ||
.AddGlobalBuffer(configuration); | ||
} | ||
|
||
/// <summary> | ||
/// Adds per incoming request log buffering to the logging infrastructure. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <param name="configure">The buffering options configuration delegate.</param> | ||
/// <returns>The value of <paramref name="builder"/>.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="configure"/> is <see langword="null"/>.</exception> | ||
/// <remarks> | ||
/// Matched logs will be buffered in a buffer specific to each incoming request | ||
/// and can optionally be flushed and emitted during the request lifetime. | ||
/// </remarks> | ||
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, Action<PerRequestLogBufferingOptions> configure) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
_ = Throw.IfNull(configure); | ||
|
||
_ = builder.Services | ||
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>() | ||
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>() | ||
.Configure(configure); | ||
|
||
PerRequestLogBufferingOptions options = new PerRequestLogBufferingOptions(); | ||
configure(options); | ||
|
||
return builder | ||
.AddPerRequestBufferManager() | ||
.AddGlobalBuffer(opts => opts.Rules = options.Rules); | ||
} | ||
|
||
/// <summary> | ||
/// Adds per incoming request log buffering to the logging infrastructure. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
/// <param name="logLevel">The level (and below) of logs to buffer.</param> | ||
/// <returns>The value of <paramref name="builder"/>.</returns> | ||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
/// <remarks> | ||
/// Matched logs will be buffered in a buffer specific to each incoming request | ||
/// and can optionally be flushed and emitted during the request lifetime. | ||
/// </remarks> | ||
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, LogLevel? logLevel = null) | ||
{ | ||
_ = Throw.IfNull(builder); | ||
|
||
_ = builder.Services | ||
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>() | ||
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>() | ||
.Configure(options => | ||
{ | ||
options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel)); | ||
}); | ||
|
||
return builder | ||
.AddPerRequestBufferManager() | ||
.AddGlobalBuffer(logLevel); | ||
} | ||
|
||
private static ILoggingBuilder AddPerRequestBufferManager(this ILoggingBuilder builder) | ||
{ | ||
builder.Services.TryAddScoped<IncomingRequestLogBufferHolder>(); | ||
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||
builder.Services.TryAddSingleton(sp => | ||
{ | ||
var globalBufferManager = sp.GetRequiredService<GlobalLogBufferManager>(); | ||
return ActivatorUtilities.CreateInstance<PerRequestLogBufferManager>(sp, globalBufferManager); | ||
}); | ||
builder.Services.TryAddSingleton<LogBuffer>(sp => sp.GetRequiredService<PerRequestLogBufferManager>()); | ||
builder.Services.TryAddSingleton<PerRequestLogBuffer>(sp => sp.GetRequiredService<PerRequestLogBufferManager>()); | ||
|
||
return builder; | ||
} | ||
} | ||
|
||
#endif |
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.