在此线程中-Should NLog flush all queued messages in the AsyncTargetWrapper when Flush() is called?-我读到“ LogManager
在域卸载或进程退出时将配置设置为null”(请参阅第一个答案的“编辑”部分)。以我的理解,这应该导致所有待处理的日志条目都写入已注册的目标。但是,在用FileTarget
包裹AsyncTargetWrapper
进行测试之后,这并不成立。我已经在GitHub-https://github.com/PaloMraz/NLogMultiProcessTargetsSample上创建了一个最小的propro,其工作方式如下:
[LogLib
是一个引用.netstandard2.0
4.6.8 NuGet程序包并公开以编程方式配置NLog
目标的CompositeLogger
类的NLog
库:
public class CompositeLogger
{
private readonly ILogger _logger;
public CompositeLogger(string logFilePath)
{
var fileTarget = new FileTarget("file")
{
FileName = logFilePath,
AutoFlush = true
};
var asyncTargetWrapper = new AsyncTargetWrapper("async", fileTarget)
{
OverflowAction = AsyncTargetWrapperOverflowAction.Discard
};
var config = new LoggingConfiguration();
config.AddTarget(asyncTargetWrapper);
config.AddRuleForAllLevels(asyncTargetWrapper);
LogManager.Configuration = config;
this._logger = LogManager.GetLogger("Default");
}
public void Log(string message) => this._logger.Trace(message);
}
[LogConsoleRunner
是一个.NET Framework 4.8控制台应用程序,它使用LogLib.CompositeLogger
将指定数量的日志消息写入文件(指定为命令行参数),两次写入之间的延迟很短:
public static class Program
{
public const int LogWritesCount = 10;
public static readonly TimeSpan DelayBetweenLogWrites = TimeSpan.FromMilliseconds(25);
static async Task Main(string[] args)
{
string logFilePath = args.FirstOrDefault();
if (string.IsNullOrWhiteSpace(logFilePath))
{
throw new InvalidOperationException("Must specify logging file path as an argument.");
}
logFilePath = Path.GetFullPath(logFilePath);
Process currentProcess = Process.GetCurrentProcess();
var logger = new CompositeLogger(logFilePath);
for(int i = 0; i < LogWritesCount; i++)
{
logger.Log($"Message from {currentProcess.ProcessName}#{currentProcess.Id} at {DateTimeOffset.Now:O}");
await Task.Delay(DelayBetweenLogWrites);
}
}
}
最后,LogTest
是一个XUnit
测试程序集,其中一个测试启动十个LogConsoleRunner
实例写入同一日志文件:
[Fact]
public async Task LaunchMultipleRunners()
{
string logFilePath = Path.GetTempFileName();
using var ensureLogFileDisposed = new Nito.Disposables.AnonymousDisposable(() => File.Delete(logFilePath));
string logConsoleRunnerAppExePath = Path.GetFullPath(
Path.Combine(
Path.GetDirectoryName(this.GetType().Assembly.Location),
@"..\..\..\..\LogConsoleRunner\bin\Debug\LogConsoleRunner.exe"));
var startInfo = new ProcessStartInfo(logConsoleRunnerAppExePath)
{
Arguments = logFilePath,
UseShellExecute = false
};
const int LaunchProcessCount = 10;
Process[] processes = Enumerable
.Range(0, LaunchProcessCount)
.Select(i => Process.Start(startInfo))
.ToArray();
while (!processes.All(p => p.HasExited))
{
await Task.Delay(LogConsoleRunner.Program.DelayBetweenLogWrites);
}
string[] lines = File.ReadAllLines(logFilePath);
Assert.Equal(LaunchProcessCount * LogConsoleRunner.Program.LogWritesCount, lines.Length);
}
最后一行的Assert.Equal
总是失败,因为目标文件写的行总是少于期望的计数,即100。在我的机器上,每次运行它在96 – 99之间变化,但是它从不包含所有100行。
我的问题:我应该如何配置NLog
以确保在所有进程退出之后,所有待处理的日志条目都被写入目标日志文件?
只需致电LogManager.Shutdown()
在LogManager.Shutdown()
的末尾。它将刷新所有未决的日志事件。 Main
。
旁注:如果刷新后需要NLog,则可以使用Read more代替关机。
已经查看了示例代码,并且您有多个进程写入相同的文件名。
认为您是性能和正确性之间折衷的受害者。
[当多个进程同时写入同一文件时,则需要进行一些锁定以进行协调。默认情况下,NLog使用最兼容的模式(KeepFileOpen = false),这是来自操作系统的文件锁(适用于大多数平台)。
从操作系统进行文件锁定是不公平的,并且当有两个以上的进程写入同一文件时,该文件无法缩放。当一个进程试图打开一个文件,而另一个进程当前正在使用该文件时,将引发异常。
NLog尝试通过重试错误(concurrentWriteAttempts = 10)并随机分配重试前等待的时间来处理这些异常。这对于2个进程来说还可以,但是当您开始增加进程数时,则增加了一个进程连续10次失败的可能性。在最后一次重试之后,NLog会丢弃LogEvent(可能是您看到的内容)。
KeepFileOpen = false很慢(300次写入/秒),当与retry-logic结合使用时,它将变得非常慢。但是,当允许批处理时,通过使用AsyncWrapper几乎可以消除性能下降。但是现在,当重试计数用完时,整个批次可能会丢失。
而不是依靠操作系统文件锁,而是可以依靠NLog使用全局互斥锁进行进程间通信。通过LogManager.Flush()
和LogManager.Flush()
启用此模式。而不是300次写入/秒,然后变为100.000次写入/秒,并且锁定机制更加公平,因此无需重试。并非所有平台都支持此模式,但在Windows上的.NET 4.8(在Linux上为NetCore2)上应该能很好地工作。
另请参见:KeepFileOpen=True