3. NLog进阶:结构化日志,布局渲染器,异步日志,日志归档与滚动策略
好,咱们接着聊NLog。上一章我们把NLog的基础用法过了一遍,说白了就是能写日志了。但真正到了生产环境,你会发现光能写远远不够。日志怎么写、写什么、写到哪里、写多大,这些细节才是决定你排障效率的关键。
这一章,我带你深入NLog的几个核心进阶特性。我个人习惯把这些特性称为「生产环境三件套」:结构化日志让日志可被机器读懂,布局渲染器让日志内容更丰富,异步与归档则让日志系统稳定可靠。咱们一个一个来。
3.1 结构化日志:让日志不再是「字符串垃圾」
先问个问题:你平时怎么记录一个订单的支付信息?
// 传统写法
logger.Info($"用户{userId}支付了订单{orderId},金额{amount}元");
这种写法,说白了就是拼字符串。你看着是中文,机器看着就是一堆字符。哪天你想统计一下「今天所有支付金额大于1000元的订单」,你怎么办?用正则去日志文件里抠?那效率太低了。
结构化日志就是来解决这个问题的。它的核心思想是:把日志中的变量和消息模板分开。
// 结构化写法
logger.Info("用户{UserId}支付了订单{OrderId},金额{Amount}元", userId, orderId, amount);
看到区别了吗?大括号里的 {UserId}、{OrderId} 不再是字符串的一部分,而是命名占位符。NLog会把它们解析成键值对,输出到目标时,可以保留为结构化数据。
核心价值:结构化日志可以被日志分析系统(如Elasticsearch、Splunk)直接索引和查询。你可以在Kibana里直接搜 UserId: 12345,而不是用正则去全文检索。
我在项目中遇到过一件事:有一次线上排查一个支付超时问题,传统日志里全是「用户xxx支付失败」,根本没法聚合。后来改成结构化日志,直接在Kibana里按 PaymentStatus: Failed 一筛,再按 Amount > 10000 排序,问题瞬间定位到是某几个大额订单触发了风控。嗯,这就是结构化日志的威力。
3.2 布局渲染器(Layout Renderers):给日志加点「料」
布局渲染器,名字听着挺唬人,其实说白了就是内置的变量占位符。NLog提供了几十种内置的渲染器,让你可以在输出日志时自动填充各种上下文信息。
比如,你想在每条日志里带上当前线程ID、请求ID、机器名,你不需要手动去取,直接用渲染器就行:
<target name="file" xsi:type="File"
layout="${longdate} | ${level:uppercase=true} | ${threadid} | ${machinename} | ${message}" />
常用的布局渲染器我列个表,你直接收藏:
| 渲染器 | 说明 | 我的使用场景 |
|---|---|---|
${longdate} |
完整日期时间,如 2025-01-15 14:30:22.1234 | 所有日志的标配 |
${level} |
日志级别,可设置大小写 | 配合 uppercase=true 更醒目 |
${logger} |
记录日志的类名 | 快速定位哪个类出的问题 |
${threadid} |
当前线程ID | 排查并发问题时必用 |
${machinename} |
服务器机器名 | 多节点部署时区分日志来源 |
${aspnet-request:item=UserAgent} |
ASP.NET Core请求的User-Agent | 分析客户端类型 |
${exception:format=ToString} |
异常堆栈信息 | 记录异常时必加 |
我的小技巧:我习惯把 ${longdate} | ${level} | ${logger} | ${message}${exception:format=ToString} 作为默认布局。这样每条日志都自带时间、级别、来源,排查问题时一眼就能看出关键信息。
你还可以自定义布局渲染器。比如,我想在每条日志里带上当前登录用户的ID,可以写一个自定义渲染器。不过说实话,大部分场景下内置的已经够用了,别过度设计。
3.3 异步日志:别让日志拖慢你的业务
日志写入文件,本质上是I/O操作。如果你的业务线程直接写日志,遇到磁盘繁忙时,业务线程就得等着。你想想看,用户请求因为写日志而变慢,这合理吗?
NLog的异步日志机制,说白了就是把写日志这件事丢给后台线程去做。业务线程只管把日志消息扔进队列,然后立刻返回,继续处理业务。
配置方式很简单,用 async 包装一下你的target:
<targets>
<target name="asyncFile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="${basedir}/logs/app.log"
layout="${longdate} | ${level} | ${message}" />
</target>
</targets>
这里有几个关键参数:
- queueLimit:队列最大长度。默认10000,我一般设5000,防止内存爆掉。
- overflowAction:队列满了怎么办?有
Discard(丢弃新日志)、Block(阻塞业务线程)、Grow(扩容队列)。我建议用Discard,宁可丢几条日志,也别拖垮业务。
避坑指南:我曾经在一个高并发项目里,把 overflowAction 设成了 Block。结果业务高峰期队列满了,所有写日志的线程都被阻塞,导致请求响应时间从50ms飙升到5秒。嗯,从那以后我再也不敢用 Block 了。
另外,异步日志的写入顺序是不保证的。如果你对日志顺序有严格要求(比如审计日志),那就别用异步,或者用 AsyncWrapper 的 batchSize 参数控制批量写入。
3.4 日志归档与滚动策略:别让日志撑爆磁盘
日志如果不加控制,几天就能把磁盘写满。我见过最夸张的一次,一个项目跑了三天,日志文件占了200GB,直接把服务器搞宕机了。所以,归档和滚动策略是必须的。
NLog的滚动策略主要通过 File target 的几个属性来控制:
<target xsi:type="File"
fileName="${basedir}/logs/app.log"
archiveFileName="${basedir}/logs/archives/app.{#}.log"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveFiles="30"
archiveAboveSize="104857600" />
我来解释一下这几个参数:
- archiveEvery:按什么周期归档。可选
Day、Hour、Minute、Month。我一般用Day,每天一个归档文件。 - archiveAboveSize:单个文件超过多大就归档。单位是字节,104857600 就是100MB。建议设一个值,防止某天日志量暴增导致单个文件过大。
- maxArchiveFiles:最多保留多少个归档文件。超过这个数,最老的会被自动删除。我一般设30,也就是保留最近30天的日志。
- archiveNumbering:归档文件的编号方式。
Rolling是滚动编号,Date是按日期编号。我习惯用Rolling,配合maxArchiveFiles自动清理。
最佳实践:我建议同时设置 archiveEvery 和 archiveAboveSize。比如每天归档一次,但如果某天日志量特别大,单个文件超过100MB也会触发归档。这样双重保险,不会出现单个文件过大的情况。
还有一个容易被忽略的点:归档文件的命名规则。 {#} 是编号占位符, {#} 会自动替换成 1、2、3... 如果你用 archiveNumbering="Date",可以用 {#} 或者 {#} 来生成带日期的文件名。
举个例子,我常用的配置:
<target xsi:type="File"
fileName="${basedir}/logs/app-${shortdate}.log"
archiveFileName="${basedir}/logs/archives/app.{#}.log"
archiveEvery="Day"
archiveAboveSize="104857600"
maxArchiveFiles="30"
archiveNumbering="Rolling"
concurrentWrites="true"
keepFileOpen="false" />
这里我加了 concurrentWrites="true" 和 keepFileOpen="false"。前者允许多个进程同时写同一个日志文件(多实例部署时有用),后者是每次写完就关闭文件句柄,防止文件被锁定。
我的习惯:我会把归档文件单独放在 archives 子目录里,和当前日志文件分开。这样查看当前日志时不会被归档文件干扰,而且归档目录可以单独设置清理策略。
3.5 把这些串起来:一个生产级配置示例
好了,上面讲的这些,咱们串成一个完整的配置。这是我个人在多个项目中验证过的模板,你直接拿去改改就能用:
<?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">
<targets>
<target name="asyncFile" xsi:type="AsyncWrapper"
queueLimit="5000" overflowAction="Discard">
<target xsi:type="File"
fileName="${basedir}/logs/app-${shortdate}.log"
archiveFileName="${basedir}/logs/archives/app.{#}.log"
archiveEvery="Day"
archiveAboveSize="104857600"
maxArchiveFiles="30"
archiveNumbering="Rolling"
concurrentWrites="true"
keepFileOpen="false"
layout="${longdate} | ${level:uppercase=true} | ${logger} | ${threadid} | ${message} ${exception:format=ToString}" />
</target>
<target name="console" xsi:type="Console"
layout="${longdate} | ${level:uppercase=true} | ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Info" writeTo="asyncFile" />
<logger name="*" minlevel="Warn" writeTo="console" />
</rules>
</nlog>
这个配置做了几件事:
- 所有 Info 及以上级别的日志,异步写入文件
- Warn 及以上级别的日志,同时输出到控制台(开发时方便看)
- 文件每天归档,单个文件超过100MB也归档
- 最多保留30个归档文件,自动清理最老的
- 每条日志都包含时间、级别、类名、线程ID、消息和异常信息
嗯,这一章的内容就到这里。结构化日志、布局渲染器、异步日志、归档滚动,这四个点你掌握好了,NLog在生产环境里基本就稳了。下一章,咱们聊聊如何把日志和全链路监控结合起来,让日志真正成为你排查问题的「眼睛」。