dotnet add package NLog
在项目根目录创建 NLog.config 文件,用于定义日志目标和规则

右键 NLog.config 文件,选择属性,将复制到输出目录设置为
如果较新则复制。

| 父标签 | 子标签(支持配置多个) |
|---|---|
<targets/> 定义日志的目标/输出 |
<target/> |
<rules/> 定义日志的路由规则 |
<logger/> |
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwExceptions="false"
internalLogLevel="Warn"
internalLogFile="internal-nlog.log">
<!-- 全局变量(可选) -->
<variable name="logDir" value="${basedir}/logs" />
<variable name="appName" value="MyApp" />
<!-- 目标(Targets)定义 -->
<targets>
<!-- 1. 控制台输出(适合开发调试) -->
<target name="console" xsi:type="Console"
layout="${longdate} | ${level:uppercase=true:padding=5} | ${logger} | ${message} ${exception:format=tostring}" />
<!-- 2. 文件输出 - 按天归档,自动删除7天前日志 -->
<target name="file" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="${logDir}/${appName}_${shortdate}.log"
layout="${longdate} | ${level:uppercase=true:padding=5} | ${logger} | ${message} ${exception:format=tostring}"
archiveEvery="Day"
archiveNumbering="Rolling"
archiveFileName="${logDir}/archives/${appName}_{#}.log"
archiveDateFormat="yyyy-MM-dd"
maxArchiveDays="7"
enableFileDelete="true"
createDirs="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
<!-- 3. 错误日志专用文件(包含异常堆栈) -->
<target name="errorFile" xsi:type="AsyncWrapper">
<target xsi:type="File"
fileName="${logDir}/${appName}_error_${shortdate}.log"
layout="${longdate} | ${level:uppercase=true} | ${message} ${exception:format=tostring:maxInnerExceptionLevel=5}"
archiveEvery="Day"
maxArchiveDays="7"
enableFileDelete="true"
createDirs="true" />
</target>
</targets>
<!-- 规则(Rules)定义:决定哪些 Logger 名称、哪些级别,写入哪些目标 -->
<rules>
<!-- 记录所有级别的日志到控制台(开发环境用) -->
<logger name="*" minlevel="Trace" writeTo="console" />
<!-- 记录Info及以上级别到普通日志文件 -->
<logger name="*" minlevel="Info" writeTo="file" />
<!-- 记录Error及以上级别到错误日志文件 -->
<logger name="*" minlevel="Error" writeTo="errorFile" />
<!-- 排除特定命名空间的冗余日志(可选) -->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
</rules>
</nlog>
以上配置,生成的文件结构示例
logs/
├── MyApp_2025-11-19.log # 当前日志文件
└── archives/
├── MyApp_2025-11-12.log # 7天前的归档文件(已自动删除)
└── MyApp_2025-11-18.log # 昨日归档文件
logs/
├── MyApp_error_2025-11-19.log # 当前错误日志
<nlog> 节点)
| 属性 | 说明 | 示例 |
|---|---|---|
| autoReload | 修改配置后自动重载 | autoReload=“true” |
| throwExceptions | 内部错误是否抛出异常(生产环境建议设为 false) | throwExceptions=“false” |
| internalLogLevel | NLog内部日志级别 | internalLogLevel=“Error” |
| internalLogFile | NLog内部日志路径 | internalLogFile=“c:/nlog-internal.log” |
<targets> 节点)
| 属性 | 作用 | 示例 |
|---|---|---|
| targets | 中定义了日志输出的方式以及输出格式 |
<targets> <target> <target/> </targets> |
| xsi:type | 指定输出方式。可选值:
Console表示输出到控制台,
File表示输出到文件 | xsi:type=“File” |
| name | rules 节点下的 logger 节点中,writeTo 指定的名称。即写入的目标要使用那个日志模板进行输出,就是通过name进行关联 | name=“file” |
| fileName | 输出的文件路径和文件名称,${basedir}表示程序运行目录,可以自行指定 | ${basedir}/logs/app.log |
| layout | 指定了日志输出的格式(模板) | ${longdate} ${level} ${message} |
| maxArchiveFiles | 最大归档文件数(可选) | maxArchiveFiles=”7“(只保留7个文件) |
| archiveFileName | 备份文件路径和名称(可选) | archive/log.{#}.txt |
| createDirs | 是否自动创建备份目录(可选) | createDirs =”true“ |
| concurrentWrites | 允许并发写入 | concurrentWrites=“true” |
| enableFileDelete | 启用删除,设置为True | enableFileDelete=“true” |
| archiveEvery | 归档周期 | archiveEvery=“Day”(按天) |
| archiveAboveSize | 触发归档的文件大小(字节) | archiveAboveSize=“5242880”(5MB) |
| keepFileOpen | 用于控制日志文件是否保持持续打开状态(设置false,每次写入后关闭文件,允许其他进程操作日志文件;若设置为true,虽然能提高日志写入性能,但文件可能被长期锁定,导致其他进程无法访问) | keepFileOpen=“false” |
2.1 如果要设置异步的写入,
targets 节点下的
target需要定义2级
例如:
<targets>
<target name="onlyfile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="Onlylogs/OnlyFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveDays="7"
archiveAboveSize="5242880"
enableFileDelete="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
</targets>
异步目标配置
(xsi:type="AsyncWrapper")
| 属性 | 说明 | 示例 |
|---|---|---|
| queueLimit | 异步队列容量 | queueLimit=“5000” |
| overflowAction | 队列满时处理策略 | overflowAction=“Discard”(丢弃新日志) |
<rules> 节点)
<rules>
<logger name="*" minlevel="Info" writeTo="logfile" />
<logger name="*" minlevel="Warn" writeTo="warnfile" />
<logger name="*" minlevel="Error" writeTo="errfile" />
</rules>
| 属性 | 说明 | 示例 |
|---|---|---|
| name | 日志记录器名称(支持通配符) | name=“*”(所有记录器) |
| minlevel | 最低日志级别 | minlevel=“Debug” |
| writeTo | 关联的目标名称(
<target>节点的name属性) | writeTo=“logfile” |
| final | 匹配后终止后续规则 | final=“true” |
rules 规则在NLog的配置中,minlevel属性不是“排除低于该级别的日志”,而是“包含等于或高于该级别的日志”。如上面的配置会导致日志被重复写入多个目标文件,具体规则如下:
| rules规则配置 | 匹配 |
|---|---|
<logger name="*" minlevel="Info" writeTo="logfile"/> | 记录所有Info及以上级别(Info、Warn、Error、Fatal)到 logfile |
<logger name="*" minlevel="Warn" writeTo="warnfile"/> | 记录所有Warn及以上级别(Warn、Error、Fatal)到 warnfile |
<logger name="*" minlevel="Error" writeTo="errfile"/> | 记录所有Error及以上级别(Error、Fatal)到 errfile |
实际测试效果示例
| 日志级别 | 写入目标文件 | 原因 |
|---|---|---|
| Info | logfile | 匹配第一条规则的minlevel=“Info” |
| Warn | logfile + warnfile | 匹配第一条(Info)和第二条(Warn)规则 |
| Error | logfile + warnfile + errfile | 匹配所有三条规则 |
总结上面配置意思:
当我们在程序中实例化NLog.Logger,例如:
NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
然后进行日志记录的时候,例如:
logger.Warn("我的第一条日志")
该日志就会被
rules 规则 第一条
minlevel="Info"和第二条
minlevel="Warn" 所匹配到。由上匹配规则表可以看出,
minlevel="Info"能匹配到Warn级别的日志,
minlevel="Warn"也能匹配到 Warn级别的日志。所以当前打印的日志,就会被2个规则匹配,从而记录到
writeTo="logfile" 和
writeTo="warnfile" 这2个日志文件当中。
在C# 代码中,使用的案例
如需通过依赖注入的方式进行使用,需先安装相关扩展包。
本节内容不涉及该使用方式
dotnet add package NLog.Extensions.Logging
dotnet add package NLog.Web.AspNetCore
using NLog;
public class MyService
{
private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();
public void DoWork()
{
Logger.Trace("Trace message");
Logger.Debug("Debug message");
Logger.Info("Application started");
try
{
// ...
throw new InvalidOperationException("Oops");
}
catch (Exception ex)
{
Logger.Error(ex, "Unhandled exception in DoWork");
}
}
}
如果我希望每个日志级别仅写入对应文件,则需调整规则顺序并添加
final="true"终止后续匹配:
<rules>
<!-- 优先匹配高优先级日志,匹配后终止后续规则 -->
<logger name="*" minlevel="Error" writeTo="errfile" final="true"/>
<logger name="*" minlevel="Warn" writeTo="warnfile" final="true"/>
<logger name="*" minlevel="Info" writeTo="logfile"/>
</rules>
在NLog中实现“每个日志级别仅写入对应文件”的需求,除了使用这样就实现了,Warn 级别的日志,只会出现在 warnfile 的日志文件中,而不会出现在logfile 的日志文件中。
final="true"阻断后续规则匹配外,还有以下三种核心方案,可根据场景灵活选择:
通过在
<logger>规则中直接添加
condition属性实现精准过滤,无需依赖
final属性。例如:
<rules>
<!-- Info日志仅写入logfile -->
<logger name="*" minlevel="Info" writeTo="logfile"
condition="level == LogLevel.Info" />
<!-- Warn日志仅写入warnfile -->
<logger name="*" minlevel="Warn" writeTo="warnfile"
condition="level == LogLevel.Warn" />
<!-- Error日志仅写入errfile -->
<logger name="*" minlevel="Error" writeTo="errfile"
condition="level == LogLevel.Error" />
</rules>
优势:
规则自包含,无需调整顺序或依赖
final避免低级别日志意外匹配高级别规则(如原配置中Warn日志会同时匹配Info规则)
在
<target>定义中通过
layout或
filters实现级别过滤,例如:
<targets>
<!-- logfile目标仅接收Info日志 -->
<target name="logfile" xsi:type="File"
layout="${longdate}|${message}"
fileName="logs/info.log">
<filter type="Condition" condition="level == LogLevel.Info" />
</target>
<!-- warnfile目标仅接收Warn日志 -->
<target name="warnfile" xsi:type="File"
layout="${longdate}|${message}"
fileName="logs/warn.log">
<filter type="Condition" condition="level == LogLevel.Warn" />
</target>
</targets>
<rules>
<!-- 单条规则覆盖所有级别 -->
<logger name="*" minlevel="Info" writeTo="logfile,warnfile,errfile" />
</rules>
优势:
规则配置极简(单规则覆盖全局)目标过滤器可复用(如多个规则指向同一目标时)通过
<logger>的
name属性结合命名空间隔离,实现物理日志源隔离:
l<rules>
<!-- 专用Info日志记录器 -->
<logger name="InfoLogger.*" minlevel="Info" writeTo="logfile" final="true" />
<!-- 专用Warn日志记录器 -->
<logger name="WarnLogger.*" minlevel="Warn" writeTo="warnfile" final="true" />
<!-- 专用Error日志记录器 -->
<logger name="ErrorLogger.*" minlevel="Error" writeTo="errfile" final="true" />
</rules>
需要在代码中需显式使用对应记录器:
// C#代码示例
var infoLogger = NLog.LogManager.GetLogger("InfoLogger.Namespace");
var warnLogger = NLog.LogManager.GetLogger("WarnLogger.Namespace");
var errorLogger = NLog.LogManager.GetLogger("ErrorLogger.Namespace");
适用场景:
需要严格隔离不同级别的日志来源配合依赖注入实现日志记录器自动注入要求:对于不同业务类型的处理逻辑生成的日志,需分别存储到对应的独立日志目录中。所有日志输出必须严格保持时序性,确保在多线程环境下不会出现日志内容交叉打印的情况(即每个线程产生的日志条目始终按照程序逻辑的执行顺序完整输出)。
Nlog.config 配置文件
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c: emp
log-internal.log">
<targets>
<!-- only设备日志配置, -->
<target name="onlyfile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="Onlylogs/OnlyFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveDays="7"
archiveAboveSize="5242880"
enableFileDelete="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
<!-- line设备日志配置, -->
<target name="linefile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="Linelogs/LineFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveDays="7"
archiveAboveSize="5242880"
enableFileDelete="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
<!-- stacker设备日志配置, -->
<target name="stackerfile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="Stackerlogs/StackerFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveDays="7"
archiveAboveSize="5242880"
enableFileDelete="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
<!-- other日志配置, -->
<target name="otherfile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="Otherlogs/OtherFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveDays="7"
archiveAboveSize="5242880"
enableFileDelete="true"
keepFileOpen="false"
concurrentWrites="true" />
</target>
<target name="errfile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<!-- err_file 配置 -->
<target xsi:type="File"
fileName="Errorlogs/ErrorFile_${shortdate}.log"
layout="${longdate}|${level:uppercase=true}|${message}${onexception:${newline}${exception:format=tostring}}${newline}"
archiveEvery="Day"
archiveNumbering="Rolling"
archiveAboveSize="5242880"
enableFileDelete="true"
maxArchiveDays="7"
keepFileOpen="false"
concurrentWrites="true" />
</target>
</targets>
<rules>
<!-- 专用Only设备日志记录器 -->
<logger name="OnlyInfoLogger.*" minlevel="Info" writeTo="onlyfile" final="true" />
<!-- 专用Line日志记录器 -->
<logger name="LineInfoLogger.*" minlevel="Info" writeTo="linefile" final="true" />
<!-- 专用Stacker日志记录器 -->
<logger name="StackerInfoLogger.*" minlevel="Info" writeTo="stackerfile" final="true" />
<!-- 专用Error日志记录器 -->
<logger name="ErrorLogger.*" minlevel="Error" writeTo="errfile" final="true" />
<!-- 其他日志记录器,默认 -->
<logger name="OtherLogger.*" minlevel="Info" writeTo="otherfile" final="true" />
</rules>
</nlog>
ExceptionLog.cs 文件主要用于记录日志信息。它会按日志生成顺序将记录写入队列,并在逻辑调用结束时,通过自动触发 Dispose 方法实现日志的批量写入操作。
public enum MessageLevel
{
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
}
public class ExceptionLog : Exception, IDisposable
{
private struct MyStruct
{
public MessageLevel Level;
public DateTime CreateDate;
public string? MethodPath;
public string Message;
}
private readonly DateTime _createDate;
private readonly List<MyStruct> _myLogs;
private readonly string? _method_name;
// 每个实例的 logger 名称与实例 ID(用于区分多个实例)
private readonly string _loggerName;
private readonly Guid _instanceId;
// private static readonly NLog.Logger _logger = LogManager.GetCurrentClassLogger();
// 用于序列化日志块并保持顺序,队列元素包含 loggerName 与 instanceId
private static readonly BlockingCollection<(string LoggerName, LogLevel Level, string Message, Guid InstanceId)> _logQueue = new();
private static readonly Task _logWorker;
static ExceptionLog()
{
// 启动按顺序写入日志的后台工作程序
_logWorker = Task.Run(() =>
{
try
{
foreach (var item in _logQueue.GetConsumingEnumerable())
{
try
{
// 动态根据 loggerName 获取 Logger,并在消息前附加 InstanceId 以便区分来源实例
var logger = LogManager.GetLogger(item.LoggerName);
var messageWithId = $"[{item.InstanceId}] {item.Message}";
logger.Log(item.Level, messageWithId);
}
catch (Exception ex)
{
try { Debug.WriteLine($"NLog工作线程写入失败: {ex}"); } catch { }
}
}
}
catch (Exception ex)
{
try { Debug.WriteLine($"NLog线程已终止: {ex}"); } catch { }
}
});
}
// 新增构造函数:允许传入 loggerName
public ExceptionLog(string? loggerName = null)
{
_createDate = DateTime.Now;
_myLogs = new List<MyStruct>();
_method_name = new StackTrace().GetFrame(1)?.GetMethod()?.DeclaringType?.FullName;
_loggerName = string.IsNullOrWhiteSpace(loggerName) ? "ErrorLogger" : loggerName!;
_instanceId = Guid.NewGuid();
}
public void AddLog(string message, MessageLevel level = MessageLevel.Debug)
{
_myLogs.Add(new MyStruct { Level = level, CreateDate = DateTime.Now, Message = message, MethodPath = new StackTrace().GetFrame(1)?.GetMethod()?.DeclaringType?.FullName });
}
public void AddLog(Exception exception, MessageLevel level = MessageLevel.Error)
{
_myLogs.Add(new MyStruct { Level = level, CreateDate = DateTime.Now, Message = exception.Message, MethodPath = new StackTrace().GetFrame(1)?.GetMethod()?.DeclaringType?.FullName });
}
public override string Message => _myLogs.LastOrDefault().Message;
private void WriteLog()
{
if (_myLogs.Count == 0) return;
var maxLevel = _myLogs.Select(x => x.Level).Max();
var sb = new StringBuilder();
sb.AppendLine($"[开始] {_createDate.ToDateTimeMillisecond()} {_method_name}");
_myLogs.ForEach(a =>
{
if ( maxLevel == MessageLevel.Error || (maxLevel != MessageLevel.Error && a.Level != MessageLevel.Debug))
{
sb.AppendLine($"[{a.Level}] {a.CreateDate.ToDateTimeMillisecond()} {a.Message}");
}
});
sb.AppendLine($"[结束] {DateTime.Now.ToDateTimeMillisecond()} {_method_name}");
var message = sb.ToString();
var nlogLevel = MapToNLogLevel(maxLevel);
// 对聚合消息进行排队,以保留块顺序并避免交织
try
{
_logQueue.Add((_loggerName, nlogLevel, message, _instanceId));
}
catch (Exception ex)
{
try { Debug.WriteLine($"日志入队失败: {ex}"); } catch { }
}
}
private static LogLevel MapToNLogLevel(MessageLevel level)
{
return level switch
{
MessageLevel.Debug => LogLevel.Debug,
MessageLevel.Info => LogLevel.Info,
MessageLevel.Warning => LogLevel.Warn,
MessageLevel.Error => LogLevel.Error,
_ => LogLevel.Info,
};
}
public void Dispose()
{
try
{
WriteLog();
_myLogs.Clear();
}
catch (Exception ex)
{
try { Debug.WriteLine($"调用Dispose批量写日志抛出异常,错误原因: {ex}"); } catch { }
}
}
}
逻辑调用
using (var exceptionLog = new ExceptionLog("OnlyInfoLogger.Namespace"))
{
var watch = Stopwatch.StartNew();
try
{
exceptionLog.AddLog($"Only设备【{equ.EquipmentCode}】开始轮询。逻辑处理计时开始:", MessageLevel.Info);
PolicyLogic(equ,exceptionLog).ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (Exception ex)
{
exceptionLog.AddLog($"Only设备[{equ.EquipmentCode}]轮询异常:{ex.Message}",MessageLevel.Info);
}
finally
{
mutex.ReleaseMutex();
watch.Stop();
exceptionLog.AddLog($"Only设备【{equ.EquipmentCode}】轮询结束。逻辑处理计时结束,耗时:{watch.ElapsedMilliseconds}", MessageLevel.Info);
}
}
在ExceptionLog中,需明确传入并使用相应的日志记录器
最后效果:
