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;
}
_context.AddAsync
and_context.SaveChangesAsync()
. What I would do is creating a log class using EF, write the log to the class instance and when I am done, just run the Async operations above, unless I am totally misunderstood you... – Yaniv Amrami