必须用c.Request.WithContext()而非c.Set(),因后者仅存于Gin内部键值对,无法被http.Request.Context()、OpenTelemetry、DB驱动等标准库组件读取;正确做法是context.WithValue(c.Request.Context(), TraceIDKey, traceID)后显式赋值c.Request = c.Request.WithContext(newCtx)。

中间件里必须用 c.Request.WithContext() 而非 c.Set()
很多人误以为 c.Set("trace_id", id) 就完成了透传,其实这只是往 Gin 的 Context 键值对里塞了个字段,下游 handler 或日志模块根本拿不到它——因为 Go 标准库的 http.Request.Context() 是独立的,Gin 的 c 并不自动同步这个值。
正确做法是显式替换请求上下文:
- 用
context.WithValue(c.Request.Context(), TraceIDKey, traceID)包装原始 context - 再调用
c.Request = c.Request.WithContext(newCtx) - 后续所有依赖
c.Request.Context()的代码(比如 OpenTelemetry 的 Extract、日志中间件、DB 驱动)才能真正读到 traceID
从 Header 提取时优先支持 traceparent,兼容 X-B3-TraceId
不同系统发来的请求可能带不同格式的追踪头,只认 X-Trace-ID 会断链。W3C 的 traceparent 是现代标准,必须优先处理;Zipkin 生态常用 X-B3-TraceId 也得兜底。
示例逻辑:
- 先尝试
c.GetHeader("traceparent"),用otel.GetTextMapPropagator().Extract()解析 - 失败再 fallback 到
c.GetHeader("X-B3-TraceId")或c.GetHeader("X-Trace-ID") - 都为空才生成新
uuid.NewString()
注意:traceparent 值形如 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01,不能直接当 traceID 用,要解析后取第一段。
日志和错误包装必须从 Request.Context() 读 traceID,不能查 c.GetHeader()
handler 执行过程中可能修改了 header(比如反向代理重写了),或者你用了异步 goroutine,此时再调 c.GetHeader() 已不可靠。唯一可信来源是注入进 c.Request.Context() 的那个值。
安全读法:
- 定义全局 key:
var TraceIDKey = struct{}{} - 读取:
traceID, _ := c.Request.Context().Value(TraceIDKey).(string) - 日志打点、
WithTrace(err, traceID)、甚至 recovery 中间件里的 panic 捕获,都走这条路
别在 handler 里重复解析 header,那等于放弃上下文传播的意义。
别把 traceID 写进 response header 就算完事
写 c.Header("X-Trace-ID", traceID) 看起来很完整,但审计日志里没它,等于没做追踪。真正关键的是:日志行里是否包含该 traceID 字段,且能被 ELK/Splunk 等工具提取为结构化字段。
所以必须确保:
- 所有日志输出(包括 zap/slog/std log)都显式拼入
traceID,比如logger.Info("user fetched", "trace_id", traceID, "user_id", uid) - 数据库 SQL 日志、HTTP 客户端出向日志、消息队列投递日志,同样要带 traceID
- 如果用了 OpenTelemetry,确认
otel.GetTextMapPropagator().Inject()在发起下游 HTTP/gRPC 调用前被调用
链路追踪不是加个中间件就结束的事,它是贯穿日志、metric、span、error 的统一标识——漏掉任意一环,排查时就会卡在“知道有错,但找不到上下文”。


















