1. 日志系统不是“打印字符串”那么简单
从入行写第一行 Console.WriteLine 开始,我们就和日志打上了交道。但很多人工作三五年之后,对日志的理解还停留在“用 Log.Info 把关键节点打出来,出问题了再去翻文件”这个层面。有一次我给一个线上事故排查了一整晚,最后发现是某个第三方接口的超时时间设置错了,而当时唯一能证明时序的日志,因为日志级别配置错误,压根没写进文件。那晚之后我花了大量时间重新梳理 .NET 生态里的日志体系,今天这篇就把这些年积累下来的使用技巧一次性说透。
这篇文章写给谁?给那些已经能用 ILogger<T> 写基础日志、但想更进一步理解日志级别设计、结构化日志、上下文串联、滚动策略、性能开销、日志采集上送等进阶问题的 .NET 开发者。无论你用的是 ASP.NET Core、Worker Service 还是控制台应用,只要你的程序真的要上生产环境,这篇内容都值得认真看一遍。
先说一个核心观念:日志系统不是一个“写字符串到文件”的工具,而是你观察程序运行时状态的“眼睛”。怎么设计日志事件的形状、怎么控制日志的生产成本、怎么在千万条日志里快速找到你关心的那一条——这些才是日志系统真正要解决的问题。而 .NET 从微软官方到第三方社区,其实已经给了我们一套相当完整的答案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础库选择:官方 ILogger 与第三方框架的取舍
很多初学者会纠结一个问题:我到底该用 NLog、log4net 还是 Serilog?其实 .NET 生态这几年的趋势已经非常明显——微软官方在 ASP.NET Core 里内置的 Microsoft.Extensions.Logging 抽象层已经成为事实标准。它不是一个具体的日志实现,而是一套统一的日志门面接口。你可以在上面挂 NLog、Serilog,也可以挂官方自己的控制台、事件日志、Debug 输出提供程序。
2.1 不急着上结构化,先从官方 ILogger 入手
我做项目有一个原则:能用官方自带能力解决的,绝不额外引第三方包。ILogger<T> 配合内置的 Console、EventLog、Debug 提供程序,已经能覆盖中小型项目 80% 的需求。对于内部管理系统、工具类应用,完全够用。
那为什么社区里这么多人推崇 Serilog?因为它把“结构化日志”这个概念做到了极致。所谓结构化日志,就是不再把日志当成一行字符串,而是当成一个携带多个字段的事件。比如:
code复制{"Timestamp":"2025-06-15T10:30:00.123Z","Level":"Error","Message":"订单处理失败","OrderId":"SO-20250615-001","UserId":12345,"Exception":"..."}
这种格式的价值在于,从 JSON 字符串 OrderId 字段里去检索,比从一长串文本里去正则匹配快得多、准得多。如果你将来要把日志接入 Elasticsearch、Loki 这类日志平台,结构化日志基本是必需品。
2.2 Serilog 快速上手与配置演示
Serilog 是 .NET 生态下最流行的结构化日志库,配置灵活、收集器多。我通常这样搭建:
csharp复制// 安装: dotnet add package Serilog.AspNetCore
// 以及: dotnet add package Serilog.Sinks.File
// 以及: dotnet add package Serilog.Sinks.Console
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Error)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "OrderService")
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/order-service-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 100 * 1024 * 1024,
rollOnFileSizeLimit: true,
shared: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
var app = builder.Build();
app.Run();
MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 这行很多人不理解为什么要写。因为 ASP.NET Core 框架内部有大量 Information 级别的日志,比如每个请求的开始和结束、路由匹配、中间件执行等。新手全开的话,文件里会瞬间被框架日志淹没。生产环境我建议对 Microsoft 命名空间用 Warning 起跳,对 Microsoft.EntityFrameworkCore 用 Error 起跳,这是经过实践验证比较合理的配置组合。
2.3 NLog 与 Serilog 的选型对比
NLog 的优势在于老牌稳定、性能好、XML 配置能力极强,很多维护中的 .NET Framework 项目里大量使用。如果你在维护老项目,NLog 完全没问题。但如果是个新项目,并且你有日志平台接入需求,我建议直接用 Serilog——它的 WriteTo.Seq()、WriteTo.Elasticsearch() 等扩展开箱即用,和 .NET 现代开发风格的契合度更高。
| 对比维度 | Serilog | NLog | 官方 ILogger |
|---|---|---|---|
| 结构化支持 | 原生,极其优秀 | 支持但配置略复杂 | 无,需自行 Json |
| 性能 | 优秀 | 优秀 | 较好 |
| 配置方式 | 代码优先,JSON 可选 | XML 传统,JSON 可用 | 配置灵活,代码为主 |
| 社区活跃度 | 高 | 中 | 官方维护 |
| 上手难度 | 中低 | 中 | 低 |
| 老项目兼容 | 一般 | 优秀 | 一般 |
我个人的选择标准很简单:老项目维护用 NLog,新项目一律 Serilog。别在这个上面反复纠结,日志库的切换成本远比想象中高,选定了就别轻易换。
3. 结构化日志设计的核心原则
结构化日志不是把 Log.Information($"User {userId} bought {productId}") 改成 Log.Information("User {UserId} bought {ProductId}", userId, productId) 就完事了。它牵涉到的是一整套日志事件的建模方法。
3.1 模板消息里的占位符,别当字符串拼接用
这是 .NET 日志系统里最高频的一个坑。看我下面两个写法的区别:
csharp复制// 错误示范(实际是字符串插值,不是结构化日志)
Log.Information($"用户 {userId} 购买了商品 {productId}");
// 正确示范(占位符会被 Serilog 解析成结构化字段)
Log.Information("用户 {UserId} 购买了商品 {ProductId}", userId, productId);
错误写法在日志平台上只会看到一条完整字符串,没法针对 `UserI
