Skip to content

Commit e40b7b6

Browse files
Major update
1 parent 988c709 commit e40b7b6

35 files changed

+1055
-418
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

+52-39
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,86 @@
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 ConcurrentQueue<SerializedLogRecord> _buffer;
1920
private readonly TimeProvider _timeProvider = TimeProvider.System;
21+
private readonly IBufferSink _bufferSink;
22+
private readonly object _bufferCapacityLocker = new();
23+
private DateTimeOffset _truncateAfter;
2024
private DateTimeOffset _lastFlushTimestamp;
2125

22-
public HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options)
26+
public HttpRequestBuffer(IBufferSink bufferSink, IOptionsMonitor<HttpRequestBufferOptions> options)
2327
{
2428
_options = options;
25-
_buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>();
26-
_lastFlushTimestamp = _timeProvider.GetUtcNow();
27-
}
29+
_bufferSink = bufferSink;
30+
_buffer = new ConcurrentQueue<SerializedLogRecord>();
2831

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

36-
public bool TryEnqueue(
37-
IBufferedLogger logger,
35+
[RequiresUnreferencedCode(
36+
"Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList<KeyValuePair<String, Object>>, Exception, String)")]
37+
public bool TryEnqueue<TState>(
3838
LogLevel logLevel,
3939
string category,
4040
EventId eventId,
41-
IReadOnlyList<KeyValuePair<string, object?>> joiner,
41+
TState attributes,
4242
Exception? exception,
43-
string formatter)
43+
Func<TState, Exception?, string> formatter)
4444
{
4545
if (!IsEnabled(category, logLevel, eventId))
4646
{
4747
return false;
4848
}
4949

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)
50+
switch (attributes)
5751
{
58-
_ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
52+
case ModernTagJoiner modernTagJoiner:
53+
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception,
54+
((Func<ModernTagJoiner, Exception?, string>)(object)formatter)(modernTagJoiner, exception)));
55+
break;
56+
case LegacyTagJoiner legacyTagJoiner:
57+
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception,
58+
((Func<LegacyTagJoiner, Exception?, string>)(object)formatter)(legacyTagJoiner, exception)));
59+
break;
60+
default:
61+
Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}");
62+
break;
5963
}
6064

61-
queue.Enqueue(record);
65+
var now = _timeProvider.GetUtcNow();
66+
lock (_bufferCapacityLocker)
67+
{
68+
if (now >= _truncateAfter)
69+
{
70+
_truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration);
71+
TruncateOverlimit();
72+
}
73+
}
6274

6375
return true;
6476
}
6577

78+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable<SerializedLogRecord>)")]
6679
public void Flush()
6780
{
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-
}
81+
var result = _buffer.ToArray();
82+
_buffer.Clear();
8183

8284
_lastFlushTimestamp = _timeProvider.GetUtcNow();
85+
86+
_bufferSink.LogRecords(result);
8387
}
8488

8589
public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
@@ -89,9 +93,18 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
8993
return false;
9094
}
9195

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

9498
return rule is not null;
9599
}
100+
101+
public void TruncateOverlimit()
102+
{
103+
// Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments.
104+
while (_buffer.Count > _options.CurrentValue.PerRequestCapacity)
105+
{
106+
_ = _buffer.TryDequeue(out _);
107+
}
108+
}
96109
}
97110
#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,84 @@
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+
19+
public HttpRequestBufferManager(
20+
GlobalBufferManager globalBufferManager,
21+
IHttpContextAccessor httpContextAccessor,
22+
IOptionsMonitor<HttpRequestBufferOptions> requestOptions)
23+
{
24+
_globalBufferManager = globalBufferManager;
25+
_httpContextAccessor = httpContextAccessor;
26+
_requestOptions = requestOptions;
27+
}
28+
29+
public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category)
30+
{
31+
var httpContext = _httpContextAccessor.HttpContext;
32+
if (httpContext is null)
33+
{
34+
return _globalBufferManager.CreateBuffer(bufferSink, category);
35+
}
36+
37+
if (!httpContext.Items.TryGetValue(category, out var buffer))
38+
{
39+
var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions);
40+
httpContext.Items[category] = httpRequestBuffer;
41+
return httpRequestBuffer;
42+
}
43+
44+
if (buffer is not ILoggingBuffer loggingBuffer)
45+
{
46+
throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}");
47+
}
48+
49+
return loggingBuffer;
50+
}
51+
52+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
53+
public void Flush() => _globalBufferManager.Flush();
54+
55+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
56+
public void FlushCurrentRequestLogs()
57+
{
58+
if (_httpContextAccessor.HttpContext is not null)
59+
{
60+
foreach (var kvp in _httpContextAccessor.HttpContext!.Items)
61+
{
62+
if (kvp.Value is ILoggingBuffer buffer)
63+
{
64+
buffer.Flush();
65+
}
66+
}
67+
}
68+
}
69+
70+
[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue<TState>(LogLevel, String, EventId, TState, Exception, Func<TState, Exception, String>)")]
71+
public bool TryEnqueue<TState>(
72+
IBufferSink bufferSink,
73+
LogLevel logLevel,
74+
string category,
75+
EventId eventId,
76+
TState attributes,
77+
Exception? exception,
78+
Func<TState, Exception?, string> formatter)
79+
{
80+
var buffer = CreateBuffer(bufferSink, category);
81+
return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter);
82+
}
83+
}
84+
#endif

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ public class HttpRequestBufferOptions
2626
public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30);
2727

2828
/// <summary>
29-
/// Gets or sets the size of the buffer for a request.
29+
/// Gets or sets the duration to check and remove the buffered items exceeding the <see cref="PerRequestCapacity"/>.
3030
/// </summary>
31-
public int PerRequestCapacity { get; set; } = 1_000;
31+
public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10);
3232

3333
/// <summary>
34-
/// Gets or sets the size of the global buffer which applies to non-request logs only.
34+
/// Gets or sets the size of the buffer for a request.
3535
/// </summary>
36-
public int GlobalCapacity { get; set; } = 1_000_000;
36+
public int PerRequestCapacity { get; set; } = 1_000;
3737

3838
#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
3939
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern

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

-30
This file was deleted.

0 commit comments

Comments
 (0)