go微服务日志必须输出标准json到stdout(容器环境)或文件(物理机),禁用拼接字符串;需用zerolog/zap等结构化库,配合filebeat multiline合并panic堆栈、force_close_files防轮转丢日志,并校准时间戳与透传trace_id。

Go微服务不直接连ELK或Loki,必须靠采集器(Filebeat/Fluent Bit/Promtail)收本地 stdout 或文件,否则丢日志、卡协程、时间戳错乱。
Go服务该往哪写日志:stdout 还是文件?
容器环境(K8s/Docker)一律走 os.Stdout;物理机或VM部署才考虑写文件。前者由运行时自动捕获+注入Pod元数据,后者需Filebeat精确监控路径且易丢日志。
- 写
os.Stdout时,必须用 JSON 编码器(如zap.NewProductionConfig()或zerolog.New(os.Stdout)),每条日志严格单行,不能含换行符 - 写文件时,必须用
os.OpenFile带os.O_APPEND | os.O_CREATE标志,避免多goroutine冲突;别用log.SetOutput套io.MultiWriter写多个目标 - 开发环境可临时用
zap.NewDevelopmentConfig()输出带颜色和行号的文本,但上线前必须切回 JSON
Filebeat 怎么不漏日志:轮转与 inode 的坑
lumberjack 轮转后新文件 inode 变了,Filebeat 默认只认文件名,旧句柄不关,新日志就“消失”。这不是配置没生效,是它根本没发现文件已切换。
- 必须在 Filebeat 配置中启用
force_close_files: true,让它检测到文件重命名/删除就主动关闭旧句柄 -
close_inactive: 5m要设合理——太短会误关活跃文件,太长导致轮转后残留句柄 -
scan_frequency: 1s(别更低),确保轮转后1秒内扫到新文件;默认10s大概率漏掉首几条 -
clean_removed: false配合max_backups: 5(别设1),防止旧日志文件被删太快,Filebeat还来不及读完
panic 堆栈怎么合并成一条日志?
Go panic 是多行输出,Filebeat 默认按行切分,结果一个 panic 变成几十条 ES 文档,根本没法查。
- 在
filebeat.inputs中加multiline.pattern: '^panic:'和multiline.negate: true -
multiline.match: before——关键!after会吞掉第一行,ES里看不到panic:关键词 - 别依赖 Logstash 做合并:延迟高、失败后难追溯;Filebeat 在采集端做更稳
- 测试方法:
kill -SIGUSR1 $(pidof myapp)触发 panic,再查 ES 是否收到完整块(含 goroutine stack)
trace_id 怎么透传并让日志可串联?
光打 trace_id 字段没用,HTTP/gRPC 调用链不断,日志就断链。必须从入口解析、透出、再注入,三步缺一不可。
- HTTP 中间件里从
r.Header.Get("X-Trace-ID")读,为空则生成ulid.Make().String(),存入context.Context - 下游调用时,HTTP 客户端要手动塞
req.Header.Set("X-Trace-ID", tid);gRPC 客户端用metadata.Pairs("trace-id", tid) - 日志字段统一用
trace_id(小写下划线),别写成TraceID或requestId,否则 Kibana/Loki 无法跨服务聚合 - OpenTelemetry 不是银弹:若不用 OTel Collector,就别开自动埋点,手动在中间件里
logger.With(zap.String("trace_id", tid))更可控
最常被忽略的是时间戳校准——Go 默认用本地时区,Filebeat 解析 JSON 时若没配 timezone: UTC,ES 里时间会偏移;还有 stdout 场景下,Docker 日志驱动可能缓存日志,得调 docker run --log-opt mode=non-blocking 避免堆积。











