过去排查线上问题时,我经常面对几百MB的文本日志文件,一条条往上翻,靠关键词碰运气。那时候最大的痛点不是日志的量,而是日志里根本没有可被程序理解的结构——时间、级别、消息混在一行字符串里,想按某个字段聚合统计几乎不可能。后来换成 Serilog 之后,一批日志从一种人读的文本,变成了有字段、有类型、可查询的事件流,排查问题的效率完全不一样了。这篇内容想把 Serilog 的用法和背后的设计逻辑系统地梳理一遍,给正在选型或准备改造日志方案的读者一份能直接上手的参考。
Serilog 是 .NET 生态里最流行的结构化日志库之一,核心思路是把“记录日志”这件事从拼字符串变成填充事件:每条日志是一个事件,带时间戳、日志级别、消息模板,以及任意多个附加属性。它设计了一套接收器(Sink)体系,Console、文件、数据库、Seq、Elasticsearch,甚至第三方监控平台,都有现成的适配器,配置好之后一行代码不用改就能切换输出目的地。对于从传统日志方案迁移过来的团队,Serilog 的学习成本很低,但它解决的问题比传统方案多一个维度——日志不再只是一堆文本,而是可以被查询、聚合和告警的数据源。
这篇文章适合正在用或者准备用 Serilog 的 .NET 开发者,也适合那些日志量上来之后开始头疼检索和排障的团队。
1. 为什么传统文本日志在生产环境越来越不够用
1.1 过去的日志排查:从文本里大海捞针
我刚工作那几年,团队用的还是 log4net 加文本文件的方式。日志格式大概是这样的:
code复制2024-06-18 14:23:45,678 ERROR [OrderService] 订单 20240618142345123 创建失败:库存不足
看起来信息都有,但一旦日志量变大,问题就暴露得很明显:
- 想按“订单号”查所有相关日志,只能靠正则匹配,而且匹配出来的结果还得自己拼上下文。
- 想统计某个时间段内有多少条 ERROR,得先把文件读了,再用脚本或者人工数。
- 想查某个用户 ID 对应的全部操作轨迹,日志里根本没有独立字段,全靠关键字碰运气。
- 想给日志接告警系统,得先做一套解析文本的逻辑,每种格式都要写一种解析器。
这不是工具的问题,而是文本日志的结构化程度太低导致的必然结果。真正的问题是:我们在记录日志的那一刻,其实已经拿到了所有上下文数据(订单号、用户 ID、耗时、堆栈),但所有信息都被编程习惯性地拼进了一个字符串,机器再想拿回来,就只能靠猜。
1.2 结构化日志到底改变了什么
Serilog 的结构化日志模型,可以简单理解成:把“日志内容”和“日志数据”分开了。
传统的日志写的是:
csharp复制_logger.Error($"订单 {orderId} 创建失败:{reason}");
Serilog 推荐写的是:
csharp复制_logger.Error("订单 {OrderId} 创建失败:{Reason}", orderId, reason);
从人的角度,看到的输出差不多,还是“订单 xxx 创建失败:xxx”。但从系统的角度,OrderId 和 Reason 在内部是作为独立属性保存的。如果接收端支持结构化存储(比如 Seq、Elasticsearch、ClickHouse 这类系统),那这两个字段就可以被索引、筛选、聚合。
这意味着什么?意味着你不再需要为了“查日志”这件事去解析文本了。你要查的不再是“哪个文件里有这个订单号”,而是“这个订单号在哪个时间段触发过哪些事件”。后者在结构化日志里是一条 SQL 或一句查询表达式的事。
这个模型的另一个好处是模板复用。传统方案下,同一类错误散落各处,写法可能不统一,有的带订单号,有的漏掉。Serilog 的模板是在代码里固定的,所有地方写同一个模板,出来的日志就必然是同一个结构。这保证了日志数据的“模式一致性”,在数据分析和监控场景下非常重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先跑起来:最小配置和两种接入方式
2.1 最低成本的接入路径
Serilog 的接入成本远比很多人想象的低。对一个普通控制台应用来说,装上三个 NuGet 包就能用:
bash复制dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
然后在程序入口配置:
csharp复制Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("应用启动,环境:{Environment}", "Production");
