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 1 commit into
base: main
Choose a base branch
from

Conversation

evgenyfedorov2
Copy link
Contributor

@evgenyfedorov2 evgenyfedorov2 commented Nov 13, 2024

Related to the #5123 proposal, this PR is focused on the logging buffering only. See also the sampling part #5574.

Microsoft Reviewers: Open in CodeFlow

if (loggerInfo.Logger is IBufferedLogger bufferedLogger)
{
if (config.BufferProvider is not null &&
config.BufferProvider.CurrentBuffer.TryEnqueue(bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, joiner.Formatter!(joiner.State, exception)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd want to only buffer the log data once even if there are multiple LogProviders that will ultimately receive it. Otherwise we might be duplicating large amounts of data for little benefit.

if (loggerInfo.Logger is IBufferedLogger bufferedLogger)
{
if (config.BufferProvider is not null &&
config.BufferProvider.CurrentBuffer.TryEnqueue(bufferedLogger, logLevel, loggerInfo.Category!, eventId, joiner, exception, joiner.Formatter!(joiner.State, exception)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My previous understanding of our intended policy is that we were going to buffer messages whenever buffering was enabled, regardless of whether the logger implements IBufferedLogger. When using buffering with an ILogger that doesn't support buffered logging we would just invoke the ILogger.Log() method and accept that the behavior might be sub-optimal if the logger captured new timestamps, thread ids or other ambient state.

If folks want to change that policy to something else I'm happy to discuss it, but otherwise I'd suggest we stick to buffering regardless of whether IBufferLogger is implemented.

{
// The record was buffered, so we skip logging it for now.
// When a caller needs to flush the buffer and calls ILoggerBuffer.Flush(),
// the buffer will internally call IBufferedLogger.LogRecords to emit log records.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set of logger providers can change dynamically as the program runs which means LoggerProviders at the time the time a buffer is flushed might not be the same as the LoggerProviders which were present when a given message was enqueued. I suspect users would find the behavior more intuitive if we flush using whatever providers are active at the time the flush occurs rather than cached providers from earlier in the program's execution. One way to achieve could be to have ExtendedLogger create an instance of IBufferedLogger which when invoked enumerates all current providers and forwards the call.

? _globalBufferProvider.CurrentBuffer
: _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService<HttpRequestBuffer>());

// TO DO: Dispose request buffer when the respective HttpContext is disposed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you mean you want to get the HttpRequestBuffer out of the dictionary? What might be easier is to avoid using _requestBuffers at all and instead use a storage location that is already scoped to HttpContext. For example HttpRequest.Items or HttpContext.RequestServices.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this measured in log messages? I think we should also consider if we need guards on the size of an individual message or size of all messages combined. For example if someone logged 500 messages each of size 10MB I expect many services would fail with an OOM. I don't expect anyone would intentionally log such a large message but it might happen because of an unintended bug or a malicious actor has discovered a way to get large content into a log message.

/// <summary>
/// Gets or sets the size of the buffer.
/// </summary>
public int Capacity { get; set; } = 1_000_000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a pretty large default size.

});

var serviceProvider = serviceCollection.BuildServiceProvider();
var buffer = serviceProvider.GetService<ILoggingBuffer>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we expecting users to query the buffer from the DI container for some reason? I wasn't expecting them to do that. If there was going to be some buffer in the DI container I also would have expected it to be the GlobalBuffer as there is no http request in progress.

@@ -714,6 +718,48 @@ await RunAsync(
});
}

#if NET9_0_OR_GREATER
[Fact]
public async Task HttpRequestBuffering()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to be this test, but it would be nice to have some test or sample code that uses the feature the way we expect an end-user to use it. For example I would not expect end users to new up the Options type, create their own instances of HttpRequestBuffer or manually add objects to the DI container.

});

var serviceProvider = serviceCollection.BuildServiceProvider();
var buffer = serviceProvider.GetService<ILoggingBuffer>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we'd want this buffer registered under a public interface in DI. Is there a user-scenario where it matters?

@@ -119,6 +122,58 @@ public static void FeatureEnablement(bool enableRedaction, bool enableEnrichment
}
}

#if NET9_0_OR_GREATER
[Fact]
public static void GlobalBuffering()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to Http, it would be nice to have an example that uses global buffering in some canonical way that we expect end-users to use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants