Log.Information 必须带 {Key} 占位符,否则日志退化为纯文本,丧失结构化能力;占位符名严格区分大小写、需与参数语义一致,对象应使用 {@Obj} 触发序列化,且配置须在 builder.Build() 前完成。

Log.Information 不是字符串拼接,而是结构化日志事件的起点——只要你用 {Key} 占位符,Serilog 就会把对应参数作为独立字段写入日志,后续能被 Seq、Elasticsearch 或日志平台直接过滤、聚合、统计。
Log.Information 为什么必须带 {Key} 占位符
- 不写占位符(比如
Log.Information("用户 " + userId + " 登录"))会导致日志变成纯文本,丢失所有结构信息 - 占位符名和传入参数顺序必须严格对应:
Log.Information("用户 {UserId} 登录 {From}", userId, from)中,UserId和From是字段名,不是变量名,大小写敏感 - 如果传入对象,推荐用
{@User}(带@前缀)触发自动序列化,而不是{User}(只调用.ToString())
常见错误现象:
- 日志里看到
"用户 {@User} 登录"输出成"用户 UserDto { Id=123 } 登录"→ 忘加@,应为{@User} - 查询时发现
UserId字段为空 → 占位符写成{userId}(小写),但传参是var UserId = 123;,字段名不匹配 - 控制台显示正常,但 Seq 里查不到
OrderId→ 检查是否用了Log.Logger = new LoggerConfiguration().WriteTo.Seq(...),没配 Sink 就不会发出去
LoggerConfiguration 初始化时机与作用域
- 必须在
Program.cs的builder.Build()之前完成Log.Logger赋值,否则 ASP.NET Core 启动异常无法被捕获 -
LoggerConfiguration是构建器模式,每调用一次.WriteTo.Xxx()就追加一个 Sink,顺序不影响日志内容,但影响性能(比如先写控制台再写文件,控制台慢会拖慢整体) - 不要重复调用
CreateLogger():多次赋值Log.Logger会导致前一个实例未CloseAndFlush(),部分日志丢失
关键点:
- 生产环境禁用
.WriteTo.Console()(除非调试需要),它会阻塞主线程且无缓冲 -
.Enrich.WithProperty("Environment", "Production")可统一注入环境标识,比每个日志都写{Environment}更可靠 - 若用
ILoggingBuilder.AddSerilog()注入 DI,确保dispose: true,避免Log.CloseAndFlush()被遗漏
RollingInterval.Day 日志滚动的实际行为
- 文件名中必须含
-或.才能触发滚动,例如"logs/app-.log"会生成app-20260413.log;而"logs/app.log"永远只写一个文件 -
rollingInterval: RollingInterval.Day是按本地时间午夜切分,不是按 24 小时滚动;跨时区部署时注意日志日期可能偏移 - 默认保留 31 天日志,如需调整,得用
retainedFileCountLimit: 7参数,否则磁盘可能被撑爆
容易踩的坑:
- 在 Windows 服务或 Docker 容器中运行时,
"logs/"目录可能无写入权限 → 改用绝对路径或检查运行账户权限 -
Log.CloseAndFlush()必须在AppDomain.CurrentDomain.ProcessExit或builder.Services.AddHostedService的StopAsync中调用,否则进程退出时异步日志会丢 - 同一进程多个线程并发写同一个
.WriteTo.File()是安全的,Serilog 内部已加锁,无需额外同步
结构化日志真正的门槛不在配置语法,而在习惯:每次写 Log.Xxx() 前,先想清楚「这个字段以后我要怎么查」。字段名一旦上线就很难改,UserId 和 user_id 在 Elasticsearch 里是两个字段。


















