使用控制台记录器的控制台应用程序不会写入最终消息。轻松重现

Elr*_*ynn 3 logging flush .net-core

Windows 上使用 LoggerFactory 的 .net 6 控制台应用程序 - 最终消息不会出现在控制台上。loggerFactory.Dispose() 没有帮助。Ilogger 没有 Dispose() 方法。

这是一个复制品。大约会出现 20 到 70 条循环消息。之后就什么都没有了。

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddFilter("Microsoft", LogLevel.Warning)
        .AddFilter("System", LogLevel.Warning)
        .AddFilter("MyTestApp.Program", LogLevel.Debug)
        .AddConsole();
});
ILogger logger = loggerFactory.CreateLogger<Program>();
try
{ 
    logger.LogInformation("Starting");
    for (var i=0; i < 100; i++)
    {
        logger.LogInformation($"This is a nice long message for {i}.");
    }
    throw new Exception("boom");
}
catch (Exception e)
{
    logger.LogInformation(e.ToString());
}
finally
{
    loggerFactory.Dispose();
}

Run Code Online (Sandbox Code Playgroud)

Han*_*ank 5

这是由于在此提交关联 PRMicrosoft.Extensions.Logging.Console\src\ConsoleLoggerProcessor.cs )中重构了方式。

调用logger.Log不必立即刷新消息。如果确实如此,调用可能会花费很长时间并且阻塞的时间比预期的要长。记录器将消息传递给ILoggers从您的ILoggerProviders 创建的所有内部进程,并根据这些消息执行他们将要执行的操作。根据您的情况配置 LoggerFactory 添加builder.AddConsole();ConsoleLoggerProvider : ILoggerProvider

创建 后ILoggerFactory,所有配置的提供程序都会被实例化,此时会构造一个名为 的ConsoleLoggerProvider 新类ConsoleLoggerProcessor。它负责Console.Out它创建的后台线程写入消息,以处理控制台记录器消息队列。记录器将消息推送到此队列,以便它们可以快速返回给调用者,而不会阻塞文件 IO。

ILoggerFactory被处置时,它将处置所有创建的内容ILoggerProviders,从而导致处置ConsoleLoggerProcesser。处理该类Thread.Join调用上面链接的消息队列处理线程。

以前,ConsoleLoggerProcessor的队列是通过BlockingCollection<T>. 这是一个极其简化的视图:

public class ConsoleLoggerProcessor : IDisposable
{
    private readonly Thread _outputThread;
    private readonly BlockingCollection<string> _messageQueue = new();

    public ConsoleLoggerProcessor()
    {
        _outputThread = new Thread(ProcessLogQueue) { IsBackground = true };
        _outputThread.Start();
    }

    public void EnqueueMessage(string message)
    { /* snip */ }

    private void ProcessLogQueue()
    {
        foreach (var message in _messageQueue.GetConsumingEnumerable())
        {
            Console.WriteLine(message);
        }
    }

    public void Dispose()
    {
        _messageQueue.CompleteAdding();
        _outputThread.Join(millisecondsTimeout: 1500);
    }
}
Run Code Online (Sandbox Code Playgroud)

处置后,BlockingCollection完成,这可以防止更多添加, 直到两个条件都为真时_messageQueue.GetConsumingEnumerable()才会发生yield break;

  • 添加完成了吗?
  • 队列是否为空?

这意味着在返回或达到毫秒超时_outputThread之前不会终止。ProcessLogQueue1500

将此与使用的新实现进行比较Queue<T>。再次极其简化以表达要点。

public class ConsoleLoggerProcessor : IDisposable
{
    private readonly Thread _outputThread;
    private readonly Queue<string> _messageQueue = new();
    private bool _isAddingCompleted;

    public ConsoleLoggerProcessor()
    {
        _outputThread = new Thread(ProcessLogQueue) { IsBackground = true };
        _outputThread.Start();
    }

    public void EnqueueMessage(string message)
    { /* snip */ }

    private void ProcessLogQueue()
    {
        while (_isAddingCompleted)
        {
            if (_messageQueue.Count > 0)
            {
                Console.WriteLine(_messageQueue.Dequeue());
            }
        }
    }

    public void Dispose()
    {
        _isAddingCompleted = true;
        _outputThread.Join(millisecondsTimeout: 1500);
    }
}
Run Code Online (Sandbox Code Playgroud)

当这个实现被处理时,标志_isAddingCompleted被设置为true,并且紧密循环ProcessLogQueue基本上立即终止。在实际实现中,存在一些同步障碍,因此ProcessLogQueue不会将核心固定在 100%。

所以归结起来就是:

以前的记录器最多需要 1.5 秒来刷新队列中剩余的内容。阻止应用程序退出最多 1.5 秒,因为Thread.Join会阻止调用线程(主线程)直到其终止。

当前的实现不会等待消息被刷新。它立即退出。

  • 我看到已经创建了一个问题:https://github.com/dotnet/runtime/issues/79812 (3认同)