There's an ILogger in asp.net core (or in other libraries too), and I can setup my code to write logs to azure or database or console etc, but what I'm wondering about is that this ILogger is synchronous. On docs.microsoft I read about this they said "logger should be synchronous, consider writing logs to some queue and have a background worker pulling these logs to your database". Now, I have a couple of questions.
Maybe I'm asking a dumb question or a broad one but this is a broad topic to me that I don't really understand. Please help. Also I would like to see some code examples (some github repos or something)
What Microsoft is doing in their implementation of the ConsoleLogger
is using a background thread which will write queued messages to the console.
I just implemented something similar but with an EF Core context instead.
The original implementation can be found here: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs
My implementation looks something like this:
public class RequestLogProcessor : IDisposable
{
private readonly BlockingCollection<RequestLog> _queue;
private readonly Thread _writingThread;
private readonly DbContext _context;
public RequestLogProcessor(DbContext context, RequestLoggerConfiguration configuration)
{
_queue = new BlockingCollection<RequestLog>(configuration.Buffer);
_context = context;
_writingThread = new Thread(WriteQueue)
{
IsBackground = true,
Name = "RequestLogProcessor Thread"
};
_writingThread.Start();
}
private void WriteQueue()
{
try
{
foreach (var message in _queue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
catch
{
try
{
_queue.CompleteAdding();
}
catch { }
}
}
public void Enqueue(RequestLog log)
{
if (!_queue.IsAddingCompleted)
{
try
{
_queue.Add(log);
return;
}
catch (InvalidOperationException) { }
}
try
{
WriteMessage(log);
}
catch (Exception) { }
}
private void WriteMessage(RequestLog log)
{
_context.RequestLogs.Add(log);
_context.SaveChanges();
}
public void Dispose()
{
_queue.CompleteAdding();
try
{
_writingThread.Join(1500);
}
catch (ThreadStateException) { }
}
}
You would use this class in your implementation of ILogger
s Log<TState>
function like so:
public class RequestLogger : ILogger
{
private readonly RequestLoggerConfiguration _config;
private readonly RequestLogProcessor _processor;
public RequestLogger(
RequestLoggerConfiguration config,
RequestLogProcessor processor)
{
_config = config;
_processor = processor;
}
public IDisposable BeginScope<TState>(TState state)
=> default;
public bool IsEnabled(LogLevel logLevel)
=> logLevel <= _config.LogLevel && logLevel != LogLevel.None;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
return;
if (state is not RequestLog log)
return;
log.Exception ??= exception?.ToString();
log.Level = logLevel.ToString();
_processor.Enqueue(log); // this is the line
}
}
This works perfectly fine in my ASP.NET Core application, although you could tweak it's performance by batch inserting logs. I did call SaveChanges()
for each entry which could possibly be pretty slow.
In your case you can just create the Thread with an async function delegate:
new Thread(async() => await WriteQueue())
and use _context.SaveChangesAsync()
.
EDIT(as per comment):
public class RequestLoggerConfiguration
{
public LogLevel LogLevel { get; set; } = LogLevel.Error;
public int Buffer { get; set; } = 1024;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With