My application uses iLogger and I've implemented a log provider to write to a local log file.
The problem I'm having is there is a lot being logged especially in debug mode and what I'm finding is that the file is in use and can't open to append to it. I'm looking for some suggestions on how to avoid this issue.
I thought about having Log function add to a Queue and have another thread start and process the queue so that there is no issues blocking writing but the iLogger doesn't have a deconstructor that I can find what would allow it to finish emptying the queue before destroying the queue and shutting down.
try
{
_locker.AcquireWriterLock(int.MaxValue);
File.AppendAllText(fullFilePath, logRecord);
}
catch (Exception ex)
{
throw ex;
}
finally
{
_locker.ReleaseWriterLock();
}
CodePudding user response:
The Easy Solution
The easy solution would be to use a library like Serilog for persisting logs.
The Not-So-Easy and Tricky Solution
In case the easy option is not available, I suggest you move your application to Generic Host model and setup a BackgroundService for log persistence. Inside the service you can use Channel to queue the logs for persistece and persist the logs inside the ExecuteAsync
into file using an asynchronous implementation.
The ExecuteAsync
would look something like this:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// open file
while (!stoppingToken.IsCancellationRequested)
{
try
{
// get item from the queue and persist it asynchronously
}
catch (Exception ex)
{
// this might not be a good idea since the new log would also be queued up creating an infinite loop
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
protected override async Task StopAsync(CancellationToken stoppingToken)
{
// do similar while loop then close the file
}
This rough setup has some corner cases and is not ideal at all but would get the job done in a test application.
CodePudding user response:
In any case you need to write concurrently in the same file you can do the following:
async Task WriteToFileAsync(string path, string text)
{
byte[] byteArray = Encoding.Unicode.GetBytes(text);
using var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, bufferSize: 2048, useAsync: true);
await stream.WriteAsync(byteArray);
}
If, lets say, you have a list of tasks that you need to execute concurrently, you can fire them all using await Task.WhenAll()
.
var path = Environment.CurrentDirectory "/logs.txt";
var tasks = Enumerable.Range(0, 10).Select(i => WriteToFileAsync(path, $"Log #{i}\n"));
await Task.WhenAll(tasks);
Update: Based on https://github.com/dotnet/docs/blob/main/docs/core/extensions/snippets/configuration/console-custom-logging
Because Log(...)
method of ILogger
must be void you could make the WriteToFile sync:
private static void WriteToFile(string text)
{
var path = Environment.CurrentDirectory "/logs.txt";
byte[] byteArray = Encoding.Unicode.GetBytes(text);
using var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, bufferSize: 2048);
stream.Write(byteArray);
}
and in Log method you can use it like:
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
ColorConsoleLoggerConfiguration config = _getCurrentConfig();
if (config.EventId == 0 || config.EventId == eventId.Id)
{
// ...
WriteToFile($"{formatter(state, exception)}");
}
}
Then you can inject the ILogger in any class you want:
public class App
{
private readonly ILogger<App> _logger;
public App(ILogger<App> logger)
{
_logger = logger;
}
public async Task ExecuteTaskAsync(string txt)
{
_logger.LogInformation(txt);
await Task.CompletedTask;
}
}
Finally the same code ( await Task.WhenAll
) should work and you will write to the file concurrently:
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
services.AddScoped<App>();
})
.ConfigureLogging(builder =>
builder.ClearProviders()
.AddColorConsoleLogger(configuration =>
{
// Replace warning value from appsettings.json of "Cyan"
configuration.LogLevels[LogLevel.Warning] = ConsoleColor.DarkCyan;
// Replace warning value from appsettings.json of "Red"
configuration.LogLevels[LogLevel.Error] = ConsoleColor.DarkRed;
}))
.Build();
// ...
var app = host.Services.GetRequiredService<App>();
var tasks = Enumerable.Range(0, 10).Select(i => app.ExecuteTaskAsync($"Log #{i}\n"));
await Task.WhenAll(tasks);
await host.RunAsync();