diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props
index 13719d54959..7cb15292288 100644
--- a/eng/MSBuild/Shared.props
+++ b/eng/MSBuild/Shared.props
@@ -50,4 +50,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..40c84f6850c
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
@@ -0,0 +1,122 @@
+// 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(LogEntry logEntry)
+ {
+ SerializedLogRecord serializedLogRecord = default;
+ if (logEntry.State is ModernTagJoiner modernTagJoiner)
+ {
+ if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, modernTagJoiner))
+ {
+ return false;
+ }
+
+ serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), modernTagJoiner, logEntry.Exception,
+ ((Func)(object)logEntry.Formatter)(modernTagJoiner, logEntry.Exception));
+ }
+ else if (logEntry.State is LegacyTagJoiner legacyTagJoiner)
+ {
+ if (!IsEnabled(logEntry.Category, logEntry.LogLevel, logEntry.EventId, legacyTagJoiner))
+ {
+ return false;
+ }
+
+ serializedLogRecord = new SerializedLogRecord(logEntry.LogLevel, logEntry.EventId, _timeProvider.GetUtcNow(), legacyTagJoiner, logEntry.Exception,
+ ((Func)(object)logEntry.Formatter)(legacyTagJoiner, logEntry.Exception));
+ }
+ else
+ {
+ Throw.InvalidOperationException($"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;
+ }
+
+ LogBufferingFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, attributes, out LogBufferingFilterRule? rule);
+
+ return rule is not null;
+ }
+
+ private void Trim()
+ {
+ while (_bufferSize > _options.CurrentValue.MaxPerRequestBufferSizeInBytes && _buffer.TryDequeue(out var item))
+ {
+ _ = Interlocked.Add(ref _bufferSize, -item.SizeInBytes);
+ }
+ }
+}
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/HttpRequestBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
new file mode 100644
index 00000000000..bc9a469cbb9
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferManager.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class HttpRequestBufferManager : HttpRequestLogBuffer
+{
+ private readonly LogBuffer _globalBuffer;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IOptionsMonitor _requestOptions;
+ private readonly IOptionsMonitor _globalOptions;
+
+ public HttpRequestBufferManager(
+ LogBuffer globalBuffer,
+ IHttpContextAccessor httpContextAccessor,
+ IOptionsMonitor requestOptions,
+ IOptionsMonitor globalOptions)
+ {
+ _globalBuffer = globalBuffer;
+ _httpContextAccessor = httpContextAccessor;
+ _requestOptions = requestOptions;
+ _globalOptions = globalOptions;
+ }
+
+ public override void Flush() => _globalBuffer.Flush();
+
+ public override void FlushCurrentRequestLogs()
+ {
+ _httpContextAccessor.HttpContext?.RequestServices.GetService()?.Flush();
+ }
+
+ public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry)
+ {
+ HttpContext? httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext is null)
+ {
+ return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
+ }
+
+ HttpRequestBufferHolder? bufferHolder = httpContext.RequestServices.GetService();
+ ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(logEntry.Category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!);
+
+ if (buffer is null)
+ {
+ return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
+ }
+
+ return buffer.TryEnqueue(logEntry);
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs
new file mode 100644
index 00000000000..8c61553e78c
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferingLoggingBuilderExtensions.cs
@@ -0,0 +1,107 @@
+// 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 HTTP request log buffering in a dependency injection container.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public static class HttpRequestBufferingLoggingBuilderExtensions
+{
+ ///
+ /// Adds HTTP request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The to add.
+ /// The value of .
+ /// is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configuration);
+
+ _ = builder.Services.AddSingleton>(new HttpRequestLogBufferingConfigureOptions(configuration));
+
+ return builder
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffering(configuration);
+ }
+
+ ///
+ /// Adds HTTP request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The buffer configuration delegate.
+ /// The value of .
+ /// is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, Action configure)
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configure);
+
+ _ = builder.Services.Configure(configure);
+
+ HttpRequestLogBufferingOptions options = new HttpRequestLogBufferingOptions();
+ configure(options);
+
+ return builder
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffering(opts => opts.Rules = options.Rules);
+ }
+
+ ///
+ /// Adds HTTP request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The log level (and below) to apply the buffer to.
+ /// The value of .
+ /// is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? logLevel = null)
+ {
+ _ = Throw.IfNull(builder);
+
+ _ = builder.Services.Configure(options => options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel)));
+
+ return builder
+ .AddHttpRequestBufferManager()
+ .AddGlobalBuffering(logLevel);
+ }
+
+ internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
+ {
+ builder.Services.TryAddScoped();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton(sp =>
+ {
+ var globalBufferManager = sp.GetRequiredService();
+ return ActivatorUtilities.CreateInstance(sp, globalBufferManager);
+ });
+ builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
+ builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
+
+ return builder;
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs
new file mode 100644
index 00000000000..6fca3fa5b82
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBuffer.cs
@@ -0,0 +1,20 @@
+// 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;
+
+///
+/// Buffers HTTP request logs into circular buffers and drops them after some time if not flushed.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public abstract class HttpRequestLogBuffer : LogBuffer
+{
+ ///
+ /// Flushes buffers and emits buffered logs for the current HTTP request.
+ ///
+ public abstract void FlushCurrentRequestLogs();
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs
new file mode 100644
index 00000000000..004fc1d89d5
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingConfigureOptions.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class HttpRequestLogBufferingConfigureOptions : IConfigureOptions
+{
+ private const string BufferingKey = "Buffering";
+ private readonly IConfiguration _configuration;
+
+ public HttpRequestLogBufferingConfigureOptions(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public void Configure(HttpRequestLogBufferingOptions options)
+ {
+ if (_configuration == null)
+ {
+ return;
+ }
+
+ var section = _configuration.GetSection(BufferingKey);
+ if (!section.Exists())
+ {
+ return;
+ }
+
+ var parsedOptions = section.Get();
+ if (parsedOptions is null)
+ {
+ return;
+ }
+
+ foreach (var rule in parsedOptions.Rules)
+ {
+ options.Rules.Add(rule);
+ }
+ }
+}
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.cs
new file mode 100644
index 00000000000..4b42320a4af
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestLogBufferingOptions.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 HTTP request log buffering.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public class HttpRequestLogBufferingOptions
+{
+ ///
+ /// 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 MaxPerRequestBufferSizeInBytes { get; set; } = 5_000_000;
+
+#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
+ ///
+ /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering.
+ ///
+ public IList Rules { get; set; } = [];
+#pragma warning restore CA2227
+}
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..0ff1fb2c872
--- /dev/null
+++ b/src/Shared/LoggingBuffering/ILoggingBuffer.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 Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.Extensions.Diagnostics.Buffering;
+
+///
+/// Interface for a logging buffer.
+///
+internal interface ILoggingBuffer
+{
+ ///
+ /// Enqueues a log record in the underlying buffer.
+ ///
+ /// Type of the log state in the instance.
+ /// if the log record was buffered; otherwise, .
+ bool TryEnqueue(LogEntry logEntry);
+
+ ///
+ /// 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..52cd5132af9
--- /dev/null
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/Buffering/HttpRequestBufferLoggerBuilderExtensionsTests.cs
@@ -0,0 +1,62 @@
+// 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 LogBufferingFilterRule(categoryName: "Program.MyLogger", logLevel: LogLevel.Information, eventId: 1, eventName: "number one"),
+ new LogBufferingFilterRule(logLevel: LogLevel.Information),
+ ];
+ ConfigurationBuilder configBuilder = new ConfigurationBuilder();
+ configBuilder.AddJsonFile("appsettings.json");
+ IConfigurationRoot configuration = configBuilder.Build();
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(b => b.AddHttpRequestBuffering(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 869b0716e17..abbf4fc5752 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 HttpRequestLogBuffer 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 HttpRequestLogBuffer would be injected via constructor
+ var bufferManager = context.RequestServices.GetService();
+ if (bufferManager is not null)
+ {
+ bufferManager.FlushCurrentRequestLogs();
+ bufferManager.Flush();
+ }
+ }));
+
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..9597f27c636 100644
--- a/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json
+++ b/test/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware.Tests/appsettings.json
@@ -17,5 +17,18 @@
"userId": "EUII",
"userContent": "CustomerContent"
}
+ },
+ "Buffering": {
+ "Rules": [
+ {
+ "CategoryName": "Program.MyLogger",
+ "LogLevel": "Information",
+ "EventId": 1,
+ "EventName": "number one"
+ },
+ {
+ "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 cf9f4d9703d..9050d16663e 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..76d62e496f9
--- /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.AddGlobalBuffering(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!.AddGlobalBuffering(LogLevel.Warning));
+ Assert.Throws(() => builder!.AddGlobalBuffering(configuration!));
+ }
+
+ [Fact]
+ public void AddGlobalBuffer_WithConfiguration_RegistersInDI()
+ {
+ List expectedData =
+ [
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Information, 1, "number one", [new("region", "westus2"), new ("priority", 1)]),
+ new LogBufferingFilterRule(logLevel : LogLevel.Information),
+ ];
+ ConfigurationBuilder configBuilder = new ConfigurationBuilder();
+ configBuilder.AddJsonFile("appsettings.json");
+ IConfigurationRoot configuration = configBuilder.Build();
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder =>
+ {
+ builder.AddGlobalBuffering(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.MaxBufferSizeInBytes); // 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..e41e877bd7c
--- /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 LogBufferingFilterRule(null, null, null, null),
+ new LogBufferingFilterRule(null, null, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Information, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Information, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, null, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, 2, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes: [new("region2", "westus2")]), // inapplicable key
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus3")]), // inapplicable value
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // the best rule - [11]
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 2, null),
+ new LogBufferingFilterRule("Program.MyLogger", null, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, null, null),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Error, 1, null),
+ };
+
+ // Act
+ LogBufferingFilterRuleSelector.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 LogBufferingFilterRule(null, LogLevel.Information, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Information, 1, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, null, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, 2, null),
+ new LogBufferingFilterRule(null, LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program1.MyLogger", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.*MyLogger1", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.MyLogger*", LogLevel.Warning, 1, null),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // the best rule
+ new LogBufferingFilterRule("Program.MyLogger*", LogLevel.Warning, 1, attributes:[new("region", "westus2")]), // same as the best, but last and should be selected
+ };
+
+ // Act
+ LogBufferingFilterRuleSelector.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 LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 1)]),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 2)]), // the best rule
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, attributes:[new("priority", 3)]),
+ new LogBufferingFilterRule("Program.MyLogger", LogLevel.Warning, 1, null),
+ };
+
+ // Act
+ LogBufferingFilterRuleSelector.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..27820b1ea24 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.AddGlobalBuffering(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 LogBuffer 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..f829c55c4dc 100644
--- a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json
+++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json
@@ -16,5 +16,30 @@
"MeterStateOverrides": {
"": "Disabled"
}
+ },
+ "Buffering": {
+ "MaxLogRecordSizeInBytes": 100,
+ "MaxBufferSizeInBytes": 1000,
+ "Rules": [
+ {
+ "CategoryName": "Program.MyLogger",
+ "LogLevel": "Information",
+ "EventId": 1,
+ "EventName" : "number one",
+ "Attributes": [
+ {
+ "key": "region",
+ "value": "westus2"
+ },
+ {
+ "key": "priority",
+ "value": 1
+ }
+ ]
+ },
+ {
+ "LogLevel": "Information"
+ }
+ ]
}
}