Zerolog 默认时间戳仅毫秒级因 RFC3339Nano 格式截断纳秒,需设 TimeFieldFormat 为空字符串以支持微秒;高并发下须每次新建带字段的 logger 而非复用,避免 goroutine 间字段污染。

为什么 Zerolog 的 With().Timestamp() 默认不是微秒级
Zerolog 默认用 time.Now().UTC().UnixNano() 生成时间戳,但输出时默认只保留毫秒(time.RFC3339Nano 格式下末尾三位被截断),实际日志里看到的是 "2024-05-21T10:23:45.123Z",丢失了微秒/纳秒精度。这不是 bug,是设计取舍——为兼容性牺牲精度。
- 若需微秒级时间戳,必须显式替换
zerolog.TimeFieldFormat,且不能用time.RFC3339Nano(它强制补零到纳秒,但解析器常忽略末尾) - 推荐设为
""(空字符串),让 Zerolog 调用time.Format("2006-01-02T15:04:05.000000Z07:00"),其中.000000对应微秒 - 注意:Go 1.20+ 的
time.Now().UnixMicro()是整数微秒,但 Zerolog 不直接支持该类型字段,仍需靠格式化字符串实现显示
如何让组件日志带服务名、请求 ID 和调用栈(不拖慢性能)
Zerolog 的 With() 是廉价的,但 Caller() 和 Stack() 默认触发运行时反射和文件读取,QPS 高时明显拖慢。关键在于分层控制:
- 生产环境禁用
Stack(),改用With().Str("trace_id", reqID)由中间件注入,避免日志库自己采样 - 启用
Caller()时务必限制深度:zerolog.CallerSkipFrame = 2(跳过封装函数),否则会定位到 Zerolog 内部代码 - 服务名不要写死,从
os.Getenv("SERVICE_NAME")或配置结构体中取,避免组件复用时日志混淆 - 避免在热路径反复调用
logger.With().Str(...).Logger()—— 提前构建带固定字段的子 logger,如svcLogger := rootLogger.With().Str("service", svcName).Logger()
JSON 输出里怎么避免 level 字段变成数字(比如 10)
Zerolog 默认把 level 存为整数(zerolog.DebugLevel = 10),而多数日志平台(Loki、Datadog)期望字符串 "debug"。这不是配置缺失,而是 Zerolog 的序列化策略默认不转换。
- 必须手动覆盖
zerolog.LevelFieldName和zerolog.LevelFieldMarshaler - 设置
zerolog.LevelFieldMarshaler = func(lv zerolog.Level) string { return lv.String() } - 同时确保
zerolog.LevelFieldName是"level"(默认值,可不设),否则字段名对不上 - 如果用了
zerolog.ConsoleWriter,它内部会自动转字符串,但 JSON output 不会 —— 这点容易漏测
如何安全地在 Goroutine 中复用 Zerolog 实例
Zerolog 的 Logger 实例是线程安全的,但它的 WithContext() 返回的 ContextLogger 不是。常见错误是把 logger.With().Str("req_id", id).Logger() 存到 context.Context 里,然后在多个 goroutine 中并发写入。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:每次需要新上下文日志时,都重新调用
logger.With().Str(...).Logger(),不要缓存结果 - 如果担心性能,可用
sync.Pool缓存zerolog.Event,但别池化Logger实例本身 - 特别注意 HTTP handler 中:不要把带 request-scoped 字段的 logger 存到全局变量或 struct 成员,goroutine 泄露会导致字段错乱
- 验证方式:在 handler 里启动 goroutine 并 sleep 100ms 后打日志,检查
req_id是否与当前请求一致



















