Skip to content

Commit

Permalink
Major update
Browse files Browse the repository at this point in the history
  • Loading branch information
evgenyfedorov2 committed Dec 12, 2024
1 parent 988c709 commit 9d61d12
Show file tree
Hide file tree
Showing 36 changed files with 1,080 additions and 420 deletions.
4 changes: 4 additions & 0 deletions eng/MSBuild/Shared.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@
<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
</ItemGroup>

<ItemGroup Condition="'$(InjectJsonExceptionConverter)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\JsonExceptionConverter\*.cs" LinkBase="Shared\JsonExceptionConverter" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,86 @@
#if NET9_0_OR_GREATER
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Diagnostics;
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.Logging;

internal sealed class HttpRequestBuffer : ILoggingBuffer
{
private readonly IOptionsMonitor<HttpRequestBufferOptions> _options;
private readonly ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>> _buffers;
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(IOptionsMonitor<HttpRequestBufferOptions> options)
public HttpRequestBuffer(IBufferSink bufferSink, IOptionsMonitor<HttpRequestBufferOptions> options)
{
_options = options;
_buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>();
_lastFlushTimestamp = _timeProvider.GetUtcNow();
}
_bufferSink = bufferSink;
_buffer = new ConcurrentQueue<SerializedLogRecord>();

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

public bool TryEnqueue(
IBufferedLogger logger,
[RequiresUnreferencedCode(
"Calls Microsoft.Extensions.Logging.SerializedLogRecord.SerializedLogRecord(LogLevel, EventId, DateTimeOffset, IReadOnlyList<KeyValuePair<String, Object>>, Exception, String)")]
public bool TryEnqueue<TState>(
LogLevel logLevel,
string category,
EventId eventId,
IReadOnlyList<KeyValuePair<string, object?>> joiner,
TState attributes,
Exception? exception,
string formatter)
Func<TState, 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)
switch (attributes)
{
_ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _);
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;
}

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

return true;
}

[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.BufferSink.LogRecords(IEnumerable<SerializedLogRecord>)")]
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);
}
var result = _buffer.ToArray();
_buffer.Clear();

_lastFlushTimestamp = _timeProvider.GetUtcNow();

_bufferSink.LogRecords(result);
}

public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
Expand All @@ -89,9 +93,18 @@ public bool IsEnabled(string category, LogLevel logLevel, EventId eventId)
return false;
}

LoggerFilterRuleSelector.Select<BufferFilterRule>(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule);
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 _);
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand All @@ -30,14 +31,16 @@ public static class HttpRequestBufferLoggerBuilderExtensions
/// <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)
public static ILoggingBuilder AddHttpRequestBuffering(this ILoggingBuilder builder, IConfiguration configuration)
{
_ = Throw.IfNull(builder);
_ = Throw.IfNull(configuration);

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

/// <summary>
Expand All @@ -49,15 +52,18 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
/// <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)
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.AddHttpRequestBufferProvider();
return builder
.AddHttpRequestBufferManager()
.AddGlobalBuffer(level)
.AddGlobalBufferManager();
}

/// <summary>
Expand All @@ -66,16 +72,20 @@ public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder,
/// <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)
internal static ILoggingBuilder AddHttpRequestBufferManager(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();
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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.AspNetCore.Diagnostics.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Diagnostics.Buffering;
internal sealed class HttpRequestBufferManager : IHttpRequestBufferManager
{
private readonly GlobalBufferManager _globalBufferManager;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptionsMonitor<HttpRequestBufferOptions> _requestOptions;

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

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);
httpContext.Items[category] = httpRequestBuffer;
return httpRequestBuffer;
}

if (buffer is not ILoggingBuffer loggingBuffer)
{
throw new InvalidOperationException($"Unable to parse value of {buffer} of the {category}");
}

return loggingBuffer;
}

[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
public void Flush() => _globalBufferManager.Flush();

[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.Flush()")]
public void FlushCurrentRequestLogs()
{
if (_httpContextAccessor.HttpContext is not null)
{
foreach (var kvp in _httpContextAccessor.HttpContext!.Items)
{
if (kvp.Value is ILoggingBuffer buffer)
{
buffer.Flush();
}
}
}
}

[RequiresUnreferencedCode("Calls Microsoft.Extensions.Logging.ILoggingBuffer.TryEnqueue<TState>(LogLevel, String, EventId, TState, Exception, Func<TState, Exception, String>)")]
public bool TryEnqueue<TState>(
IBufferSink bufferSink,
LogLevel logLevel,
string category,
EventId eventId,
TState attributes,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var buffer = CreateBuffer(bufferSink, category);
return buffer.TryEnqueue(logLevel, category, eventId, attributes, exception, formatter);
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ public class HttpRequestBufferOptions
public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Gets or sets the size of the buffer for a request.
/// Gets or sets the duration to check and remove the buffered items exceeding the <see cref="PerRequestCapacity"/>.
/// </summary>
public int PerRequestCapacity { get; set; } = 1_000;
public TimeSpan PerRequestDuration { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// Gets or sets the size of the global buffer which applies to non-request logs only.
/// Gets or sets the size of the buffer for a request.
/// </summary>
public int GlobalCapacity { get; set; } = 1_000_000;
public int PerRequestCapacity { get; set; } = 1_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
Expand Down

This file was deleted.

Loading

0 comments on commit 9d61d12

Please sign in to comment.