标准库log无法满足http请求日志需求,因其缺乏结构化能力、无法提取trace_id/status_code等字段,不支持自动脱敏与精准过滤,且无writeheader钩子机制导致状态码记录失真。

zap 是生产环境最稳的日志框架选择,结构化、高性能、OTel 链路集成成熟;slog 适合新项目轻量起步,但必须自己补 trace_id 注入逻辑;logrus 和 zerolog 各有适用边界,但都需警惕字段类型或上下文丢失风险。
为什么不能直接用标准库 log 记录 HTTP 请求日志
标准库 log 缺少结构化能力,无法提取 trace_id、status_code 等字段供日志平台聚合分析。它输出的是纯文本,比如:2026/08/25 17:57:03 GET /api/user 200 123ms,这种格式在 ELK 或 Loki 里没法做 status_code:500 这类过滤。
- 所有业务日志必须是键值对(如
zap.String("path", r.URL.Path)),而非字符串拼接 -
log.Printf("path=%s status=%d", r.URL.Path, statusCode)看似能用,但字段名不统一、无法自动脱敏、查问题时得正则硬匹配 - HTTP 中间件里记录请求日志,必须在
rw.WriteHeader()之后读取真实状态码——标准库没这个钩子机制,容易记成 200(实际已 panic)
zap 怎么自动注入 trace_id 和 span_id
别在每条日志里手动传 zap.String("trace_id", tid),既易漏又破坏可维护性。核心是包装 Core,从 context.Context 提取 trace 信息并塞进日志 entry。
- 异步 goroutine 里
context会丢,必须用context.WithValue(ctx, key, val)显式传递,不能依赖 middleware 注入的原始 ctx - 不要调用
otel.Tracer.Start()来临时取 trace ID——这会创建新 span,导致链路断裂、时间戳错乱 - 推荐用
zap.NewOpenTelemetryHook(来自go.opentelemetry.io/contrib/zap),它能直连 OTel Collector,自动透传trace_id和span_id - 若不用 OTel,至少加
zap.AddCallerSkip(1),避免日志位置显示在封装函数里,而不是真实调用处
多输出场景下怎么按级别分流(比如 Debug 写文件,Info 同时写终端+文件)
io.MultiWriter 不行——它只是把同一条日志复制到多个 io.Writer,无法区分级别。真正可行的是用日志库自身的 Level Filter + Hook 或自定义 Core。
-
zap:用zapcore.NewTee组合多个Core,每个Core绑定不同LevelEnabler和WriteSyncer -
zerolog:靠zerolog.LevelParameter+ 自定义Writer实现,但它不支持time.Time直接写字段,得先转成字符串,否则 panic -
slog:需实现slog.Handler接口,在Handle方法里判断r.Level,再分发到不同io.Writer - 文件输出务必用带轮转的
lumberjack.Logger,别自己搞os.OpenFile+ 定时切分——并发写时容易丢日志或损坏文件
slog 要用好,绕不开的三个坑
slog 原生简洁,但生产落地时容易踩空。它不是“开箱即用”,而是“开箱即配”。
- 字段必须用
slog.String()、slog.Int()等强类型函数,不能传裸字符串键值对,否则 lint 会报!badkey - 没有内置
trace_id注入,必须自己写一个func(context.Context) slog.Handler包装器,从trace.SpanFromContext(ctx).SpanContext().TraceID().String()提取 -
slog.NewJSONHandler默认不带 caller 信息(文件+行号),要手动加slog.HandlerOptions.AddSource: true,且只在非生产环境开——性能损耗明显
context 在 goroutine 里丢失、日志级别和输出目标混在一起配置——这些问题不会在本地跑通时暴露,而是在线上查一个 500 错误花了 40 分钟才定位到是某个中间件没传 trace_id 时才突然变得刺眼。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











