Skip to content

Commit 3ebeec1

Browse files
Add logging buffering (#5635)
1 parent 06b7eba commit 3ebeec1

File tree

52 files changed

+2912
-51
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2912
-51
lines changed

eng/MSBuild/Shared.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@
5050
<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
5151
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
5252
</ItemGroup>
53+
54+
<ItemGroup Condition="'$(InjectSharedLogBuffering)' == 'true'">
55+
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\LogBuffering\*.cs" LinkBase="Shared\LogBuffering" />
56+
</ItemGroup>
5357
</Project>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#if NET9_0_OR_GREATER
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading;
10+
using Microsoft.Extensions.Diagnostics.Buffering;
11+
using Microsoft.Extensions.Logging.Abstractions;
12+
using Microsoft.Extensions.ObjectPool;
13+
using Microsoft.Extensions.Options;
14+
using Microsoft.Shared.Diagnostics;
15+
using Microsoft.Shared.Pools;
16+
17+
namespace Microsoft.AspNetCore.Diagnostics.Buffering;
18+
19+
internal sealed class IncomingRequestLogBuffer : IDisposable
20+
{
21+
private const int MaxBatchSize = 256;
22+
private static readonly ObjectPool<List<DeserializedLogRecord>> _recordsToEmitListPool =
23+
PoolFactory.CreateListPoolWithCapacity<DeserializedLogRecord>(MaxBatchSize);
24+
25+
private readonly IBufferedLogger _bufferedLogger;
26+
private readonly LogBufferingFilterRuleSelector _ruleSelector;
27+
private readonly IOptionsMonitor<PerRequestLogBufferingOptions> _options;
28+
private readonly TimeProvider _timeProvider = TimeProvider.System;
29+
private readonly LogBufferingFilterRule[] _filterRules;
30+
private readonly Lock _bufferSwapLock = new();
31+
private volatile bool _disposed;
32+
private ConcurrentQueue<SerializedLogRecord> _activeBuffer = new();
33+
private ConcurrentQueue<SerializedLogRecord> _standbyBuffer = new();
34+
private int _activeBufferSize;
35+
private DateTimeOffset _lastFlushTimestamp;
36+
37+
public IncomingRequestLogBuffer(
38+
IBufferedLogger bufferedLogger,
39+
string category,
40+
LogBufferingFilterRuleSelector ruleSelector,
41+
IOptionsMonitor<PerRequestLogBufferingOptions> options)
42+
{
43+
_bufferedLogger = bufferedLogger;
44+
_ruleSelector = ruleSelector;
45+
_options = options;
46+
_filterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), category);
47+
}
48+
49+
public bool TryEnqueue<TState>(LogEntry<TState> logEntry)
50+
{
51+
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.AutoFlushDuration)
52+
{
53+
return false;
54+
}
55+
56+
IReadOnlyList<KeyValuePair<string, object?>>? attributes = logEntry.State as IReadOnlyList<KeyValuePair<string, object?>>;
57+
if (attributes is null)
58+
{
59+
// we expect state to be either ModernTagJoiner or LegacyTagJoiner
60+
// which both implement IReadOnlyList<KeyValuePair<string, object?>>
61+
// and if not, we throw an exception
62+
Throw.InvalidOperationException(
63+
$"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList<KeyValuePair<string, object?>>");
64+
}
65+
66+
if (_ruleSelector.Select(_filterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null)
67+
{
68+
// buffering is not enabled for this log entry,
69+
// return false to indicate that the log entry should be logged normally.
70+
return false;
71+
}
72+
73+
SerializedLogRecord serializedLogRecord = SerializedLogRecordFactory.Create(
74+
logEntry.LogLevel,
75+
logEntry.EventId,
76+
_timeProvider.GetUtcNow(),
77+
attributes,
78+
logEntry.Exception,
79+
logEntry.Formatter(logEntry.State, logEntry.Exception));
80+
81+
if (serializedLogRecord.SizeInBytes > _options.CurrentValue.MaxLogRecordSizeInBytes)
82+
{
83+
SerializedLogRecordFactory.Return(serializedLogRecord);
84+
return false;
85+
}
86+
87+
lock (_bufferSwapLock)
88+
{
89+
_activeBuffer.Enqueue(serializedLogRecord);
90+
_ = Interlocked.Add(ref _activeBufferSize, serializedLogRecord.SizeInBytes);
91+
92+
}
93+
94+
TrimExcessRecords();
95+
96+
return true;
97+
}
98+
99+
public void Flush()
100+
{
101+
_lastFlushTimestamp = _timeProvider.GetUtcNow();
102+
103+
ConcurrentQueue<SerializedLogRecord> tempBuffer;
104+
int numItemsToEmit;
105+
lock (_bufferSwapLock)
106+
{
107+
tempBuffer = _activeBuffer;
108+
_activeBuffer = _standbyBuffer;
109+
_standbyBuffer = tempBuffer;
110+
111+
numItemsToEmit = tempBuffer.Count;
112+
113+
_ = Interlocked.Exchange(ref _activeBufferSize, 0);
114+
}
115+
116+
for (int offset = 0; offset < numItemsToEmit && !tempBuffer.IsEmpty; offset += MaxBatchSize)
117+
{
118+
int currentBatchSize = Math.Min(MaxBatchSize, numItemsToEmit - offset);
119+
List<DeserializedLogRecord> recordsToEmit = _recordsToEmitListPool.Get();
120+
try
121+
{
122+
for (int i = 0; i < currentBatchSize && tempBuffer.TryDequeue(out SerializedLogRecord bufferedRecord); i++)
123+
{
124+
recordsToEmit.Add(new DeserializedLogRecord(
125+
bufferedRecord.Timestamp,
126+
bufferedRecord.LogLevel,
127+
bufferedRecord.EventId,
128+
bufferedRecord.Exception,
129+
bufferedRecord.FormattedMessage,
130+
bufferedRecord.Attributes));
131+
}
132+
133+
_bufferedLogger.LogRecords(recordsToEmit);
134+
}
135+
finally
136+
{
137+
_recordsToEmitListPool.Return(recordsToEmit);
138+
}
139+
}
140+
}
141+
142+
public void Dispose()
143+
{
144+
if (_disposed)
145+
{
146+
return;
147+
}
148+
149+
_disposed = true;
150+
151+
_ruleSelector.InvalidateCache();
152+
}
153+
154+
private void TrimExcessRecords()
155+
{
156+
while (_activeBufferSize > _options.CurrentValue.MaxPerRequestBufferSizeInBytes &&
157+
_activeBuffer.TryDequeue(out SerializedLogRecord item))
158+
{
159+
_ = Interlocked.Add(ref _activeBufferSize, -item.SizeInBytes);
160+
SerializedLogRecordFactory.Return(item);
161+
}
162+
}
163+
}
164+
#endif
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
#if NET9_0_OR_GREATER
4+
using System;
5+
using System.Collections.Concurrent;
6+
7+
namespace Microsoft.AspNetCore.Diagnostics.Buffering;
8+
9+
internal sealed class IncomingRequestLogBufferHolder : IDisposable
10+
{
11+
private readonly ConcurrentDictionary<string, IncomingRequestLogBuffer> _buffers = new();
12+
private bool _disposed;
13+
14+
public IncomingRequestLogBuffer GetOrAdd(string category, Func<string, IncomingRequestLogBuffer> valueFactory) =>
15+
_buffers.GetOrAdd(category, valueFactory);
16+
17+
public void Flush()
18+
{
19+
foreach (IncomingRequestLogBuffer buffer in _buffers.Values)
20+
{
21+
buffer.Flush();
22+
}
23+
}
24+
25+
public void Dispose()
26+
{
27+
if (_disposed)
28+
{
29+
return;
30+
}
31+
32+
_disposed = true;
33+
34+
foreach (IncomingRequestLogBuffer buffer in _buffers.Values)
35+
{
36+
buffer.Dispose();
37+
}
38+
}
39+
}
40+
#endif
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
#if NET9_0_OR_GREATER
4+
5+
using System;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.AspNetCore.Diagnostics.Buffering;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.Configuration;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.DependencyInjection.Extensions;
12+
using Microsoft.Extensions.Diagnostics.Buffering;
13+
using Microsoft.Extensions.Options;
14+
using Microsoft.Shared.DiagnosticIds;
15+
using Microsoft.Shared.Diagnostics;
16+
17+
namespace Microsoft.Extensions.Logging;
18+
19+
/// <summary>
20+
/// Lets you register per incoming request log buffering in a dependency injection container.
21+
/// </summary>
22+
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
23+
public static class PerIncomingRequestLoggingBuilderExtensions
24+
{
25+
/// <summary>
26+
/// Adds per incoming request log buffering to the logging infrastructure.
27+
/// </summary>
28+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
29+
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param>
30+
/// <returns>The value of <paramref name="builder"/>.</returns>
31+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="configuration"/> is <see langword="null"/>.</exception>
32+
/// <remarks>
33+
/// Matched logs will be buffered in a buffer specific to each incoming request
34+
/// and can optionally be flushed and emitted during the request lifetime.
35+
/// </remarks>
36+
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
37+
{
38+
_ = Throw.IfNull(builder);
39+
_ = Throw.IfNull(configuration);
40+
41+
_ = builder.Services
42+
.AddSingleton<IConfigureOptions<PerRequestLogBufferingOptions>>(new PerRequestLogBufferingConfigureOptions(configuration))
43+
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
44+
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>();
45+
46+
return builder
47+
.AddPerRequestBufferManager()
48+
.AddGlobalBuffer(configuration);
49+
}
50+
51+
/// <summary>
52+
/// Adds per incoming request log buffering to the logging infrastructure.
53+
/// </summary>
54+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
55+
/// <param name="configure">The buffering options configuration delegate.</param>
56+
/// <returns>The value of <paramref name="builder"/>.</returns>
57+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
58+
/// <remarks>
59+
/// Matched logs will be buffered in a buffer specific to each incoming request
60+
/// and can optionally be flushed and emitted during the request lifetime.
61+
/// </remarks>
62+
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, Action<PerRequestLogBufferingOptions> configure)
63+
{
64+
_ = Throw.IfNull(builder);
65+
_ = Throw.IfNull(configure);
66+
67+
_ = builder.Services
68+
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
69+
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>()
70+
.Configure(configure);
71+
72+
PerRequestLogBufferingOptions options = new PerRequestLogBufferingOptions();
73+
configure(options);
74+
75+
return builder
76+
.AddPerRequestBufferManager()
77+
.AddGlobalBuffer(opts => opts.Rules = options.Rules);
78+
}
79+
80+
/// <summary>
81+
/// Adds per incoming request log buffering to the logging infrastructure.
82+
/// </summary>
83+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
84+
/// <param name="logLevel">The level (and below) of logs to buffer.</param>
85+
/// <returns>The value of <paramref name="builder"/>.</returns>
86+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
87+
/// <remarks>
88+
/// Matched logs will be buffered in a buffer specific to each incoming request
89+
/// and can optionally be flushed and emitted during the request lifetime.
90+
/// </remarks>
91+
public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, LogLevel? logLevel = null)
92+
{
93+
_ = Throw.IfNull(builder);
94+
95+
_ = builder.Services
96+
.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsValidator>()
97+
.Services.AddOptionsWithValidateOnStart<PerRequestLogBufferingOptions, PerRequestLogBufferingOptionsCustomValidator>()
98+
.Configure(options =>
99+
{
100+
options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel));
101+
});
102+
103+
return builder
104+
.AddPerRequestBufferManager()
105+
.AddGlobalBuffer(logLevel);
106+
}
107+
108+
private static ILoggingBuilder AddPerRequestBufferManager(this ILoggingBuilder builder)
109+
{
110+
builder.Services.TryAddScoped<IncomingRequestLogBufferHolder>();
111+
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
112+
builder.Services.TryAddSingleton(sp =>
113+
{
114+
var globalBufferManager = sp.GetRequiredService<GlobalLogBufferManager>();
115+
return ActivatorUtilities.CreateInstance<PerRequestLogBufferManager>(sp, globalBufferManager);
116+
});
117+
builder.Services.TryAddSingleton<LogBuffer>(sp => sp.GetRequiredService<PerRequestLogBufferManager>());
118+
builder.Services.TryAddSingleton<PerRequestLogBuffer>(sp => sp.GetRequiredService<PerRequestLogBufferManager>());
119+
120+
return builder;
121+
}
122+
}
123+
124+
#endif

0 commit comments

Comments
 (0)