diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props
index a68b0e4298f..599fc2bde44 100644
--- a/eng/MSBuild/Shared.props
+++ b/eng/MSBuild/Shared.props
@@ -42,4 +42,8 @@
+
+
+
+
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
index 1d25077f8f0..64591cd910b 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
@@ -4,94 +4,111 @@
#if NET9_0_OR_GREATER
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Diagnostics;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Shared.Diagnostics;
+using static Microsoft.Extensions.Logging.ExtendedLogger;
namespace Microsoft.AspNetCore.Diagnostics.Logging;
internal sealed class HttpRequestBuffer : ILoggingBuffer
{
private readonly IOptionsMonitor _options;
- private readonly ConcurrentDictionary> _buffers;
+ private readonly IOptionsMonitor _globalOptions;
+ private readonly ConcurrentQueue _buffer;
private readonly TimeProvider _timeProvider = TimeProvider.System;
+ private readonly IBufferSink _bufferSink;
+ private readonly object _bufferCapacityLocker = new();
+ private DateTimeOffset _truncateAfter;
private DateTimeOffset _lastFlushTimestamp;
- public HttpRequestBuffer(IOptionsMonitor options)
+ public HttpRequestBuffer(IBufferSink bufferSink,
+ IOptionsMonitor options,
+ IOptionsMonitor globalOptions)
{
_options = options;
- _buffers = new ConcurrentDictionary>();
- _lastFlushTimestamp = _timeProvider.GetUtcNow();
- }
+ _globalOptions = globalOptions;
+ _bufferSink = bufferSink;
+ _buffer = new ConcurrentQueue();
- internal HttpRequestBuffer(IOptionsMonitor options, TimeProvider timeProvider)
- : this(options)
- {
- _timeProvider = timeProvider;
- _lastFlushTimestamp = _timeProvider.GetUtcNow();
+ _truncateAfter = _timeProvider.GetUtcNow();
}
- public bool TryEnqueue(
- IBufferedLogger logger,
+ [RequiresUnreferencedCode(
+ "Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList>, Exception, String)")]
+ public bool TryEnqueue(
LogLevel logLevel,
string category,
EventId eventId,
- IReadOnlyList> joiner,
+ TState attributes,
Exception? exception,
- string formatter)
+ Func formatter)
{
if (!IsEnabled(category, logLevel, eventId))
{
return false;
}
- var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter);
- var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue());
-
- // probably don't need to limit buffer capacity?
- // because buffer is disposed when the respective HttpContext is disposed
- // don't expect it to grow so much to cause a problem?
- if (queue.Count >= _options.CurrentValue.PerRequestCapacity)
+ switch (attributes)
{
- _ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
+ case ModernTagJoiner modernTagJoiner:
+ _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception,
+ ((Func)(object)formatter)(modernTagJoiner, exception)));
+ break;
+ case LegacyTagJoiner legacyTagJoiner:
+ _buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception,
+ ((Func)(object)formatter)(legacyTagJoiner, exception)));
+ break;
+ default:
+ Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}");
+ break;
}
- queue.Enqueue(record);
+ var now = _timeProvider.GetUtcNow();
+ lock (_bufferCapacityLocker)
+ {
+ if (now >= _truncateAfter)
+ {
+ _truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration);
+ TruncateOverlimit();
+ }
+ }
return true;
}
+ [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable)")]
public void Flush()
{
- foreach (var (logger, queue) in _buffers)
- {
- var result = new List();
- while (!queue.IsEmpty)
- {
- if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item))
- {
- result.Add(item);
- }
- }
-
- logger.LogRecords(result);
- }
+ var result = _buffer.ToArray();
+ _buffer.Clear();
_lastFlushTimestamp = _timeProvider.GetUtcNow();
+
+ _bufferSink.LogRecords(result);
}
public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
{
- if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration)
+ if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration)
{
return false;
}
- LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
+ LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
return rule is not null;
}
+
+ public void TruncateOverlimit()
+ {
+ // Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments.
+ while (_buffer.Count > _options.CurrentValue.PerRequestCapacity)
+ {
+ _ = _buffer.TryDequeue(out _);
+ }
+ }
}
#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
index 50faa1522e0..b3c4f94f786 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
@@ -9,6 +9,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Diagnostics.Buffering;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Shared.DiagnosticIds;
@@ -30,14 +31,16 @@ public static class HttpRequestBufferLoggerBuilderExtensions
/// The to add.
/// The value of .
/// is .
- public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configuration);
return builder
.AddHttpRequestBufferConfiguration(configuration)
- .AddHttpRequestBufferProvider();
+ .AddHttpRequestBufferManager()
+ .AddGlobalBufferConfiguration(configuration)
+ .AddGlobalBufferManager();
}
///
@@ -49,7 +52,7 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
/// The buffer configuration options.
/// The value of .
/// is .
- public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null)
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null)
{
_ = Throw.IfNull(builder);
@@ -57,7 +60,10 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
.Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null)))
.Configure(configure ?? new Action(_ => { }));
- return builder.AddHttpRequestBufferProvider();
+ return builder
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffer(level)
+ .AddGlobalBufferManager();
}
///
@@ -66,16 +72,20 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
/// The .
/// The so that additional calls can be chained.
/// is .
- public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder)
+ internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
{
_ = Throw.IfNull(builder);
- builder.Services.TryAddScoped();
- builder.Services.TryAddScoped(sp => sp.GetRequiredService());
builder.Services.TryAddSingleton();
- builder.Services.TryAddActivatedSingleton();
- return builder.AddGlobalBufferProvider();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService()));
+
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton(static sp => sp.GetRequiredService());
+ builder.Services.TryAddSingleton(static sp => sp.GetRequiredService());
+
+ return builder;
}
///
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
new file mode 100644
index 00000000000..16b51bf6528
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
@@ -0,0 +1,87 @@
+// 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.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Diagnostics.Logging;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Diagnostics.Buffering;
+internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager
+{
+ private readonly GlobalBufferManager _globalBufferManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IOptionsMonitor _requestOptions;
+ private readonly IOptionsMonitor _globalOptions;
+
+ public HttpRequestBufferManager(
+ GlobalBufferManager globalBufferManager,
+ IHttpContextAccessor httpContextAccessor,
+ IOptionsMonitor requestOptions,
+ IOptionsMonitor globalOptions)
+ {
+ _globalBufferManager = globalBufferManager;
+ _httpContextAccessor = httpContextAccessor;
+ _requestOptions = requestOptions;
+ _globalOptions = globalOptions;
+ }
+
+ public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category)
+ {
+ var httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext is null)
+ {
+ return _globalBufferManager.CreateBuffer(bufferSink, category);
+ }
+
+ if (!httpContext.Items.TryGetValue(category, out var buffer))
+ {
+ var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions, _globalOptions);
+ httpContext.Items[category] = httpRequestBuffer;
+ return httpRequestBuffer;
+ }
+
+ if (buffer is not ILoggingBuffer loggingBuffer)
+ {
+ throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}");
+ }
+
+ return loggingBuffer;
+ }
+
+ [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
+ public void Flush() => _globalBufferManager.Flush();
+
+ [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
+ public void FlushCurrentRequestLogs()
+ {
+ if (_httpContextAccessor.HttpContext is not null)
+ {
+ foreach (var kvp in _httpContextAccessor.HttpContext!.Items)
+ {
+ if (kvp.Value is ILoggingBuffer buffer)
+ {
+ buffer.Flush();
+ }
+ }
+ }
+ }
+
+ [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue(LogLevel, String, EventId, TState, Exception, Func)")]
+ public bool TryEnqueue(
+ IBufferSink bufferSink,
+ LogLevel logLevel,
+ string category,
+ EventId eventId,
+ TState attributes,
+ Exception? exception,
+ Func formatter)
+ {
+ var buffer = CreateBuffer(bufferSink, category);
+ return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter);
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
index cf04cd2da8c..cc266422026 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
@@ -17,24 +17,15 @@ namespace Microsoft.AspNetCore.Diagnostics.Logging;
public class HttpRequestBufferOptions
{
///
- /// Gets or sets the time to suspend the buffer after flushing.
+ /// Gets or sets the duration to check and remove the buffered items exceeding the .
///
- ///
- /// 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,
- /// so the buffering will be suspended for the time.
- ///
- public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30);
+ public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10);
///
/// Gets or sets the size of the buffer for a request.
///
public int PerRequestCapacity { get; set; } = 1_000;
- ///
- /// Gets or sets the size of the global buffer which applies to non-request logs only.
- ///
- public int GlobalCapacity { get; set; } = 1_000_000;
-
#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
///
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs
deleted file mode 100644
index 3ed5c08b1d7..00000000000
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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.Collections.Concurrent;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace Microsoft.AspNetCore.Diagnostics.Logging;
-
-internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider
-{
- private readonly GlobalBufferProvider _globalBufferProvider;
- private readonly IHttpContextAccessor _accessor;
- private readonly ConcurrentDictionary _requestBuffers = new();
-
- public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor)
- {
- _globalBufferProvider = globalBufferProvider;
- _accessor = accessor;
- }
-
- public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null
- ? _globalBufferProvider.CurrentBuffer
- : _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService());
-
- // TO DO: Dispose request buffer when the respective HttpContext is disposed
-}
-#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs
deleted file mode 100644
index 8983e97843b..00000000000
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.Generic;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-
-namespace Microsoft.AspNetCore.Diagnostics.Logging;
-
-internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord
-{
- public HttpRequestBufferedLogRecord(
- LogLevel logLevel,
- EventId eventId,
- IReadOnlyList> state,
- Exception? exception,
- string? formatter)
- {
- LogLevel = logLevel;
- EventId = eventId;
- Attributes = state;
- Exception = exception?.ToString(); // wtf??
- FormattedMessage = formatter;
- }
-
- public override IReadOnlyList> Attributes { get; }
- public override string? FormattedMessage { get; }
- public override string? Exception { get; }
-
- public override DateTimeOffset Timestamp { get; }
-
- public override LogLevel LogLevel { get; }
-
- public override EventId EventId { get; }
-}
-#endif
diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs
similarity index 53%
rename from src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs
rename to src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs
index 52742297d0b..d237cd9cf51 100644
--- a/src/Libraries/Microsoft.Extensions.Telemetry/Buffering/ILoggingBufferProvider.cs
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs
@@ -1,21 +1,21 @@
// 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.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
-namespace Microsoft.Extensions.Logging;
+namespace Microsoft.Extensions.Diagnostics.Buffering;
///
-/// Interface providing access to the current logging buffer.
+/// Interface for a global buffer manager.
///
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
-public interface ILoggingBufferProvider
+public interface IHttpRequestBufferManager : IBufferManager
{
///
- /// Gets current logging buffer.
+ /// Flushes the buffer and emits buffered logs for the current request.
///
- public ILoggingBuffer CurrentBuffer { get; }
+ [RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
+ public void FlushCurrentRequestLogs();
}
#endif
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
index 9ba5586dba6..d3fb76cec0b 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
@@ -8,9 +8,10 @@
using System.Globalization;
#if NET9_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
-
+using Microsoft.Shared.JsonExceptionConverter;
#endif
using Microsoft.Shared.Diagnostics;
@@ -118,19 +119,20 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
#if NET9_0_OR_GREATER
///
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+ [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
public void LogRecords(IEnumerable records)
{
_ = Throw.IfNull(records);
var l = new List