Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logging buffering #5635

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions eng/MSBuild/Shared.props
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@
<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
</ItemGroup>

<ItemGroup Condition="'$(InjectSharedLoggingBuffering)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\LoggingBuffering\*.cs" LinkBase="Shared\LoggingBuffering" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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<HttpRequestLogBufferingOptions> _options;
private readonly IOptionsMonitor<GlobalLogBufferingOptions> _globalOptions;
private readonly ConcurrentQueue<SerializedLogRecord> _buffer;
private readonly TimeProvider _timeProvider = TimeProvider.System;
private readonly IBufferedLogger _bufferedLogger;

private DateTimeOffset _lastFlushTimestamp;
private int _bufferSize;

public HttpRequestBuffer(IBufferedLogger bufferedLogger,
IOptionsMonitor<HttpRequestLogBufferingOptions> options,
IOptionsMonitor<GlobalLogBufferingOptions> globalOptions)
{
_options = options;
_globalOptions = globalOptions;
_bufferedLogger = bufferedLogger;
_buffer = new ConcurrentQueue<SerializedLogRecord>();
}

public bool TryEnqueue<TState>(LogEntry<TState> 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<ModernTagJoiner, Exception?, string>)(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<LegacyTagJoiner, Exception?, string>)(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<DeserializedLogRecord>(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<KeyValuePair<string, object?>> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<string, ILoggingBuffer> _buffers = new();

public ILoggingBuffer GetOrAdd(string category, Func<string, ILoggingBuffer> valueFactory) =>
_buffers.GetOrAdd(category, valueFactory);

public void Flush()
{
foreach (var buffer in _buffers.Values)
{
buffer.Flush();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HttpRequestLogBufferingOptions> _requestOptions;
private readonly IOptionsMonitor<GlobalLogBufferingOptions> _globalOptions;

public HttpRequestBufferManager(
LogBuffer globalBuffer,
IHttpContextAccessor httpContextAccessor,
IOptionsMonitor<HttpRequestLogBufferingOptions> requestOptions,
IOptionsMonitor<GlobalLogBufferingOptions> globalOptions)
{
_globalBuffer = globalBuffer;
_httpContextAccessor = httpContextAccessor;
_requestOptions = requestOptions;
_globalOptions = globalOptions;
}

public override void Flush() => _globalBuffer.Flush();

public override void FlushCurrentRequestLogs()
{
_httpContextAccessor.HttpContext?.RequestServices.GetService<HttpRequestBufferHolder>()?.Flush();
}

public override bool TryEnqueue<TState>(IBufferedLogger bufferedLogger, in LogEntry<TState> logEntry)
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
{
return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
}

HttpRequestBufferHolder? bufferHolder = httpContext.RequestServices.GetService<HttpRequestBufferHolder>();
ILoggingBuffer? buffer = bufferHolder?.GetOrAdd(logEntry.Category, _ => new HttpRequestBuffer(bufferedLogger, _requestOptions, _globalOptions)!);

if (buffer is null)
{
return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
}

return buffer.TryEnqueue(logEntry);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Lets you register HTTP request log buffering in a dependency injection container.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HttpRequestBufferingLoggingBuilderExtensions
{
/// <summary>
/// Adds HTTP request log buffering to the logging infrastructure.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
/// <param name="configuration">The <see cref="IConfiguration" /> to add.</param>
/// <returns>The value of <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
/// </remarks>
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configuration);

_ = builder.Services.AddSingleton<IConfigureOptions<HttpRequestLogBufferingOptions>>(new HttpRequestLogBufferingConfigureOptions(configuration));

return builder
.AddHttpRequestBufferManager()
.AddGlobalBuffering(configuration);
}

/// <summary>
/// Adds HTTP request log buffering to the logging infrastructure.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
/// <param name="configure">The buffer configuration delegate.</param>
/// <returns>The value of <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
/// </remarks>
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, Action<HttpRequestLogBufferingOptions> 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);
}

/// <summary>
/// Adds HTTP request log buffering to the logging infrastructure.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
/// <param name="logLevel">The log level (and below) to apply the buffer to.</param>
/// <returns>The value of <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Matched logs will be buffered in a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime.
/// </remarks>
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? logLevel = null)
{
_ = Throw.IfNull(builder);

_ = builder.Services.Configure<HttpRequestLogBufferingOptions>(options => options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel)));

return builder
.AddHttpRequestBufferManager()
.AddGlobalBuffering(logLevel);
}

internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
{
builder.Services.TryAddScoped<HttpRequestBufferHolder>();
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.TryAddSingleton(sp =>
{
var globalBufferManager = sp.GetRequiredService<GlobalBufferManager>();
return ActivatorUtilities.CreateInstance<HttpRequestBufferManager>(sp, globalBufferManager);
});
builder.Services.TryAddSingleton<LogBuffer>(sp => sp.GetRequiredService<HttpRequestBufferManager>());
builder.Services.TryAddSingleton<HttpRequestLogBuffer>(sp => sp.GetRequiredService<HttpRequestBufferManager>());

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Buffers HTTP request logs into circular buffers and drops them after some time if not flushed.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public abstract class HttpRequestLogBuffer : LogBuffer
{
/// <summary>
/// Flushes buffers and emits buffered logs for the current HTTP request.
/// </summary>
public abstract void FlushCurrentRequestLogs();
}
Original file line number Diff line number Diff line change
@@ -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<HttpRequestLogBufferingOptions>
{
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<HttpRequestLogBufferingOptions>();
if (parsedOptions is null)
{
return;
}

foreach (var rule in parsedOptions.Rules)
{
options.Rules.Add(rule);
}
}
}
Loading
Loading