Go日志库选型标准:log/slog适合简单服务或调试,生产环境高吞吐场景首选zap(性能好、OTel生态成熟),zerolog更轻快但字段类型受限,slog需自行封装context提取trace_id。

Go 日志库选型:log/slog vs. zap vs. zerolog 怎么选
标准库 log 和 Go 1.21 引入的 slog 适合简单服务或调试,但生产环境高吞吐、结构化、带上下文的日志必须用第三方。选型关键看三点:是否支持字段结构化、是否可注入 trace_id、是否兼容 OpenTelemetry。
zap 是最稳的选择——性能好、生态成熟、zap.NewOpenTelemetryHook 能直连 OTel Collector;zerolog 更轻更快,但字段类型限制多(比如不支持 time.Time 直接写,得转成字符串);slog 原生支持 Handler 扩展,但目前缺少开箱即用的链路注入能力,需自己 wrap context.Context 提取 trace ID。
- 已有 OTel SDK 的项目,优先用
zap+otlpgrpcexporter - 边缘小服务或嵌入式场景,用
zerolog+ 自定义Writer输出 JSON 到 stdout - 新项目想最小依赖,可用
slog,但必须补一层func(ctx context.Context) slog.Handler来提取trace.SpanFromContext(ctx).SpanContext().TraceID().String()
如何让日志自动带上 trace_id 和 span_id
手动在每条日志里加 logger.Info("req done", "trace_id", tid, "span_id", sid) 不现实,也容易漏。正确做法是把 trace 上下文“透传”进日志 handler。
以 zap 为例,核心是用 zap.AddCallerSkip(1) 避免日志位置错乱,再通过 zap.WrapCore 包装原始 core,在 Write 方法里从 entry.Logger 的 ctx(或 entry.Caller 附近隐式传入的 context)中提取 trace 信息。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖 HTTP middleware 注入的 context —— 异步 goroutine 里会丢 context,必须用
context.WithValue显式传递 - 避免在
log.Info时临时调用otel.GetTraceProvider().GetTracer(...).Start,这会创建新 span,破坏链路 - 字段名统一用
trace_id和span_id(小写下划线),别用TraceID或traceId,否则 Grafana Loki 查询时匹配不上
HTTP 中间件里怎么串联 trace 和日志
HTTP handler 是链路起点,中间件必须完成三件事:生成 root span、把 span context 注入 context、把 trace_id 注入 response header(如 X-Trace-ID)。
用 otelhttp.NewHandler 包裹 handler 是最简方案,但它默认不记录请求 body 和响应 status code。要补全,得自定义 otelhttp.Option:
otelhttp.WithSpanOptions(
trace.WithAttributes(
semconv.HTTPMethodKey.String(r.Method),
semconv.HTTPURLKey.String(r.URL.String()),
),
),
同时注意:中间件顺序不能错——otelhttp.NewHandler 必须在日志中间件之前,否则日志拿不到刚创建的 span context。
- 不要用
r.Header.Get("X-Trace-ID")作为 trace_id 来源,应优先用otel.GetTextMapPropagator().Extract从 header 解析 W3C TraceContext - 如果下游是 Python/Java 服务,确保 Go 端用
otelhttp.NewTransport发起 http.Client 请求,否则 trace 无法透传 - 本地开发时关闭采样(
trace.AlwaysSample()),线上用trace.ProbabilitySampler(0.01)防止日志爆炸
gRPC 场景下 trace 跨进程丢失怎么办
gRPC 默认不传播 trace context,即使 client 端调用了 otelgrpc.Interceptor,server 端没配对应 interceptor,span 就断了。
client 侧必须用 grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),server 侧必须用 grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor())。漏掉任一端,链路就是两截。
- server interceptor 要放在所有其他 interceptor 之前,否则中间件修改了
ctx导致 trace context 被覆盖 - gRPC 流式方法(stream)需额外配
otelgrpc.StreamClientInterceptor和otelgrpc.StreamServerInterceptor,不能复用 unary 的 - 如果用
grpc.Dial连接的是 Istio sidecar,确认 sidecar 开启了 tracing config,否则 header 会被清洗
链路追踪不是加几个 middleware 就完事,trace context 的生命周期管理比日志更敏感——它必须随每个 goroutine 显式传递,且不能被任意中间件无意覆盖。最容易出问题的地方,永远是异步任务和定时器启动的新 goroutine。


















