必须用zerolog或zap替代标准log包实现结构化json日志,因标准log仅输出字符串无法提取level等字段;zerolog支持原生level字段、链式注入、panic堆栈结构化及goroutine上下文传递。

直接结论:别用标准 log 包做 JSON 日志,它不支持结构化字段注入;必须换 zerolog 或 zap,且等级字段(level)不能靠前缀模拟,得由库原生支持。
为什么标准 log 无法输出合规 JSON 日志
标准 log 的 log.Printf 输出的是完整字符串,比如 "[ERROR] db timeout\n"。即使你调用 log.SetFlags(0) 清掉时间戳和文件名,也无法把 "ERROR" 拆出来作为独立 JSON 字段。所有内容都已拼成一行文本,io.Writer 收到的只是字节流,没法反向提取 level、msg、ts 等语义字段。
- 常见错误:用正则从
Write([]byte)里提取"\[ERROR\]"—— 不稳定,日志消息本身含方括号就崩 - 更糟的是:
debug.Stack()返回[]byte,直接%v打印会截断或乱码 - HTTP
Server.ErrorLog同理,它只接收字符串,不区分 panic 类型或上下文
zerolog 是微服务 JSON 日志最简可行方案
zerolog 默认输出合法 JSON 行,字段可链式注入,无运行时反射开销,适合容器环境。关键不是“能输出 JSON”,而是“字段可被 Loki / Grafana / ELK 原生解析”。
- 等级字段必须是
"level":"error"这种键值对,不是"msg":"[ERROR] ..." - 用
zerolog.New(os.Stdout).With().Timestamp().Logger()初始化,.With()注入固定字段(如service_name、container_id) - 调用
log.Error().Str("db", "user").Int("attempts", 3).Msg("connect failed"),输出自动带"level":"error"和所有字段 - 禁用颜色:
zerolog.SetGlobalLevel(zerolog.InfoLevel),避免 ANSI 码污染 stdout
HTTP panic 捕获必须走结构化日志,而非 log.Printf
gin 或 echo 的 recover 中间件若还用 log.Printf("panic: %v\n%v", r, debug.Stack()),等于把堆栈当普通字符串塞进一条日志——下游系统无法按 stack 字段过滤,也无法关联 trace_id。
- 正确做法:recover 后构造
zerolog.Event,显式写入stack字段:log.Panic().Stack().Err(r).(err).Msg("unhandled panic") - 必须提前在 logger 中启用
.Caller()和.Stack(),否则Stack()方法无效 - 如果用了 OpenTelemetry,记得把
trace.SpanContext()注入 event:.Str("trace_id", span.SpanContext().TraceID().String())
跨 goroutine 传递日志上下文容易丢字段
zerolog 的 Logger 是值类型,传参或闭包捕获时若没用 With() 显式派生新实例,子 goroutine 写的日志不会继承父级字段(如 request_id)。
- 错误示范:
go func() { log.Info().Msg("in goroutine") }()—— request_id 丢失 - 正确做法:
ctxLog := log.With().Str("request_id", reqID).Logger(),再go func() { ctxLog.Info().Msg("in goroutine") }() - 不要依赖全局 logger:微服务中每个请求应有独立上下文 logger,避免字段污染
真正难的不是格式化成 JSON,而是让每条日志都携带可查询、可关联、可聚合的结构化字段。一旦字段缺失或类型错位(比如把 int 当 string 存),下游告警和看板就失效。别省那几行代码,从第一行日志就走 zerolog 的 Event 链式调用。











