json格式输出是日志聚合前提,因elk、loki、seq等依赖结构化字段索引;纯文本日志无法自动提取username、traceid等属性,只能低效grep;必须用{@r}序列化对象而非{message}字符串拼接,且需确保linux容器中目录权限与filebeat路径匹配。

跨平台日志聚合在 C# 中不是“配个 Sink 就完事”,关键在于结构化输出、上下文一致性、以及与采集链路的对齐。Serilog 是目前最稳妥的选择,但直接用 WriteTo.File 或 WriteTo.Console 会卡在日志解析和字段对齐上。
为什么 JSON 格式输出是日志聚合的前提
ELK、Loki、Seq 等后端系统依赖结构化字段做索引与过滤。纯文本日志(如默认控制台格式)无法被自动提取 Username、TraceId、StatusCode 等关键属性,后续查问题只能靠 grep,效率极低。
- 必须启用
WriteTo.File(..., outputTemplate: "{@l} {@t} {@m} {@x} {@r}")或更推荐的WriteTo.JsonFile()(需安装Serilog.Sinks.Filev5+) -
{@r}表示将传入的对象完整序列化为 JSON 字段,不是字符串拼接 - 避免使用
{Message}占位符输出对象——它只会调用.ToString(),丢失结构 - Linux 容器中若用
WriteTo.File("/logs/app.json"),要确保目录存在且进程有写权限,否则静默失败
Serilog.Enrichers 包如何补全分布式上下文
单条日志脱离请求链路毫无价值。没有 TraceId、SpanId、Environment,你无法把前端报错、API 日志、DB 慢查询串成一条线。
- 添加
Serilog.Enrichers.Environment和Serilog.Enrichers.Thread获取MachineName、ThreadId - ASP.NET Core 中集成
Serilog.AspNetCore后,自动 enrichHttpRequestId、RequestPath、StatusCode - 手动注入全局属性:
Enrich.WithProperty("ServiceName", "order-api"),避免每处Log.Information都重复写 - 不要用
Enrich.WithExceptionDetails()生产环境——它会把整个异常堆栈转成字符串,破坏结构化,改用@x输出原生异常对象
Filebeat / Fluent Bit 采集时最常踩的三个坑
本地日志写对了,不等于能被正确采集。采集器不是万能解析器,它依赖明确的换行、编码和路径约定。
- 日志文件必须以
\n结尾,且每条记录独占一行;多行异常栈需用multiline.pattern配置合并,否则被切碎成多条 - Filebeat 默认读取
utf-8,若 .NET 写入时用了 BOM 或utf-16(尤其 Windows 上),采集会乱码或丢数据 - 容器内挂载日志目录时,
Filebeat的paths必须匹配容器内路径(如/app/logs/*.json),而不是宿主机路径 - 别依赖
rollingInterval: RollingInterval.Day自动轮转后立刻被采集——Filebeat 缓存 inode,旧文件可能被跳过,建议配合close_inactive: 1h
什么时候该放弃直接写文件,改用 HTTP Sink
不是所有场景都适合“先写磁盘再采集”。当部署密度高、磁盘 I/O 敏感、或需要强顺序保障时,直连聚合服务更可控。
- 用
Serilog.Sinks.Http可直接 POST JSON 到 Loki 的/loki/api/v1/push或自建 API,绕过文件层 - 注意设置
batchPostingLimit和period,避免高频小请求打垮接收端;典型配置是 10 条/2 秒 - HTTP Sink 没有本地缓存,网络中断即丢日志;生产环境务必加
SelfLog监控异常:Serilog.Debugging.SelfLog.Enable(Console.Error) - Kubernetes 中若用 DaemonSet 部署 Fluent Bit,优先走文件采集;若用 Sidecar 模式且服务量小,HTTP Sink 更轻量
真正难的不是把日志发出去,而是让每条日志在任意节点、任意时间、任意错误分支下,都携带可关联的 TraceId 和一致的字段名。这需要从中间件、全局异常过滤器、甚至数据库访问层统一注入,而不是只在 Log.Information 调用点临时补。










