Skip to content

Commit 2f1a335

Browse files
Major update
1 parent 988c709 commit 2f1a335

35 files changed

+1081
-416
lines changed

eng/MSBuild/Shared.props

+4
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,8 @@
4242
<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
4343
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
4444
</ItemGroup>
45+
46+
<ItemGroup Condition="'$(InjectJsonExceptionConverter)' == 'true'">
47+
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\JsonExceptionConverter\*.cs" LinkBase="Shared\JsonExceptionConverter" />
48+
</ItemGroup>
4549
</Project>

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs

+57-40
Original file line numberDiff line numberDiff line change
@@ -4,94 +4,111 @@
44
#if NET9_0_OR_GREATER
55
using System;
66
using System.Collections.Concurrent;
7-
using System.Collections.Generic;
7+
using System.Diagnostics.CodeAnalysis;
88
using Microsoft.Extensions.Diagnostics;
99
using Microsoft.Extensions.Logging;
10-
using Microsoft.Extensions.Logging.Abstractions;
1110
using Microsoft.Extensions.Options;
11+
using Microsoft.Shared.Diagnostics;
12+
using static Microsoft.Extensions.Logging.ExtendedLogger;
1213

1314
namespace Microsoft.AspNetCore.Diagnostics.Logging;
1415

1516
internal sealed class HttpRequestBuffer : ILoggingBuffer
1617
{
1718
private readonly IOptionsMonitor<HttpRequestBufferOptions> _options;
18-
private readonly ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>> _buffers;
19+
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions;
20+
private readonly ConcurrentQueue<SerializedLogRecord> _buffer;
1921
private readonly TimeProvider _timeProvider = TimeProvider.System;
22+
private readonly IBufferSink _bufferSink;
23+
private readonly object _bufferCapacityLocker = new();
24+
private DateTimeOffset _truncateAfter;
2025
private DateTimeOffset _lastFlushTimestamp;
2126

22-
public HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options)
27+
public HttpRequestBuffer(IBufferSink bufferSink,
28+
IOptionsMonitor<HttpRequestBufferOptions> options,
29+
IOptionsMonitor<GlobalBufferOptions> globalOptions)
2330
{
2431
_options = options;
25-
_buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>();
26-
_lastFlushTimestamp = _timeProvider.GetUtcNow();
27-
}
32+
_globalOptions = globalOptions;
33+
_bufferSink = bufferSink;
34+
_buffer = new ConcurrentQueue<SerializedLogRecord>();
2835

29-
internal HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options, TimeProvider timeProvider)
30-
: this(options)
31-
{
32-
_timeProvider = timeProvider;
33-
_lastFlushTimestamp = _timeProvider.GetUtcNow();
36+
_truncateAfter = _timeProvider.GetUtcNow();
3437
}
3538

36-
public bool TryEnqueue(
37-
IBufferedLogger logger,
39+
[RequiresUnreferencedCode(
40+
"Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList<KeyValuePair<String, Object>>, Exception, String)")]
41+
public bool TryEnqueue<TState>(
3842
LogLevel logLevel,
3943
string category,
4044
EventId eventId,
41-
IReadOnlyList<KeyValuePair<string, object?>> joiner,
45+
TState attributes,
4246
Exception? exception,
43-
string formatter)
47+
Func<TState, Exception?, string> formatter)
4448
{
4549
if (!IsEnabled(category, logLevel, eventId))
4650
{
4751
return false;
4852
}
4953

50-
var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter);
51-
var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue<HttpRequestBufferedLogRecord>());
52-
53-
// probably don't need to limit buffer capacity?
54-
// because buffer is disposed when the respective HttpContext is disposed
55-
// don't expect it to grow so much to cause a problem?
56-
if (queue.Count >= _options.CurrentValue.PerRequestCapacity)
54+
switch (attributes)
5755
{
58-
_ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
56+
case ModernTagJoiner modernTagJoiner:
57+
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception,
58+
((Func<ModernTagJoiner, Exception?, string>)(object)formatter)(modernTagJoiner, exception)));
59+
break;
60+
case LegacyTagJoiner legacyTagJoiner:
61+
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception,
62+
((Func<LegacyTagJoiner, Exception?, string>)(object)formatter)(legacyTagJoiner, exception)));
63+
break;
64+
default:
65+
Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}");
66+
break;
5967
}
6068

