说实话,日志这玩意儿,写的时候没人觉得重要,等线上出问题的时候,谁都抓瞎。干.NET开发这些年,我见过太多线上故障是从"看不了日志"开始的:日志打了一堆,控制台刷屏,可真要查一条具体请求的执行链路,翻半天文件找不到;服务一多,逐台机器去tail日志,眼睛都快看瞎了。日志系统这个听起来基础得不能再基础的东西,其实是很多项目的隐形短板。这篇博文,我想把自己在.NET生态里折腾日志系统的完整经验整理出来,从选型、结构化、集中采集、性能开销到各种让我熬夜排查的坑,一次性说清楚,希望给正在用.NET做服务端开发、又对日志没什么头绪的朋友一些真正能落地的参考。
1. 选型:.NET日志体系里那几套方案,边界到底在哪
1.1 Microsoft.Extensions.Logging 只是门面,不是日志框架
很多新手(包括早年的我)都以为ASP.NET Core项目里那个ILogger<xxx>就是全部了,往控制台里打几条Information就算"接好日志系统"了。这个认知要纠正一下:Microsoft.Extensions.Logging(下面叫MEL)本质是一套日志门面接口,它本身不负责把日志写到任何地方——控制台输出之所以开箱即用,只是因为默认注册了一个ConsoleProvider。
MEL只提供抽象层、级别过滤、格式化和Provider机制。真正干活的是Provider:默认的Console、Debug、EventLog、EventSource,以及第三方提供的Serilog Provider、NLog Provider。也就是说,你用ILogger写了代码,底层到底是写到文件、数据库还是日志采集平台,全看注册了哪个Provider。理解这一点很重要,它决定了你的业务代码可以保持对具体日志实现零依赖。
1.2 Serilog、NLog 这类完整框架,多出来的自由不是白给的
如果项目只是验证个想法、跑几个简单接口,默认的Console输出确实够用。但凡是上了规模、需要排查历史问题、需要从海量日志里捞线索的,就必须做三件事:结构化输出、按天/按大小滚动落盘、把日志送往日志平台。这套能力MEL默认没有,而Serilog和NLog恰好都提供。
Serilog的强项是消息模板和结构化日志,它把日志事件当成强类型数据处理,默认输出JSON也可以很友好;配合sink生态,写文件、接Elasticsearch、接Grafana Loki都非常顺。NLog的长处在于配置灵活、性能也不错,而且老项目里用NLog的历史包袱比较重。两个框架都很成熟,但如果条件允许,我个人更倾向Serilog——它在.NET社区的活跃度、文档质量和和MEL的集成顺滑度确实有优势。
下面这张表是我给团队讲选型时常用的对照:
| 方案 | 适合场景 | 结构化输出 | 文件滚动 | 日志平台对接 | 学习成本 |
|---|---|---|---|---|---|
| MEL + Console | 开发调试、临时脚本 | 无 | 无 | 无 | 极低 |
| MEL + 文件Provider | 单机小工具 | 弱 | 部分支持 | 无 | 低 |
| NLog | 成熟项目、老系统改造 | 支持 | 支持 | 插件丰富 | 中 |
| Serilog | 新项目、微服务、结构化日志优先 | 原生支持 | 支持 | sink生态极好 | 中 |
提示:选日志库不是选"最好"的,而是选"团队能长期维护"的。引入一套日志框架等于给项目增加长期依赖成本,没必要为了炫技重复造轮子,也别让NLog和Serilog混着用,到时候运维排查两边对不上,反而更痛苦。
1.3 我实际项目里的典型组合
我现在落地新项目,最常见的组合是:Serilog + MySQL/PostgreSQL业务数据,日志走文件或直接推Loki。文件用于本地快速排查,聚合平台用于线上全局检索。在ASP.NET Core里接Serilog的启动代码如下:
csharp复制builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.WriteTo.Console());
这里做了两件事:从appsettings.json读取Serilog配置,再注册LogContext。后者是Serilog的一个关键能力,后面讲链路追踪时会详细展开。如果项目里用了OpenTelemetry,还要配合WithTraceId()这类扩展,让日志里带上链路ID。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 结构化日志:从"人能看懂"到"机器能检索"
2.1 字符串拼接的日志,排查起来有多痛苦
很多老代码写日志是这么写的:
csharp复制logger.LogError($"用户 {userId} 下单失败,订单号 {orderId},错误:{ex.Message}");
这种写法在本地调试没毛病,真上了日志平台就是灾难。第一,整个日志变成了一段不可拆分的字符串,想按订单号查,只能做模糊搜索,日志一多速度直线下降;第二,如果日志平台按关键字索引,这种拼接式日志会占掉大量索引空间;第三,消息模板里变量和文本混在一起,后续想提取结构化字段做统计,基本不可能。
我接手过一个订单系统,排查某类支付失败时,运维从Elasticsearch里搜订单号,一次搜索要扫几十个索引,慢到怀疑人生。后来我把日志改成结构化输出,按订单号字段精确查询,秒出结果。这就是那一行代码带来的差别。
2.2 消息模板的正确用法:参数化,不要字符串插值
Serilog把一个日志事件拆成三部分:消息模板、参数列表、级别。样例:
csharp复制logger.Log
