Home > Mobile >  C# iLogger when Log is called unable to write to file because file is in use
C# iLogger when Log is called unable to write to file because file is in use

Time:08-26

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();
  • Related