61-
queue.Enqueue(record);
69+
var now = _timeProvider.GetUtcNow();
70+
lock (_bufferCapacityLocker)
71+
{
72+
if (now >= _truncateAfter)
73+
{
74+
_truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration);
75+
TruncateOverlimit();
76+
}
77+
}
6278

6379
return true;
6480
}
6581

82+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable<SerializedLogRecord>)")]
6683
public void Flush()
6784
{
68-
foreach (var (logger, queue) in _buffers)
69-
{
70-
var result = new List<BufferedLogRecord>();
71-
while (!queue.IsEmpty)
72-
{
73-
if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item))
74-
{
75-
result.Add(item);
76-
}
77-
}
78-
79-
logger.LogRecords(result);
80-
}
85+
var result = _buffer.ToArray();
86+
_buffer.Clear();
8187

8288
_lastFlushTimestamp = _timeProvider.GetUtcNow();
89+
90+
_bufferSink.LogRecords(result);
8391
}
8492

8593
public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
8694
{
87-
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration)
95+
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration)
8896
{
8997
return false;
9098
}
9199

92-
LoggerFilterRuleSelector.Select<BufferFilterRule>(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
100+
LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
93101

94102
return rule is not null;
95103
}
104+
105+
public void TruncateOverlimit()
106+
{
107+
// Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments.
108+
while (_buffer.Count > _options.CurrentValue.PerRequestCapacity)
109+
{
110+
_ = _buffer.TryDequeue(out _);
111+
}
112+
}
96113
}
97114
#endif

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs

+19-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Extensions.Configuration;
1010
using Microsoft.Extensions.DependencyInjection;
1111
using Microsoft.Extensions.DependencyInjection.Extensions;
12+
using Microsoft.Extensions.Diagnostics.Buffering;
1213
using Microsoft.Extensions.Logging;
1314
using Microsoft.Extensions.Options;
1415
using Microsoft.Shared.DiagnosticIds;
@@ -30,14 +31,16 @@ public static class HttpRequestBufferLoggerBuilderExtensions
3031
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param>
3132
/// <returns>The value of <paramref name="builder"/>.</returns>
3233
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
33-
public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
34+
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
3435
{
3536
_ = Throw.IfNull(builder);
3637
_ = Throw.IfNull(configuration);
3738

3839
return builder
3940
.AddHttpRequestBufferConfiguration(configuration)
40-
.AddHttpRequestBufferProvider();
41+
.AddHttpRequestBufferManager()
42+
.AddGlobalBufferConfiguration(configuration)
43+
.AddGlobalBufferManager();
4144
}
4245

4346
/// <summary>
@@ -49,15 +52,18 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
4952
/// <param name="configure">The buffer configuration options.</param>
5053
/// <returns>The value of <paramref name="builder"/>.</returns>
5154
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
52-
public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action<HttpRequestBufferOptions>? configure = null)
55+
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action<HttpRequestBufferOptions>? configure = null)
5356
{
5457
_ = Throw.IfNull(builder);
5558

5659
_ = builder.Services
5760
.Configure<HttpRequestBufferOptions>(options => options.Rules.Add(new BufferFilterRule(null, level, null)))
5861
.Configure(configure ?? new Action<HttpRequestBufferOptions>(_ => { }));
5962

60-
return builder.AddHttpRequestBufferProvider();
63+
return builder
64+
.AddHttpRequestBufferManager()
65+
.AddGlobalBuffer(level)
66+
.AddGlobalBufferManager();
6167
}
6268

6369
/// <summary>
@@ -66,16 +72,20 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
6672
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
6773
/// <returns>The <see cref="ILoggingBuilder"/> so that additional calls can be chained.</returns>
6874
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
69-
public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder)
75+
internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
7076
{
7177
_ = Throw.IfNull(builder);
7278

73-
builder.Services.TryAddScoped<HttpRequestBuffer>();
74-
builder.Services.TryAddScoped<ILoggingBuffer>(sp => sp.GetRequiredService<HttpRequestBuffer>());
7579
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
76-
builder.Services.TryAddActivatedSingleton<ILoggingBufferProvider, HttpRequestBufferProvider>();
7780

78-
return builder.AddGlobalBufferProvider();
81+
builder.Services.TryAddSingleton<ExtendedLoggerFactory>();
82+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerFactory, ExtendedLoggerFactory>(sp => sp.GetRequiredService<ExtendedLoggerFactory>()));
83+
84+
builder.Services.TryAddSingleton<HttpRequestBufferManager>();
85+
builder.Services.TryAddSingleton<IBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>());
86+
builder.Services.TryAddSingleton<IHttpRequestBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>());
87+
88+
return builder;
7989
}
8090

