应使用 context.Context 透传 trace ID:入口从请求头提取 ID 并用自定义 key 注入 ctx,下游调用显式传递该 ctx;日志通过 WithContext 动态提取 trace ID,避免 With 频繁创建 logger 实例;gRPC/HTTP 混合调用需手动处理 metadata 和 header 传递。

怎么用 context.Context 透传调用链 ID 而不污染业务逻辑
微服务间日志无法关联,本质是请求上下文没贯穿。Go 里唯一可靠的方式是靠 context.Context 向下传递 trace ID,而不是靠全局变量或中间件塞进 http.Request 的字段里——后者在异步 goroutine 或 grpc/HTTP 混用时会丢。
实操建议:
- 入口处(如 HTTP handler 或 gRPC server)从请求头(
X-Trace-ID或traceparent)提取 ID,用context.WithValue注入到 ctx,值类型必须是自定义 key(别用字符串) - 所有下游调用(
http.Client.Do、grpc.ClientConn.Invoke)都显式传这个 ctx,别用context.Background() - 日志库(如
zap)初始化时配置zap.AddCallerSkip(1),避免 wrapper 函数干扰行号;写日志时统一取ctx.Value(traceKey)注入字段
为什么 zap 的 With 不适合存 trace ID
很多人直接在每个 logger.Info 前加 logger.With(zap.String("trace_id", id)),看似简单,但实际埋了坑:每次 With 都生成新 logger 实例,高频打日志时 GC 压力明显上升;更关键的是,如果某次调用漏写了 With,那条日志就彻底掉链了,排查时根本串不起来。
正确做法:
立即学习“go语言免费学习笔记(深入)”;
- 用
zap.NewAtomicLevelAt初始化 logger,配合zap.AddStacktrace控制错误栈输出 - 封装一个
WithContext(ctx context.Context, logger *zap.Logger) *zap.Logger,内部从 ctx 提取 trace ID 并缓存为 field,返回带该 field 的 logger —— 这样业务代码只管用 logger,不用每行都手动塞 - 注意:不要把 trace ID 存进 logger 的
Fields列表再反复复用 logger,因为zap.Logger是不可变的,With返回新实例,而你真正要的是“每次打日志都动态读 ctx”
grpc 和 HTTP 混合调用时 trace ID 丢失的典型场景
常见错误现象:http -> grpc -> http 链路中,第二跳 http 日志没了 trace_id。根源是 grpc client 默认不转发 context 中的值到 wire 上,也不自动解析 response header 里的 trace 头。
必须手动处理:
- grpc client 发起调用前,用
metadata.Pairs("x-trace-id", traceID)构造 metadata,并通过grpc.Header(&md)从 response 里回读(否则服务端返回的 trace 头你根本拿不到) - HTTP client 发起请求时,手动把 ctx 里的 trace ID 写进
req.Header.Set("X-Trace-ID", id);别依赖http.DefaultClient,它不会自动读写 context - 如果用了
gRPC-Gateway,注意它默认把 HTTP header 映射成 grpc metadata,但反向(grpc metadata → HTTP header)需显式配置runtime.WithMetadata回传
日志采样和敏感信息过滤不能靠事后 grep
线上环境全量打 trace 日志,磁盘撑爆只是时间问题;更麻烦的是,有些请求带 token、手机号等字段,一不小心就写进日志,合规风险直接拉满。
关键控制点:
- 在日志写入前做采样:按 trace ID 哈希后取模,比如只记录
hash(id)%100 == 0的请求,避免用随机数(会导致同一条链日志分散) - 敏感字段过滤必须在结构化日志序列化前完成,不是靠日志系统侧正则脱敏——
zap.String("user_token", token)这种写法已经泄漏了,得改用zap.String("user_token", "***")或提前 scrub - 数据库 SQL 日志尤其危险:不要直接
logger.Info("sql", zap.String("query", rawSQL)),而是用sqlparser解析后只留表名+操作类型,参数一律掩码
复杂点在于 trace ID 的生命周期管理——它得从入口开始生成、透传、落盘、上报、归档,任何一个环节 ctx 被重置或 logger 被误复用,整条链就断了。没人会专门查“为什么这条日志没 trace_id”,只会觉得“这服务日志不全”。









