diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props
index dee583f7e39..6bf0fb6abc4 100644
--- a/eng/MSBuild/Shared.props
+++ b/eng/MSBuild/Shared.props
@@ -46,4 +46,8 @@
+
+
+
+
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
new file mode 100644
index 00000000000..35a1893e43b
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
@@ -0,0 +1,128 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.Extensions.Diagnostics.Buffering;
+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.Buffering;
+
+internal sealed class HttpRequestBuffer : ILoggingBuffer
+{
+ private readonly IOptionsMonitor _options;
+ private readonly IOptionsMonitor _globalOptions;
+ private readonly ConcurrentQueue _buffer;
+ private readonly TimeProvider _timeProvider = TimeProvider.System;
+ private readonly IBufferedLogger _bufferedLogger;
+
+ private DateTimeOffset _lastFlushTimestamp;
+ private int _bufferSize;
+
+ public HttpRequestBuffer(IBufferedLogger bufferedLogger,
+ IOptionsMonitor options,
+ IOptionsMonitor globalOptions)
+ {
+ _options = options;
+ _globalOptions = globalOptions;
+ _bufferedLogger = bufferedLogger;
+ _buffer = new ConcurrentQueue();
+ }
+
+ public bool TryEnqueue(
+ LogLevel logLevel,
+ string category,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ SerializedLogRecord serializedLogRecord = default;
+ if (state is ModernTagJoiner modernTagJoiner)
+ {
+ if (!IsEnabled(category, logLevel, eventId, modernTagJoiner))
+ {
+ return false;
+ }
+
+ serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception,
+ ((Func)(object)formatter)(modernTagJoiner, exception));
+ }
+ else if (state is LegacyTagJoiner legacyTagJoiner)
+ {
+ if (!IsEnabled(category, logLevel, eventId, legacyTagJoiner))
+ {
+ return false;
+ }
+
+ serializedLogRecord = new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception,
+ ((Func)(object)formatter)(legacyTagJoiner, exception));
+ }
+ else
+ {
+ Throw.ArgumentException(nameof(state), $"Unsupported type of the log state object detected: {typeof(TState)}");
+ }
+
+ if (serializedLogRecord.SizeInBytes > _globalOptions.CurrentValue.MaxLogRecordSizeInBytes)
+ {
+ return false;
+ }
+
+ _buffer.Enqueue(serializedLogRecord);
+ _ = Interlocked.Add(ref _bufferSize, serializedLogRecord.SizeInBytes);
+
+ Trim();
+
+ return true;
+ }
+
+ public void Flush()
+ {
+ _lastFlushTimestamp = _timeProvider.GetUtcNow();
+
+ SerializedLogRecord[] bufferedRecords = _buffer.ToArray();
+
+ _buffer.Clear();
+
+ var deserializedLogRecords = new List(bufferedRecords.Length);
+ foreach (var bufferedRecord in bufferedRecords)
+ {
+ deserializedLogRecords.Add(
+ new DeserializedLogRecord(
+ bufferedRecord.Timestamp,
+ bufferedRecord.LogLevel,
+ bufferedRecord.EventId,
+ bufferedRecord.Exception,
+ bufferedRecord.FormattedMessage,
+ bufferedRecord.Attributes));
+ }
+
+ _bufferedLogger.LogRecords(deserializedLogRecords);
+ }
+
+ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId, IReadOnlyList> attributes)
+ {
+ if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration)
+ {
+ return false;
+ }
+
+ BufferFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out BufferFilterRule? rule);
+
+ return rule is not null;
+ }
+
+ private void Trim()
+ {
+ while (_bufferSize > _options.CurrentValue.PerRequestBufferSizeInBytes && _buffer.TryDequeue(out var item))
+ {
+ _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes);
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs
new file mode 100644
index 00000000000..81cc67d4e22
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions
+{
+ private const string BufferingKey = "Buffering";
+ private readonly IConfiguration _configuration;
+
+ public HttpRequestBufferConfigureOptions(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public void Configure(HttpRequestBufferOptions options)
+ {
+ if (_configuration == null)
+ {
+ return;
+ }
+
+ var section = _configuration.GetSection(BufferingKey);
+ if (!section.Exists())
+ {
+ return;
+ }
+
+ var parsedOptions = section.Get();
+ if (parsedOptions is null)
+ {
+ return;
+ }
+
+ options.Rules.AddRange(parsedOptions.Rules);
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs
new file mode 100644
index 00000000000..4200b5e5494
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferHolder.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Diagnostics.Buffering;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class HttpRequestBufferHolder
+{
+ private readonly ConcurrentDictionary _buffers = new();
+
+ public ILoggingBuffer GetOrAdd(string category, Func valueFactory) =>
+ _buffers.GetOrAdd(category, valueFactory);
+
+ public void Flush()
+ {
+ foreach (var buffer in _buffers.Values)
+ {
+ buffer.Flush();
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
new file mode 100644
index 00000000000..8c53955220f
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
@@ -0,0 +1,100 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Diagnostics.Buffering;
+using Microsoft.AspNetCore.Http;
+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;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Logging;
+
+///
+/// Lets you register log buffers in a dependency injection container.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public static class HttpRequestBufferLoggerBuilderExtensions
+{
+ ///
+ /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in
+ /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>.
+ ///
+ /// The .
+ /// The to add.
+ /// The value of .
+ /// is .
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configuration);
+
+ return builder
+ .AddHttpRequestBufferConfiguration(configuration)
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffer(configuration);
+ }
+
+ ///
+ /// Adds HTTP request-aware buffering to the logging infrastructure. Matched logs will be buffered in
+ /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>.
+ ///
+ /// The .
+ /// The log level (and below) to apply the buffer to.
+ /// The buffer configuration options.
+ /// The value of .
+ /// is .
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action? configure = null)
+ {
+ _ = Throw.IfNull(builder);
+
+ _ = builder.Services
+ .Configure(options => options.Rules.Add(new BufferFilterRule(null, level, null, null)))
+ .Configure(configure ?? new Action(_ => { }));
+
+ return builder
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffer(level);
+ }
+
+ ///
+ /// Adds HTTP request buffer provider to the logging infrastructure.
+ ///
+ /// The .
+ /// The so that additional calls can be chained.
+ /// is .
+ internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
+ {
+ _ = Throw.IfNull(builder);
+
+ builder.Services.TryAddScoped();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton(static sp => sp.GetRequiredService());
+ builder.Services.TryAddSingleton(static sp => sp.GetRequiredService());
+
+ return builder;
+ }
+
+ ///
+ /// Configures from an instance of .
+ ///
+ /// The .
+ /// The to add.
+ /// The value of .
+ /// is .
+ internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration)
+ {
+ _ = Throw.IfNull(builder);
+
+ _ = builder.Services.AddSingleton>(new HttpRequestBufferConfigureOptions(configuration));
+
+ 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..e0c48bf691a
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager
+{
+ private readonly IGlobalBufferManager _globalBufferManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IOptionsMonitor _requestOptions;
+ private readonly IOptionsMonitor _globalOptions;
+
+ public HttpRequestBufferManager(
+ IGlobalBufferManager globalBufferManager,
+ IHttpContextAccessor httpContextAccessor,
+ IOptionsMonitor requestOptions,
+ IOptionsMonitor globalOptions)
+ {
+ _globalBufferManager = globalBufferManager;
+ _httpContextAccessor = httpContextAccessor;
+ _requestOptions = requestOptions;
+ _globalOptions = globalOptions;
+ }
+
+ public void FlushNonRequestLogs() => _globalBufferManager.Flush();
+
+ public void FlushCurrentRequestLogs()
+ {
+ _httpContextAccessor.HttpContext?.RequestServices.GetService()?.Flush();
+ }
+
+ public bool TryEnqueue(
+ IBufferedLogger bufferedLogger,
+ LogLevel logLevel,
+ string category,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ HttpContext? httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext is null)
+ {
+ return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter);
+ }
+
+ HttpRequestBufferHolder? bufferHolder = httpContext.RequestServices.GetService();
+ ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!);
+
+ if (buffer is null)
+ {
+ return _globalBufferManager.TryEnqueue(bufferedLogger, logLevel, category, eventId, state, exception, formatter);
+ }
+
+ return buffer.TryEnqueue(logLevel, category, eventId, state, exception, formatter);
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
new file mode 100644
index 00000000000..e97dc38f260
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+///
+/// The options for LoggerBuffer.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public class HttpRequestBufferOptions
+{
+ ///
+ /// Gets or sets the size in bytes of the buffer for a request. If the buffer size exceeds this limit, the oldest buffered log records will be dropped.
+ ///
+ /// TO DO: add validation.
+ public int PerRequestBufferSizeInBytes { get; set; } = 5_000_000;
+
+#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
+ ///
+ /// Gets the collection of used for filtering log messages for the purpose of further buffering.
+ ///
+ public List Rules { get; } = [];
+#pragma warning restore CA1002 // Do not expose generic lists
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs
new file mode 100644
index 00000000000..c6951b5042a
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IHttpRequestBufferManager.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+///
+/// Interface for an HTTP request buffer manager.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public interface IHttpRequestBufferManager : IBufferManager
+{
+ ///
+ /// Flushes the buffer and emits non-request logs.
+ ///
+ void FlushNonRequestLogs();
+
+ ///
+ /// Flushes the buffer and emits buffered logs for the current request.
+ ///
+ void FlushCurrentRequestLogs();
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
index 5484aa8f5af..0c42f2575ec 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
@@ -8,17 +8,17 @@
$(NetCoreTargetFrameworks)
+ true
true
true
- false
- false
+ true
true
false
false
true
false
- true
- true
+ false
+ false
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
index 56973c9e78d..19d91bb8e27 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
@@ -5,7 +5,10 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.Logging.Testing;
@@ -17,7 +20,7 @@ namespace Microsoft.Extensions.Logging.Testing;
/// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it
/// to validate that your code is logging what it should.
///
-public class FakeLogger : ILogger
+public class FakeLogger : ILogger, IBufferedLogger
{
private readonly ConcurrentDictionary _disabledLevels = new(); // used as a set, the value is ignored
@@ -105,6 +108,25 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
///
public string? Category { get; }
+ ///
+ [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+ public void LogRecords(IEnumerable records)
+ {
+ _ = Throw.IfNull(records);
+
+ var l = new List
normal
@@ -31,9 +36,11 @@
-
+
+
+
diff --git a/src/Shared/LoggingBuffering/DeserializedLogRecord.cs b/src/Shared/LoggingBuffering/DeserializedLogRecord.cs
new file mode 100644
index 00000000000..02a5cf1712d
--- /dev/null
+++ b/src/Shared/LoggingBuffering/DeserializedLogRecord.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if !SHARED_PROJECT || NET9_0_OR_GREATER
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.Extensions.Diagnostics.Buffering;
+
+///
+/// Represents a log record deserialized from somewhere, such as buffer.
+///
+internal sealed class DeserializedLogRecord : BufferedLogRecord
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The time when the log record was first created.
+ /// Logging severity level.
+ /// Event ID.
+ /// An exception string for this record.
+ /// The formatted log message.
+ /// The set of name/value pairs associated with the record.
+ public DeserializedLogRecord(
+ DateTimeOffset timestamp,
+ LogLevel logLevel,
+ EventId eventId,
+ string? exception,
+ string? formattedMessage,
+ IReadOnlyList> attributes)
+ {
+ _timestamp = timestamp;
+ _logLevel = logLevel;
+ _eventId = eventId;
+ _exception = exception;
+ _formattedMessage = formattedMessage;
+ _attributes = attributes;
+ }
+
+ ///
+ public override DateTimeOffset Timestamp => _timestamp;
+ private DateTimeOffset _timestamp;
+
+ ///
+ public override LogLevel LogLevel => _logLevel;
+ private LogLevel _logLevel;
+
+ ///
+ public override EventId EventId => _eventId;
+ private EventId _eventId;
+
+ ///
+ public override string? Exception => _exception;
+ private string? _exception;
+
+ ///
+ public override string? FormattedMessage => _formattedMessage;
+ private string? _formattedMessage;
+
+ ///
+ public override IReadOnlyList> Attributes => _attributes;
+ private IReadOnlyList> _attributes;
+}
+#endif
diff --git a/src/Shared/LoggingBuffering/ILoggingBuffer.cs b/src/Shared/LoggingBuffering/ILoggingBuffer.cs
new file mode 100644
index 00000000000..8cc1b1f0d90
--- /dev/null
+++ b/src/Shared/LoggingBuffering/ILoggingBuffer.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Diagnostics.Buffering;
+
+///
+/// Interface for a logging buffer.
+///
+internal interface ILoggingBuffer
+{
+ ///
+ /// Enqueues a log record in the underlying buffer.
+ ///
+ /// Log level.
+ /// Category.
+ /// Event ID.
+ /// Log state attributes.
+ /// Exception.
+ /// Formatter delegate.
+ /// Type of the instance.
+ /// if the log record was buffered; otherwise, .
+ bool TryEnqueue(
+ LogLevel logLevel,
+ string category,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter);
+
+ ///
+ /// Flushes the buffer.
+ ///
+ void Flush();
+}
diff --git a/src/Shared/LoggingBuffering/SerializedLogRecord.cs b/src/Shared/LoggingBuffering/SerializedLogRecord.cs
new file mode 100644
index 00000000000..561220e2094
--- /dev/null
+++ b/src/Shared/LoggingBuffering/SerializedLogRecord.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Diagnostics.Buffering;
+
+///
+/// Represents a log record that has been serialized for purposes of buffering or similar.
+///
+#pragma warning disable CA1815 // Override equals and operator equals on value types - not used for this struct, would be dead code
+internal readonly struct SerializedLogRecord
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Logging severity level.
+ /// Event ID.
+ /// The time when the log record was first created.
+ /// The set of name/value pairs associated with the record.
+ /// An exception string for this record.
+ /// The formatted log message.
+ public SerializedLogRecord(
+ LogLevel logLevel,
+ EventId eventId,
+ DateTimeOffset timestamp,
+ IReadOnlyList> attributes,
+ Exception? exception,
+ string formattedMessage)
+ {
+ LogLevel = logLevel;
+ EventId = eventId;
+ Timestamp = timestamp;
+
+ List> serializedAttributes = [];
+ if (attributes is not null)
+ {
+ serializedAttributes = new List>(attributes.Count);
+ for (int i = 0; i < attributes.Count; i++)
+ {
+ string key = attributes[i].Key;
+ string value = attributes[i].Value?.ToString() ?? string.Empty;
+ serializedAttributes.Add(new KeyValuePair(key, value));
+
+ SizeInBytes += key.Length * sizeof(char);
+ SizeInBytes += value.Length * sizeof(char);
+ }
+ }
+
+ Attributes = serializedAttributes;
+
+ Exception = exception?.Message;
+ if (Exception is not null)
+ {
+ SizeInBytes += Exception.Length * sizeof(char);
+ }
+
+ FormattedMessage = formattedMessage;
+ if (FormattedMessage is not null)
+ {
+ SizeInBytes += FormattedMessage.Length * sizeof(char);
+ }
+ }
+
+ ///
+ /// Gets the variable set of name/value pairs associated with the record.
+ ///
+ public IReadOnlyList> Attributes { get; }
+
+ ///
+ /// Gets the formatted log message.
+ ///
+ public string? FormattedMessage { get; }
+
+ ///
+ /// Gets an exception string for this record.
+ ///
+ public string? Exception { get; }
+
+ ///
+ /// Gets the time when the log record was first created.
+ ///
+ public DateTimeOffset Timestamp { get; }
+
+ ///
+ /// Gets the record's logging severity level.
+ ///
+ public LogLevel LogLevel { get; }
+
+ ///
+ /// Gets the record's event ID.
+ ///
+ public EventId EventId { get; }
+
+ ///
+ /// Gets the approximate size of the serialized log record in bytes.
+ ///
+ public int SizeInBytes { get; init; }
+}
diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj
index 439c3788557..d25c011a05f 100644
--- a/src/Shared/Shared.csproj
+++ b/src/Shared/Shared.csproj
@@ -29,6 +29,7 @@
+
diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs
new file mode 100644
index 00000000000..929b97b97b2
--- /dev/null
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering.Test;
+
+public class HttpRequestBufferLoggerBuilderExtensionsTests
+{
+ [Fact]
+ public void AddHttpRequestBuffering_RegistersInDI()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder =>
+ {
+ builder.AddHttpRequestBuffering(LogLevel.Warning);
+ });
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var buffer = serviceProvider.GetService();
+
+ Assert.NotNull(buffer);
+ Assert.IsAssignableFrom(buffer);
+ }
+
+ [Fact]
+ public void WhenArgumentNull_Throws()
+ {
+ var builder = null as ILoggingBuilder;
+ var configuration = null as IConfiguration;
+
+ Assert.Throws(() => builder!.AddHttpRequestBuffering(LogLevel.Warning));
+ Assert.Throws(() => builder!.AddHttpRequestBuffering(configuration!));
+ }
+
+ [Fact]
+ public void AddHttpRequestBufferConfiguration_RegistersInDI()
+ {
+ List expectedData =
+ [
+ new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, null),
+ new BufferFilterRule(null, LogLevel.Information, null, null),
+ ];
+ ConfigurationBuilder configBuilder = new ConfigurationBuilder();
+ configBuilder.AddJsonFile("appsettings.json");
+ IConfigurationRoot configuration = configBuilder.Build();
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder =>
+ {
+ builder.AddHttpRequestBufferConfiguration(configuration);
+ });
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var options = serviceProvider.GetService>();
+ Assert.NotNull(options);
+ Assert.NotNull(options.CurrentValue);
+ Assert.Equivalent(expectedData, options.CurrentValue.Rules);
+ }
+}
diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs
index 5794709560f..bb9aebec280 100644
--- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/AcceptanceTests.cs
@@ -11,6 +11,7 @@
using System.Net.Mime;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.Buffering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpLogging;
@@ -55,6 +56,30 @@ public static void Configure(IApplicationBuilder app)
app.UseRouting();
app.UseHttpLogging();
+ app.Map("/flushrequestlogs", static x =>
+ x.Run(static async context =>
+ {
+ await context.Request.Body.DrainAsync(default);
+
+ // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor
+ var bufferManager = context.RequestServices.GetService();
+ bufferManager?.FlushCurrentRequestLogs();
+ }));
+
+ app.Map("/flushalllogs", static x =>
+ x.Run(static async context =>
+ {
+ await context.Request.Body.DrainAsync(default);
+
+ // normally, this would be a Middleware and IHttpRequestBufferManager would be injected via constructor
+ var bufferManager = context.RequestServices.GetService();
+ if (bufferManager is not null)
+ {
+ bufferManager.FlushCurrentRequestLogs();
+ bufferManager.FlushNonRequestLogs();
+ }
+ }));
+
app.Map("/error", static x =>
x.Run(static async context =>
{
@@ -714,6 +739,50 @@ await RunAsync(
});
}
+ [Fact]
+ public async Task HttpRequestBuffering()
+ {
+ await RunAsync(
+ LogLevel.Trace,
+ services => services
+ .AddLogging(builder =>
+ {
+ // enable Microsoft.AspNetCore.Routing.Matching.DfaMatcher debug logs
+ // which are produced by ASP.NET Core within HTTP context.
+ // This is what is going to be buffered and tested.
+ builder.AddFilter("Microsoft.AspNetCore.Routing.Matching.DfaMatcher", LogLevel.Debug);
+
+ // Disable HTTP logging middleware, otherwise even though they are not buffered,
+ // they will be logged as usual and contaminate test results:
+ builder.AddFilter("Microsoft.AspNetCore.HttpLogging", LogLevel.None);
+
+ builder.AddHttpRequestBuffering(LogLevel.Debug);
+ }),
+ async (logCollector, client, sp) =>
+ {
+ // just HTTP request logs:
+ using var response = await client.GetAsync("/flushrequestlogs").ConfigureAwait(false);
+ Assert.True(response.IsSuccessStatusCode);
+ await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout);
+ Assert.Equal(1, logCollector.Count);
+ Assert.Equal(LogLevel.Debug, logCollector.LatestRecord.Level);
+ Assert.Equal("Microsoft.AspNetCore.Routing.Matching.DfaMatcher", logCollector.LatestRecord.Category);
+
+ // HTTP request logs + global logs:
+ using var loggerFactory = sp.GetRequiredService();
+ var logger = loggerFactory.CreateLogger("test");
+ logger.LogTrace("This is a log message");
+ using var response2 = await client.GetAsync("/flushalllogs").ConfigureAwait(false);
+ Assert.True(response2.IsSuccessStatusCode);
+ await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout);
+
+ // 1 and 2 records are from DfaMatcher, and 3rd is from our test category
+ Assert.Equal(3, logCollector.Count);
+ Assert.Equal(LogLevel.Trace, logCollector.LatestRecord.Level);
+ Assert.Equal("test", logCollector.LatestRecord.Category);
+ });
+ }
+
[Fact]
public async Task HttpLogging_LogRecordIsNotCreated_If_Disabled()
{
diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs
index 481a186a08d..82a063cc55c 100644
--- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Logging/HeaderNormalizerTests.cs
@@ -5,6 +5,8 @@
using Microsoft.Extensions.Compliance.Classification;
using Xunit;
+namespace Microsoft.AspNetCore.Diagnostics.Logging.Test;
+
public class HeaderNormalizerTests
{
[Fact]
diff --git a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json
index c1503ee98da..79676b0e1e9 100644
--- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json
@@ -17,5 +17,17 @@
"userId": "EUII",
"userContent": "CustomerContent"
}
+ },
+ "Buffering": {
+ "Rules": [
+ {
+ "Category": "Program.MyLogger",
+ "LogLevel": "Information",
+ "EventId": 1
+ },
+ {
+ "LogLevel": "Information"
+ }
+ ]
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj
index dc7703a8eb0..bc7533bf049 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj
@@ -8,6 +8,7 @@
$(NoWarn);CA1063;CA1861;SA1130;VSTHRD003
true
+ true
diff --git a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj
index 5db789e3b6b..1b485b302ca 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.Ollama.Tests/Microsoft.Extensions.AI.Ollama.Tests.csproj
@@ -5,6 +5,7 @@
+ true
true
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
index 66412bfeace..eae3a41d364 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
@@ -6,6 +6,7 @@
+ true
true
true
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj
index 32589c430e0..4a1753b2350 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Microsoft.Extensions.AI.Tests.csproj
@@ -5,6 +5,7 @@
+ true
$(NoWarn);CA1063;CA1861;SA1130;VSTHRD003
true
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj
index 5f6ca415e12..6f9488a33d8 100644
--- a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/Microsoft.Extensions.Diagnostics.Probes.Tests.csproj
@@ -2,6 +2,7 @@
Microsoft.Extensions.Diagnostics.Probes.Test
Unit tests for Microsoft.Extensions.Diagnostics.Probes
+ true
diff --git a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj
index 4bc20735577..cf7192a9df5 100644
--- a/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Http.Diagnostics.Tests/Microsoft.Extensions.Http.Diagnostics.Tests.csproj
@@ -2,6 +2,7 @@
Microsoft.Extensions.Http.Diagnostics.Test
Unit tests for Microsoft.Extensions.Http.Diagnostics.
+ true
diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj
index d440b8820db..a2f9e84a160 100644
--- a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj
@@ -2,6 +2,7 @@
Microsoft.Extensions.Options.Contextual.Test
Unit tests for Microsoft.Extensions.Options.Contextual
+ true
diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj
index 163b01082d1..02ad31cde93 100644
--- a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj
@@ -2,6 +2,7 @@
Microsoft.Extensions.Resilience.Test
Unit tests for Microsoft.Extensions.Resilience
+ true
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs
new file mode 100644
index 00000000000..9a02d8f254c
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/GlobalBufferLoggerBuilderExtensionsTests.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Test;
+
+public class GlobalBufferLoggerBuilderExtensionsTests
+{
+ [Fact]
+ public void AddGlobalBuffer_RegistersInDI()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder =>
+ {
+ builder.AddGlobalBuffer(LogLevel.Warning);
+ });
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var bufferManager = serviceProvider.GetService();
+
+ Assert.NotNull(bufferManager);
+ Assert.IsAssignableFrom(bufferManager);
+ }
+
+ [Fact]
+ public void WhenArgumentNull_Throws()
+ {
+ var builder = null as ILoggingBuilder;
+ var configuration = null as IConfiguration;
+
+ Assert.Throws(() => builder!.AddGlobalBuffer(LogLevel.Warning));
+ Assert.Throws(() => builder!.AddGlobalBuffer(configuration!));
+ }
+
+ [Fact]
+ public void AddGlobalBuffer_WithConfiguration_RegistersInDI()
+ {
+ List expectedData =
+ [
+ new BufferFilterRule("Program.MyLogger", LogLevel.Information, 1, [new("region", "westus2"), new ("priority", 1)]),
+ new BufferFilterRule(null, LogLevel.Information, null, null),
+ ];
+ ConfigurationBuilder configBuilder = new ConfigurationBuilder();
+ configBuilder.AddJsonFile("appsettings.json");
+ IConfigurationRoot configuration = configBuilder.Build();
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder =>
+ {
+ builder.AddGlobalBuffer(configuration);
+ builder.Services.Configure(options =>
+ {
+ options.MaxLogRecordSizeInBytes = 33;
+ });
+ });
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var options = serviceProvider.GetService>();
+ Assert.NotNull(options);
+ Assert.NotNull(options.CurrentValue);
+ Assert.Equal(33, options.CurrentValue.MaxLogRecordSizeInBytes); // value comes from the Configure() call
+ Assert.Equal(1000, options.CurrentValue.BufferSizeInBytes); // value comes from appsettings.json
+ Assert.Equal(TimeSpan.FromSeconds(30), options.CurrentValue.SuspendAfterFlushDuration); // value comes from default
+ Assert.Equivalent(expectedData, options.CurrentValue.Rules);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs
new file mode 100644
index 00000000000..041408f7bd1
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Buffering/LoggerFilterRuleSelectorTests.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Test;
+public class LoggerFilterRuleSelectorTests
+{
+ [Fact]
+ public void SelectsRightRule()
+ {
+ // Arrange
+ var rules = new List
+ {
+ new BufferFilterRule(null, null, null, null),
+ new BufferFilterRule(null, null, 1, null),
+ new BufferFilterRule(null, LogLevel.Information, 1, null),
+ new BufferFilterRule(null, LogLevel.Information, 1, null),
+ new BufferFilterRule(null, LogLevel.Warning, null, null),
+ new BufferFilterRule(null, LogLevel.Warning, 2, null),
+ new BufferFilterRule(null, LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region2", "westus2")]), // inapplicable key
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus3")]), // inapplicable value
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule - [11]
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 2, null),
+ new BufferFilterRule("Program.MyLogger", null, 1, null),
+ new BufferFilterRule(null, LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, null, null),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Error, 1, null),
+ };
+
+ // Act
+ BufferFilterRuleSelector.Select(
+ rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult);
+
+ // Assert
+ Assert.Same(rules[11], actualResult);
+ }
+
+ [Fact]
+ public void WhenManyRuleApply_SelectsLast()
+ {
+ // Arrange
+ var rules = new List
+ {
+ new BufferFilterRule(null, LogLevel.Information, 1, null),
+ new BufferFilterRule(null, LogLevel.Information, 1, null),
+ new BufferFilterRule(null, LogLevel.Warning, null, null),
+ new BufferFilterRule(null, LogLevel.Warning, 2, null),
+ new BufferFilterRule(null, LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")]), // the best rule
+ new BufferFilterRule("Program.MyLogger*", LogLevel.Warning, 1, [new("region", "westus2")]), // same as the best, but last and should be selected
+ };
+
+ // Act
+ BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("region", "westus2")], out var actualResult);
+
+ // Assert
+ Assert.Same(rules.Last(), actualResult);
+ }
+
+ [Fact]
+ public void CanWorkWithValueTypeAttributes()
+ {
+ // Arrange
+ var rules = new List
+ {
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 1)]),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 2)]), // the best rule
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, [new("priority", 3)]),
+ new BufferFilterRule("Program.MyLogger", LogLevel.Warning, 1, null),
+ };
+
+ // Act
+ BufferFilterRuleSelector.Select(rules, "Program.MyLogger", LogLevel.Warning, 1, [new("priority", "2")], out var actualResult);
+
+ // Assert
+ Assert.Same(rules[1], actualResult);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs
index 69d1d0e8f57..fa2dafbe8ce 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/ExtendedLoggerTests.cs
@@ -6,6 +6,7 @@
using System.Linq;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
using Microsoft.Extensions.Diagnostics.Enrichment;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
@@ -119,6 +120,34 @@ public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment
}
}
+ [Fact]
+ public static void GlobalBuffering_CanonicalUsecase()
+ {
+ using var provider = new Provider();
+ using var factory = Utils.CreateLoggerFactory(
+ builder =>
+ {
+ builder.AddProvider(provider);
+ builder.AddGlobalBuffer(LogLevel.Warning);
+ });
+
+ var logger = factory.CreateLogger("my category");
+ logger.LogWarning("MSG0");
+ logger.Log(LogLevel.Warning, new EventId(2, "ID2"), "some state", null, (_, _) => "MSG2");
+
+ // nothing is logged because the buffer not flushed yet
+ Assert.Equal(0, provider.Logger!.Collector.Count);
+
+ // instead of this, users would get IBufferManager from DI and call Flush on it
+ var dlf = (Utils.DisposingLoggerFactory)factory;
+ var bufferManager = dlf.ServiceProvider.GetRequiredService();
+
+ bufferManager.Flush();
+
+ // 2 log records emitted because the buffer has been flushed
+ Assert.Equal(2, provider.Logger!.Collector.Count);
+ }
+
[Theory]
[CombinatorialData]
public static void BagAndJoiner(bool objectVersion)
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs
index bf09f8cb91c..cb6a39a29e8 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Utils.cs
@@ -24,21 +24,21 @@ public static ILoggerFactory CreateLoggerFactory(Action? config
return new DisposingLoggerFactory(loggerFactory, serviceProvider);
}
- private sealed class DisposingLoggerFactory : ILoggerFactory
+ internal sealed class DisposingLoggerFactory : ILoggerFactory
{
private readonly ILoggerFactory _loggerFactory;
- private readonly ServiceProvider _serviceProvider;
+ internal readonly ServiceProvider ServiceProvider;
public DisposingLoggerFactory(ILoggerFactory loggerFactory, ServiceProvider serviceProvider)
{
_loggerFactory = loggerFactory;
- _serviceProvider = serviceProvider;
+ ServiceProvider = serviceProvider;
}
public void Dispose()
{
- _serviceProvider.Dispose();
+ ServiceProvider.Dispose();
}
public ILogger CreateLogger(string categoryName)
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj
index 7273b05c6c7..522fe8a9991 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj
@@ -5,6 +5,7 @@
+ true
false
false
false
diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json
index 16ea15c7ed8..d70df4596ba 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json
@@ -16,5 +16,29 @@
"MeterStateOverrides": {
"": "Disabled"
}
+ },
+ "Buffering": {
+ "MaxLogRecordSizeInBytes": 100,
+ "BufferSizeInBytes": 1000,
+ "Rules": [
+ {
+ "Category": "Program.MyLogger",
+ "LogLevel": "Information",
+ "EventId": 1,
+ "Attributes": [
+ {
+ "key": "region",
+ "value": "westus2"
+ },
+ {
+ "key": "priority",
+ "value": 1
+ }
+ ]
+ },
+ {
+ "LogLevel": "Information"
+ }
+ ]
}
}