8191
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.AspNetCore.Diagnostics.Logging;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.Extensions.Diagnostics.Buffering;
13+
internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager
14+
{
15+
private readonly GlobalBufferManager _globalBufferManager;
16+
private readonly IHttpContextAccessor _httpContextAccessor;
17+
private readonly IOptionsMonitor<HttpRequestBufferOptions> _requestOptions;
18+
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions;
19+
20+
public HttpRequestBufferManager(
21+
GlobalBufferManager globalBufferManager,
22+
IHttpContextAccessor httpContextAccessor,
23+
IOptionsMonitor<HttpRequestBufferOptions> requestOptions,
24+
IOptionsMonitor<GlobalBufferOptions> globalOptions)
25+
{
26+
_globalBufferManager = globalBufferManager;
27+
_httpContextAccessor = httpContextAccessor;
28+
_requestOptions = requestOptions;
29+
_globalOptions = globalOptions;
30+
}
31+
32+
public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category)
33+
{
34+
var httpContext = _httpContextAccessor.HttpContext;
35+
if (httpContext is null)
36+
{
37+
return _globalBufferManager.CreateBuffer(bufferSink, category);
38+
}
39+
40+
if (!httpContext.Items.TryGetValue(category, out var buffer))
41+
{
42+
var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions, _globalOptions);
43+
httpContext.Items[category] = httpRequestBuffer;
44+
return httpRequestBuffer;
45+
}
46+
47+
if (buffer is not ILoggingBuffer loggingBuffer)
48+
{
49+
throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}");
50+
}
51+
52+
return loggingBuffer;
53+
}
54+
55+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
56+
public void Flush() => _globalBufferManager.Flush();
57+
58+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
59+
public void FlushCurrentRequestLogs()
60+
{
61+
if (_httpContextAccessor.HttpContext is not null)
62+
{
63+
foreach (var kvp in _httpContextAccessor.HttpContext!.Items)
64+
{
65+
if (kvp.Value is ILoggingBuffer buffer)
66+
{
67+
buffer.Flush();
68+
}
69+
}
70+
}
71+
}
72+
73+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue<TState>(LogLevel, String, EventId, TState, Exception, Func<TState, Exception, String>)")]
74+
public bool TryEnqueue<TState>(
75+
IBufferSink bufferSink,
76+
LogLevel logLevel,
77+
string category,
78+
EventId eventId,
79+
TState attributes,
80+
Exception? exception,
81+
Func<TState, Exception?, string> formatter)
82+
{
83+
var buffer = CreateBuffer(bufferSink, category);
84+
return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter);
85+
}
86+
}
87+
#endif

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs

+2-11
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,15 @@ namespace Microsoft.AspNetCore.Diagnostics.Logging;
1717
public class HttpRequestBufferOptions
1818
{
1919
/// <summary>
20-
/// Gets or sets the time to suspend the buffer after flushing.
20+
/// Gets or sets the duration to check and remove the buffered items exceeding the <see cref="PerRequestCapacity"/>.
2121
/// </summary>
22-
/// <remarks>
23-
/// 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,
24-
/// so the buffering will be suspended for the <see paramref="SuspendAfterFlushDuration"/> time.
25-
/// </remarks>
26-
public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30);
22+
public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10);
2723

2824
/// <summary>
2925
/// Gets or sets the size of the buffer for a request.
3026
/// </summary>
3127
public int PerRequestCapacity { get; set; } = 1_000;
3228

33-
/// <summary>
34-
/// Gets or sets the size of the global buffer which applies to non-request logs only.
35-
/// </summary>
36-
public int GlobalCapacity { get; set; } = 1_000_000;
37-
3829
#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
3930
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
4031
/// <summary>

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs

-30
This file was deleted.

0 commit comments

Comments
 (0)