grpc客户端需显式将w3c格式traceparent注入metadata,服务端用otel extract解析并桥接至context,日志库须每次从ctx取值,异步goroutine必须显式传入ctx或span,否则链路断裂。

GRPC客户端必须显式把TraceID写进metadata
HTTP靠中间件自动注入context,GRPC没有这层机制。只调用context.WithValue(ctx, traceKey, tid),下游服务的ctx.Value(traceKey)一定是nil——因为GRPC的metadata.MD和context.Context是两个独立容器,不会自动映射。
实操建议:
- 客户端发起调用前,用
metadata.Pairs("x-trace-id", traceID)构造metadata.MD - 再通过
grpc.InjectMetadata(ctx, md)或更稳妥的metadata.AppendToOutgoingContext(ctx, "x-trace-id", traceID)注入到context - 服务端在
UnaryServerInterceptor中用metadata.FromIncomingContext(ctx)取头,再用context.WithValue塞回context供日志使用 - 注意header名统一用小写
x-trace-id,GRPC metadata key默认转为小写,大小写不一致会导致取不到
HTTP与GRPC之间传递TraceID要对齐格式
HTTP中间件可能从traceparent头提取32位trace-id,但GRPC客户端若只传X-Trace-ID: abc123这种短ID,下游服务解析时就会因长度/格式不匹配而丢弃——尤其当链路接入Jaeger或OTel Collector时,会直接拒绝非W3C标准的span。
实操建议:
- 统一用W3C
traceparent格式透传:例如00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01,其中第2段是trace-id - GRPC客户端写入metadata时,不要只写value,应保持key为
traceparent,value为完整字符串 - 服务端解析时优先用
otel.GetTextMapPropagator().Extract(),它能兼容traceparent、X-Trace-ID、X-Request-ID多种来源 - 避免混用:同一链路中HTTP用
traceparent,GRPC却用X-Trace-ID,会导致OTel SDK认为是两个独立trace
日志库必须桥接context值,不能依赖logger实例缓存
zap或zerolog的logger对象本身不感知context,logger.With(zap.String("trace_id", tid))生成的子logger只是临时快照,一旦tid变化(比如下一个请求),复用该logger就会打错trace_id。更糟的是,slog默认根本不透传context到Handler。
实操建议:
- 封装一个
WithContext(ctx context.Context) *zap.Logger方法,每次调用都重新从ctx.Value(traceKey)取值并.With() - zerolog推荐用
zerolog.Ctx(ctx).Info().Str("trace_id", tid).Msg(...),它内部已做context提取 - slog需自定义
slog.Handler,在Handle(ctx context.Context, r slog.Record)里手动调用ctx.Value(traceKey)再r.AddAttrs() - 禁止在HTTP handler外初始化一个“全局带trace_id的logger”,那只会让所有请求共享同一个trace_id
异步goroutine中TraceID丢失最隐蔽也最致命
Go的go func() { ... }()不继承父goroutine的context,哪怕你把带trace_id的ctx传进去,只要没显式作为参数传入函数体,里面调用DB或发HTTP请求时拿到的都是空context。日志看着有trace_id,实际span却是root,链路在第一个异步点就断了。
实操建议:
- 显式把
ctx作为参数传入goroutine函数:go doWork(ctx, req),而不是go doWork(req) - 如果用了OpenTelemetry,优先传
span而非ctx,因为span.SpanContext()可安全跨goroutine使用 - DB操作如
db.QueryContext(ctx, ...)必须传ctx,否则sql driver不会把trace信息注入到driver.Conn - 定时任务或消息队列消费者,也要在消费入口就从消息体解析trace_id并注入context,不能等到handler里才生成
req.Header.Set写成Add,一个GRPC metadata key大小写不一致,一个goroutine忘了传ctx——链路就在这些地方无声断裂。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











