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 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 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 @@ -46,4 +46,8 @@
<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
</ItemGroup>

<ItemGroup Condition="'$(InjectExceptionJsonConverter)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\ExceptionJsonConverter\*.cs" LinkBase="Shared\ExceptionJsonConverter" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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;
using Microsoft.Extensions.Logging;
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<HttpRequestBufferOptions> _options;
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions;
private readonly ConcurrentQueue<SerializedLogRecord> _buffer;
private readonly TimeProvider _timeProvider = TimeProvider.System;
private readonly IBufferSink _bufferSink;
private readonly object _bufferCapacityLocker = new();
private DateTimeOffset _truncateAfter;
private DateTimeOffset _lastFlushTimestamp;

public HttpRequestBuffer(IBufferSink bufferSink,
IOptionsMonitor<HttpRequestBufferOptions> options,
IOptionsMonitor<GlobalBufferOptions> globalOptions)
{
_options = options;
_globalOptions = globalOptions;
_bufferSink = bufferSink;
_buffer = new ConcurrentQueue<SerializedLogRecord>();

_truncateAfter = _timeProvider.GetUtcNow();
}

public bool TryEnqueue<TState>(
LogLevel logLevel,
string category,
EventId eventId,
TState attributes,
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(category, logLevel, eventId))
{
return false;
}

switch (attributes)
{
case ModernTagJoiner modernTagJoiner:
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), modernTagJoiner, exception,
((Func<ModernTagJoiner, Exception?, string>)(object)formatter)(modernTagJoiner, exception)));
break;
case LegacyTagJoiner legacyTagJoiner:
_buffer.Enqueue(new SerializedLogRecord(logLevel, eventId, _timeProvider.GetUtcNow(), legacyTagJoiner, exception,
((Func<LegacyTagJoiner, Exception?, string>)(object)formatter)(legacyTagJoiner, exception)));
break;
default:
Throw.ArgumentException(nameof(attributes), $"Unsupported type of the log attributes object detected: {typeof(TState)}");
break;
}

var now = _timeProvider.GetUtcNow();
lock (_bufferCapacityLocker)
{
if (now >= _truncateAfter)
{
_truncateAfter = now.Add(_options.CurrentValue.PerRequestDuration);
TruncateOverlimit();
}
}

return true;
}

public void Flush()
{
var result = _buffer.ToArray();
_buffer.Clear();

_lastFlushTimestamp = _timeProvider.GetUtcNow();

_bufferSink.LogRecords(result);
}

public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
{
if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _globalOptions.CurrentValue.SuspendAfterFlushDuration)
{
return false;
}

LoggerFilterRuleSelector.Select(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);

return rule is not null;
}

public void TruncateOverlimit()
{
// Capacity is a soft limit, which might be exceeded, esp. in multi-threaded environments.
while (_buffer.Count > _options.CurrentValue.PerRequestCapacity)
{
_ = _buffer.TryDequeue(out _);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HttpRequestBufferOptions>
{
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<HttpRequestBufferOptions>();
if (parsedOptions is null)
{
return;
}

options.Rules.AddRange(parsedOptions.Rules);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 log buffers in a dependency injection container.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HttpRequestBufferLoggerBuilderExtensions
{
/// <summary>
/// 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./>.
/// </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>
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configuration);

return builder
.AddHttpRequestBufferConfiguration(configuration)
.AddHttpRequestBufferManager()
.AddGlobalBufferConfiguration(configuration)
.AddGlobalBufferManager();
}

/// <summary>
/// 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./>.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
/// <param name="level">The log level (and below) to apply the buffer to.</param>
/// <param name="configure">The buffer configuration options.</param>
/// <returns>The value of <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, LogLevel? level = null, Action<HttpRequestBufferOptions>? configure = null)
{
_ = Throw.IfNull(builder);

_ = builder.Services
.Configure<HttpRequestBufferOptions>(options => options.Rules.Add(new BufferFilterRule(null, level, null)))
.Configure(configure ?? new Action<HttpRequestBufferOptions>(_ => { }));

return builder
.AddHttpRequestBufferManager()
.AddGlobalBuffer(level)
.AddGlobalBufferManager();
}

/// <summary>
/// Adds HTTP request buffer provider to the logging infrastructure.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder" />.</param>
/// <returns>The <see cref="ILoggingBuilder"/> so that additional calls can be chained.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
internal static ILoggingBuilder AddHttpRequestBufferManager(this ILoggingBuilder builder)
{
_ = Throw.IfNull(builder);

builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

builder.Services.TryAddSingleton<ExtendedLoggerFactory>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerFactory, ExtendedLoggerFactory>(sp => sp.GetRequiredService<ExtendedLoggerFactory>()));

builder.Services.TryAddSingleton<HttpRequestBufferManager>();
builder.Services.TryAddSingleton<IBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>());
builder.Services.TryAddSingleton<IHttpRequestBufferManager>(static sp => sp.GetRequiredService<HttpRequestBufferManager>());

return builder;
}

