In this thread - Should NLog flush all queued messages in the AsyncTargetWrapper when Flush() is called? - I am reading that „LogManager
sets the config to null on domain unload or process exit“ (see the Edit section in the 1st answer). In my understanding, this should cause all the pending log entries be written to registered targets. However, after testing with FileTarget
wrapped with AsyncTargetWrapper
, this does not hold true. I have created a minimal repro on GitHub - https://github.com/PaloMraz/NLogMultiProcessTargetsSample, which works as follows:
LogLib
is a .netstandard2.0
library referencing NLog
4.6.8 NuGet package and exposing a CompositeLogger
class which programmatically configures the NLog
targets:
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
is a .NET Framework 4.8 console app that uses LogLib.CompositeLogger
to write specified number of log messages to a file (specified as a command line argument) with a short delay between writes:
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);
}
}
}
Finally, LogTest
is a XUnit
test assembly with one test launching ten LogConsoleRunner
instances writing to the same log file:
[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);
}
The Assert.Equal
on the last line always fails, because the target file has always less lines written than the expected count, which is 100. On my machine, it varies with each run between 96 – 99, but it never contains all 100 lines.
My question: how should I configure NLog
to make sure that after all processes exit, all pending log entries are written to the target log file?