Skip to content

Commit

Permalink
Buffering
Browse files Browse the repository at this point in the history
  • Loading branch information
evgenyfedorov2 committed Nov 13, 2024
1 parent 1073446 commit 988c709
Show file tree
Hide file tree
Showing 32 changed files with 1,374 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET9_0_OR_GREATER
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Extensions.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Diagnostics.Logging;

internal sealed class HttpRequestBuffer : ILoggingBuffer
{
private readonly IOptionsMonitor<HttpRequestBufferOptions> _options;
private readonly ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>> _buffers;
private readonly TimeProvider _timeProvider = TimeProvider.System;
private DateTimeOffset _lastFlushTimestamp;

public HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options)
{
_options = options;
_buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>();
_lastFlushTimestamp = _timeProvider.GetUtcNow();
}

internal HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options, TimeProvider timeProvider)
: this(options)
{
_timeProvider = timeProvider;
_lastFlushTimestamp = _timeProvider.GetUtcNow();
}

public bool TryEnqueue(
IBufferedLogger logger,
LogLevel logLevel,
string category,
EventId eventId,
IReadOnlyList<KeyValuePair<string, object?>> joiner,
Exception? exception,
string formatter)
{
if (!IsEnabled(category, logLevel, eventId))
{
return false;
}

var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter);
var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue<HttpRequestBufferedLogRecord>());

// probably don't need to limit buffer capacity?
// because buffer is disposed when the respective HttpContext is disposed
// don't expect it to grow so much to cause a problem?
if (queue.Count >= _options.CurrentValue.PerRequestCapacity)
{
_ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
}

queue.Enqueue(record);

return true;
}

public void Flush()
{
foreach (var (logger, queue) in _buffers)
{
var result = new List<BufferedLogRecord>();
while (!queue.IsEmpty)
{
if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item))
{
result.Add(item);
}
}

logger.LogRecords(result);
}

_lastFlushTimestamp = _timeProvider.GetUtcNow();
}

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

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

return rule is not null;
}
}
#endif
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.

#if NET9_0_OR_GREATER
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Diagnostics.Logging;

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);
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET9_0_OR_GREATER
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Diagnostics.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
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 AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configuration);

return builder
.AddHttpRequestBufferConfiguration(configuration)
.AddHttpRequestBufferProvider();
}

/// <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="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 AddHttpRequestBuffer(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.AddHttpRequestBufferProvider();
}

/// <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>
public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder)
{
_ = Throw.IfNull(builder);

builder.Services.TryAddScoped<HttpRequestBuffer>();
builder.Services.TryAddScoped<ILoggingBuffer>(sp => sp.GetRequiredService<HttpRequestBuffer>());
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.TryAddActivatedSingleton<ILoggingBufferProvider, HttpRequestBufferProvider>();

return builder.AddGlobalBufferProvider();
}

/// <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;
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET9_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.AspNetCore.Diagnostics.Logging;

/// <summary>
/// The options for LoggerBuffer.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public class HttpRequestBufferOptions
{
/// <summary>
/// Gets or sets the time to suspend the buffer after flushing.
/// </summary>
/// <remarks>
/// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately,
/// so the buffering will be suspended for the <see paramref="SuspendAfterFlushDuration"/> time.
/// </remarks>
public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Gets or sets the size of the buffer for a request.
/// </summary>
public int PerRequestCapacity { get; set; } = 1_000;

/// <summary>
/// Gets or sets the size of the global buffer which applies to non-request logs only.
/// </summary>
public int GlobalCapacity { get; set; } = 1_000_000;

#pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange()
#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
/// <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
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET9_0_OR_GREATER
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Diagnostics.Logging;

internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider
{
private readonly GlobalBufferProvider _globalBufferProvider;
private readonly IHttpContextAccessor _accessor;
private readonly ConcurrentDictionary<string, HttpRequestBuffer> _requestBuffers = new();

public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor)
{
_globalBufferProvider = globalBufferProvider;
_accessor = accessor;
}

public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null
? _globalBufferProvider.CurrentBuffer
: _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService<HttpRequestBuffer>());

// TO DO: Dispose request buffer when the respective HttpContext is disposed
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET9_0_OR_GREATER
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.AspNetCore.Diagnostics.Logging;

internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord
{
public HttpRequestBufferedLogRecord(
LogLevel logLevel,
EventId eventId,
IReadOnlyList<KeyValuePair<string, object?>> state,
Exception? exception,
string? formatter)
{
LogLevel = logLevel;
EventId = eventId;
Attributes = state;
Exception = exception?.ToString(); // wtf??
FormattedMessage = formatter;
}

public override IReadOnlyList<KeyValuePair<string, object?>> Attributes { get; }
public override string? FormattedMessage { get; }
public override string? Exception { get; }

public override DateTimeOffset Timestamp { get; }

public override LogLevel LogLevel { get; }

public override EventId EventId { get; }
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<InjectSharedPools>false</InjectSharedPools>
<InjectSharedBufferWriterPool>true</InjectSharedBufferWriterPool>
<InjectSharedNumericExtensions>false</InjectSharedNumericExtensions>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
</PropertyGroup>

Expand Down
Loading

0 comments on commit 988c709

Please sign in to comment.