/// <summary>
/// Configures <see cref="HttpRequestBufferOptions" /> from an instance of <see cref="IConfiguration" />.
/// </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>
internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);

_ = builder.Services.AddSingleton<IConfigureOptions<HttpRequestBufferOptions>>(new HttpRequestBufferConfigureOptions(configuration));

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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.Diagnostics.Buffering;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Diagnostics.Buffering;

internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager
{
private readonly GlobalBufferManager _globalBufferManager;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptionsMonitor<HttpRequestBufferOptions> _requestOptions;
private readonly IOptionsMonitor<GlobalBufferOptions> _globalOptions;

public HttpRequestBufferManager(
GlobalBufferManager globalBufferManager,
IHttpContextAccessor httpContextAccessor,
IOptionsMonitor<HttpRequestBufferOptions> requestOptions,
IOptionsMonitor<GlobalBufferOptions> globalOptions)
{
_globalBufferManager = globalBufferManager;
_httpContextAccessor = httpContextAccessor;
_requestOptions = requestOptions;
_globalOptions = globalOptions;
}

public ILoggingBuffer CreateBuffer(IBufferSink bufferSink, string category)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
{
return _globalBufferManager.CreateBuffer(bufferSink, category);
}

if (!httpContext.Items.TryGetValue(category, out var buffer))
{
var httpRequestBuffer = new HttpRequestBuffer(bufferSink, _requestOptions, _globalOptions);
httpContext.Items[category] = httpRequestBuffer;
return httpRequestBuffer;
}

if (buffer is not ILoggingBuffer loggingBuffer)
{
throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}");
}
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved

return loggingBuffer;
}

public void Flush() => _globalBufferManager.Flush();

public void FlushCurrentRequestLogs()
{
if (_httpContextAccessor.HttpContext is not null)
{
foreach (var kvp in _httpContextAccessor.HttpContext!.Items)
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
{
if (kvp.Value is ILoggingBuffer buffer)
{
buffer.Flush();
}
}
}
}

public bool TryEnqueue<TState>(
IBufferSink bufferSink,
LogLevel logLevel,
string category,
EventId eventId,
TState attributes,
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var buffer = CreateBuffer(bufferSink, category);
return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Diagnostics.Buffering;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.AspNetCore.Diagnostics.Buffering;

/// <summary>
/// The options for LoggerBuffer.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public class HttpRequestBufferOptions
{
/// <summary>
/// Gets or sets the duration to check and remove the buffered items exceeding the <see cref="PerRequestCapacity"/>.
/// </summary>
public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// Gets or sets the size of the buffer for a request.
/// </summary>
public int PerRequestCapacity { get; set; } = 1_000;
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved

#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
/// <summary>
/// Gets or sets the collection of <see cref="BufferFilterRule"/> used for filtering log messages for the purpose of further buffering.
/// </summary>
public List<BufferFilterRule> Rules { get; set; } = [];
#pragma warning restore CA2227 // Collection properties should be read only
#pragma warning restore CA1002 // Do not expose generic lists
}
Loading
Loading