Skip to content

Commit 988c709

Browse files
Buffering
1 parent 1073446 commit 988c709

32 files changed

+1374
-5
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 Microsoft.Extensions.Diagnostics;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Logging.Abstractions;
11+
using Microsoft.Extensions.Options;
12+
13+
namespace Microsoft.AspNetCore.Diagnostics.Logging;
14+
15+
internal sealed class HttpRequestBuffer : ILoggingBuffer
16+
{
17+
private readonly IOptionsMonitor<HttpRequestBufferOptions> _options;
18+
private readonly ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>> _buffers;
19+
private readonly TimeProvider _timeProvider = TimeProvider.System;
20+
private DateTimeOffset _lastFlushTimestamp;
21+
22+
public HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options)
23+
{
24+
_options = options;
25+
_buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>();
26+
_lastFlushTimestamp = _timeProvider.GetUtcNow();
27+
}
28+
29+
internal HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options, TimeProvider timeProvider)
30+
: this(options)
31+
{
32+
_timeProvider = timeProvider;
33+
_lastFlushTimestamp = _timeProvider.GetUtcNow();
34+
}
35+
36+
public bool TryEnqueue(
37+
IBufferedLogger logger,
38+
LogLevel logLevel,
39+
string category,
40+
EventId eventId,
41+
IReadOnlyList<KeyValuePair<string, object?>> joiner,
42+
Exception? exception,
43+
string formatter)
44+
{
45+
if (!IsEnabled(category, logLevel, eventId))
46+
{
47+
return false;
48+
}
49+
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)
57+
{
58+
_ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
59+
}
60+
61+
queue.Enqueue(record);
62+
63+
return true;
64+
}
65+
66+
public void Flush()
67+
{
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+
82+
_lastFlushTimestamp = _timeProvider.GetUtcNow();
83+
}
84+
85+
public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
86+
{
87+
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration)
88+
{
89+
return false;
90+
}
91+
92+
LoggerFilterRuleSelector.Select<BufferFilterRule>(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
93+
94+
return rule is not null;
95+
}
96+
}
97+
#endif
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.Collections.Generic;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Microsoft.AspNetCore.Diagnostics.Logging;
10+
11+
internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions<HttpRequestBufferOptions>
12+
{
13+
private const string BufferingKey = "Buffering";
14+
private readonly IConfiguration _configuration;
15+
16+
public HttpRequestBufferConfigureOptions(IConfiguration configuration)
17+
{
18+
_configuration = configuration;
19+
}
20+
21+
public void Configure(HttpRequestBufferOptions options)
22+
{
23+
if (_configuration == null)
24+
{
25+
return;
26+
}
27+
28+
var section = _configuration.GetSection(BufferingKey);
29+
if (!section.Exists())
30+
{
31+
return;
32+
}
33+
34+
var parsedOptions = section.Get<HttpRequestBufferOptions>();
35+
if (parsedOptions is null)
36+
{
37+
return;
38+
}
39+
40+
options.Rules.AddRange(parsedOptions.Rules);
41+
}
42+
}
43+
#endif
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.Diagnostics.CodeAnalysis;
7+
using Microsoft.AspNetCore.Diagnostics.Logging;
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.Logging;
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 log buffers in a dependency injection container.
21+
/// </summary>
22+
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
23+
public static class HttpRequestBufferLoggerBuilderExtensions
24+
{
25+
/// <summary>
26+
/// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in
27+
/// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>.
28+
/// </summary>
29+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
30+
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param>
31+
/// <returns>The value of <paramref name="builder"/>.</returns>
32+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
33+
public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
34+
{
35+
_ = Throw.IfNull(builder);
36+
_ = Throw.IfNull(configuration);
37+
38+
return builder
39+
.AddHttpRequestBufferConfiguration(configuration)
40+
.AddHttpRequestBufferProvider();
41+
}
42+
43+
/// <summary>
44+
/// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in
45+
/// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>.
46+
/// </summary>
47+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
48+
/// <param name="level">The log level (and below) to apply the buffer to.</param>
49+
/// <param name="configure">The buffer configuration options.</param>
50+
/// <returns>The value of <paramref name="builder"/>.</returns>
51+
/// <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)
53+
{
54+
_ = Throw.IfNull(builder);
55+
56+
_ = builder.Services
57+
.Configure<HttpRequestBufferOptions>(options => options.Rules.Add(new BufferFilterRule(null, level, null)))
58+
.Configure(configure ?? new Action<HttpRequestBufferOptions>(_ => { }));
59+
60+
return builder.AddHttpRequestBufferProvider();
61+
}
62+
63+
/// <summary>
64+
/// Adds HTTP request buffer provider to the logging infrastructure.
65+
/// </summary>
66+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
67+
/// <returns>The <see cref="ILoggingBuilder"/> so that additional calls can be chained.</returns>
68+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
69+
public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder)
70+
{
71+
_ = Throw.IfNull(builder);
72+
73+
builder.Services.TryAddScoped<HttpRequestBuffer>();
74+
builder.Services.TryAddScoped<ILoggingBuffer>(sp => sp.GetRequiredService<HttpRequestBuffer>());
75+
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
76+
builder.Services.TryAddActivatedSingleton<ILoggingBufferProvider, HttpRequestBufferProvider>();
77+
78+
return builder.AddGlobalBufferProvider();
79+
}
80+
81+
/// <summary>
82+
/// Configures <see cref="HttpRequestBufferOptions" /> from an instance of <see cref="IConfiguration" />.
83+
/// </summary>
84+
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
85+
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param>
86+
/// <returns>The value of <paramref name="builder"/>.</returns>
87+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
88+
internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration)
89+
{
90+
_ = Throw.IfNull(builder);
91+
92+
_ = builder.Services.AddSingleton<IConfigureOptions<HttpRequestBufferOptions>>(new HttpRequestBufferConfigureOptions(configuration));
93+
94+
return builder;
95+
}
96+
}
97+
#endif
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.Generic;
7+
using System.Diagnostics.CodeAnalysis;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Shared.DiagnosticIds;
10+
11+
namespace Microsoft.AspNetCore.Diagnostics.Logging;
12+
13+
/// <summary>
14+
/// The options for LoggerBuffer.
15+
/// </summary>
16+
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
17+
public class HttpRequestBufferOptions
18+
{
19+
/// <summary>
20+
/// Gets or sets the time to suspend the buffer after flushing.
21+
/// </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);
27+
28+
/// <summary>
29+
/// Gets or sets the size of the buffer for a request.
30+
/// </summary>
31+
public int PerRequestCapacity { get; set; } = 1_000;
32+
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+
38+
#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
39+
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
40+
/// <summary>
41+
/// Gets or sets the collection of <see cref="BufferFilterRule"/> used for filtering log messages for the purpose of further buffering.
42+
/// </summary>
43+
public List<BufferFilterRule> Rules { get; set; } = [];
44+
#pragma warning restore CA2227 // Collection properties should be read only
45+
#pragma warning restore CA1002 // Do not expose generic lists
46+
}
47+
#endif
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.Collections.Concurrent;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Microsoft.AspNetCore.Diagnostics.Logging;
11+
12+
internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider
13+
{
14+
private readonly GlobalBufferProvider _globalBufferProvider;
15+
private readonly IHttpContextAccessor _accessor;
16+
private readonly ConcurrentDictionary<string, HttpRequestBuffer> _requestBuffers = new();
17+
18+
public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor)
19+
{
20+
_globalBufferProvider = globalBufferProvider;
21+
_accessor = accessor;
22+
}
23+
24+
public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null
25+
? _globalBufferProvider.CurrentBuffer
26+
: _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService<HttpRequestBuffer>());
27+
28+
// TO DO: Dispose request buffer when the respective HttpContext is disposed
29+
}
30+
#endif
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Generic;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
10+
namespace Microsoft.AspNetCore.Diagnostics.Logging;
11+
12+
internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord
13+
{
14+
public HttpRequestBufferedLogRecord(
15+
LogLevel logLevel,
16+
EventId eventId,
17+
IReadOnlyList<KeyValuePair<string, object?>> state,
18+
Exception? exception,
19+
string? formatter)
20+
{
21+
LogLevel = logLevel;
22+
EventId = eventId;
23+
Attributes = state;
24+
Exception = exception?.ToString(); // wtf??
25+
FormattedMessage = formatter;
26+
}
27+
28+
public override IReadOnlyList<KeyValuePair<string, object?>> Attributes { get; }
29+
public override string? FormattedMessage { get; }
30+
public override string? Exception { get; }
31+
32+
public override DateTimeOffset Timestamp { get; }
33+
34+
public override LogLevel LogLevel { get; }
35+
36+
public override EventId EventId { get; }
37+
}
38+
#endif

src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
<InjectSharedPools>false</InjectSharedPools>
1818
<InjectSharedBufferWriterPool>true</InjectSharedBufferWriterPool>
1919
<InjectSharedNumericExtensions>false</InjectSharedNumericExtensions>
20-
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
2120
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
2221
</PropertyGroup>
2322

0 commit comments

Comments
 (0)