gRPC客户端必须显式将traceparent注入metadata,因gRPC不自动透传context;header名须为全小写traceparent,value须为完整W3C格式;日志需每次从ctx提取trace_id,不可复用Logger;HTTP与gRPC间traceparent须原样传递;context key须用未导出私有类型。

gRPC客户端必须显式注入traceparent到metadata
GRPC不自动透传context,只靠context.WithValue写trace_id进ctx,下游ctx.Value()一定取不到——因为GRPC的metadata和context是两个隔离容器。常见错误是HTTP网关生成了traceparent头,但GRPC客户端没把它塞进outgoing metadata,导致链路在第一个RPC调用就断掉。
- 用
metadata.AppendToOutgoingContext(ctx, "traceparent", traceparentStr),比grpc.InjectMetadata更稳妥,能自动处理key小写化 - header名必须是
traceparent(全小写),不能用X-Trace-ID或trace-id,否则OTel Propagator解析失败 - value必须是完整W3C格式:
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",不能只传中间那段32位trace-id
日志库每次调用都得从ctx里取trace_id
全局*slog.Logger或*zap.Logger实例一旦初始化,就固化了字段,复用它打日志必然串号。压测时多请求日志显示同一个trace_id,或者goroutine里打出来是<nil></nil>,基本都是这个原因。
- 不要在中间件里
logger = logger.With("trace_id", id)然后存到request.Context()里——那只是快照,不是绑定 - 正确做法:每个日志语句前都调用
getTraceID(ctx),再传给slog.With()或zap.Logger.With() - 如果用
zerolog,必须用log.With().Str("trace_id", getTraceID(ctx)).Logger()临时构造,不能复用上一个请求的Logger实例
HTTP与gRPC之间trace_id格式必须对齐
HTTP中间件从traceparent头提取出32位trace-id后,若直接当成字符串塞进gRPC的X-Trace-ID metadata,下游OTel SDK会因格式不合规拒绝接收——尤其对接Jaeger或OTLP Collector时,直接丢弃span。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- HTTP服务发gRPC请求前,别自己截取、拼接、转格式;直接把原始
traceparentheader value原样写入gRPC metadata - gRPC服务端在
UnaryServerInterceptor里,优先用otel.GetTextMapPropagator().Extract(ctx, MD)解析,它能兼容traceparent、b3、X-B3-TraceId等多种格式 - 避免混用:同一链路里HTTP走
traceparent,gRPC却用X-Trace-ID,OTel会认为是两条独立trace
context.WithValue的key必须是私有类型
用字符串字面量做key,比如context.WithValue(ctx, "trace_id", id),极大概率被其他库覆盖或读错——zap、grpc、otlp等内部都可能用相同字符串当key。
- 定义私有类型:
type ctxKey string,再const traceIDKey ctxKey = "trace_id" - key类型必须未导出(首字母小写),否则跨包可能冲突
- 提取时也必须用同一
traceIDKey,不能换成string("trace_id")或另一个常量
真正容易被忽略的是:goroutine启动时必须显式传入带trace_id的ctx,而不是拷贝原始ctx再context.WithCancel——后者会丢失WithValue里的所有值。哪怕只起一个异步DB查询,漏掉这一步,那条日志就彻底脱离链路。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










