必须先提取或生成trace id再记录日志,否则日志缺失trace上下文;应使用otelgrpc.unaryserverinterceptor自动注入span并从metadata提取traceparent,日志库需动态注入trace_id字段,且客户端必须显式配置unaryclientinterceptor以透传trace上下文。

gRPC Server Interceptor怎么加日志和trace ID
直接在 grpc.UnaryInterceptor 里套两层逻辑就行,但顺序很重要:必须先提取或生成 trace ID,再记录请求日志,否则日志里看不到 trace 上下文。常见错误是把日志打在 interceptor 外层(比如 handler 里),导致不同 goroutine 中 trace ID 丢失或错乱。
实操建议:
- 用
opentelemetry-go的otelgrpc.UnaryServerInterceptor()自动注入 span,它会从metadata或 HTTP header(若走 gateway)读traceparent;没则新建 - 自定义日志 interceptor 要调用
req.Context().Value("trace_id")——但别这么干,应该用otel.GetTextMapPropagator().Extract()从 context 提取 span - 日志库(如
zap)需用logger.With(zap.String("trace_id", span.SpanContext().TraceID().String()))注入字段,不是拼字符串
Client Interceptor里如何透传trace上下文
客户端不手动 inject,就等于断链。默认 grpc.Dial 不带任何 propagator,otelgrpc.UnaryClientInterceptor() 必须显式传入,且要和 server 端用同一套 propagator(比如 propagation.TraceContext{})。
容易踩的坑:
- 忘记在
grpc.Dial时加grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),结果 client 发出去的请求 header 里没有traceparent - server 端用了
Baggagepropagator,client 却只配了TraceContext,baggage 字段被丢弃 - 调用链跨语言(比如 Go client → Java server),必须确认双方都支持并启用
traceparent格式,而不是自定义 header 名
为什么UnaryInterceptor里ctx.Done()总被提前关闭
因为 gRPC 默认用 context.WithTimeout 包一层,而你的 interceptor 如果做了耗时操作(比如写日志到远程 ES、发 metric 到 Prometheus pushgateway),可能阻塞在 handler(ctx, req) 之前,导致 ctx 超时触发 cancel,后续所有基于该 ctx 的 trace/span 自动结束。
解决办法很实际:
- 日志和 trace 上报必须异步:用
go func() { ... }()启 goroutine,但注意别传原始ctx,要用context.Background()或带独立 timeout 的新 ctx - 不要在 interceptor 里调
span.End()——otelgrpc的 interceptor 已在 handler 返回后自动 end,重复调用会导致 span 状态异常 - 如果必须同步等日志落盘(审计场景),改用
context.WithDeadline预留 buffer 时间,比如原 timeout 是 5s,interceptor 里最多花 200ms 做预处理
Interceptor和middleware顺序冲突怎么办
gRPC 没 middleware 概念,所谓“冲突”其实是多个 interceptor 的执行顺序问题。Go 的 grpc.UnaryInterceptor 只接受一个函数,你得自己 compose:最外层的先执行,最内层的最后执行。比如想先鉴权再日志,就得把鉴权 interceptor 当参数传给日志 interceptor。
推荐写法:
func ChainUnaryServer(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
h := handler
for i := len(interceptors) - 1; i >= 0; i-- {
h = wrapHandler(interceptors[i], h)
}
return h(ctx, req)
}
}
关键点:
- 遍历顺序是倒着来(
i--),保证第一个 interceptor 最先拿到 ctx - 每个
wrapHandler必须返回新 handler,不能直接调handler(ctx, req)——那会跳过后续 interceptor - OpenTelemetry 的
otelgrpcinterceptor 应该放在最外层(即数组第一个),确保 span 包住所有其他逻辑
trace ID 在整个链路里能串起来,靠的是每个环节都严格遵循 context 传递和 propagator 解析。漏掉任意一环,比如 client 少个 interceptor 或 server 少个 propagator 配置,整条链就断成两截。











