必须通过 context.Context 透传 trace_id,HTTP/gRPC 入口解析并注入,goroutine 启动时显式传递,日志用 LoggerFromCtx 封装自动提取,跨协议需用 OpenTelemetry Propagator 桥接 header/metadata。

Go 微服务里日志和链路追踪必须绑定 trace_id 才能查得清问题,否则日志是散的、链路是断的——不是配了 OpenTelemetry 就自动好了,关键在 context.Context 怎么透传、logger 怎么取值、HTTP/gRPC 边界怎么不丢。
trace_id 为什么总在 goroutine 里丢失
常见错误现象:log.Printf("user_id=%s", userID) 这种硬拼字段写法,在启动新 goroutine 后就拿不到 trace_id;或者用全局 map + goroutine ID 做“伪 MDC”,结果并发写崩、GC 报警飙升。
根本原因是 Go 没有隐式线程局部存储(TLS),goroutine 之间不共享变量。唯一可靠载体就是 context.Context。
- 所有入口(HTTP handler、gRPC UnaryServerInterceptor、消息消费函数)必须从请求头(如
X-Trace-ID或traceparent)解析出 trace_id,并用context.WithValue(ctx, traceIDKey, id)注入 - 每次启动新 goroutine,必须显式传入该 context,绝不能用
context.Background()或context.TODO() - 定义 key 类型防冲突:
type ctxKey string; const traceIDKey ctxKey = "trace_id" - DB 查询、HTTP client 调用、下游 gRPC 请求,全部要求传带 trace_id 的 context,漏一次,链路就断一截
zap 日志如何自动带上 trace_id
zap 本身不读 context,直接 logger.With(zap.String("trace_id", getTraceID(ctx))).Info(...) 容易漏、难维护。必须封装一层,让 logger 构造时自动提取。
实操建议:
- 写一个
LoggerFromCtx(ctx context.Context) *zap.Logger函数,内部调用getTraceID(ctx)并logger.With()构造新实例 - 在 HTTP middleware 中统一构造一次 logger,然后通过
ctx = context.WithValue(ctx, loggerKey, logger)透传,后续各层直接logger := getLoggerFromCtx(ctx) - 避免在初始化阶段一次性注入 trace_id 字段——那只会固定为启动时刻的空值或默认值
- 性能注意:
With()是浅拷贝,开销小,但别在 hot path 上高频新建 logger;request scope 内复用一次即可
HTTP 和 gRPC 之间 trace header 怎么不丢
HTTP 到 gRPC 调用时,traceparent 不会自动变成 gRPC metadata;反过来,gRPC client 发 HTTP 请求也一样。协议边界不桥接,链路必然断裂。
正确做法:
- HTTP client 发 gRPC 服务前:用
metadata.MD{}.Set("trace-id", getTraceID(ctx))手动塞进 metadata;或更标准地,用otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(headers))先注入到 headers,再映射过去 - gRPC server 端:在
UnaryServerInterceptor中,从metadata.FromIncomingContext(ctx)提取"trace-id"或"traceparent",再用otel.GetTextMapPropagator().Extract()解析并注入新 context - gRPC client 调用 HTTP 服务时:同样先用 propagator.Extract 从当前 ctx 提取 span,再用 Inject 写入
http.Header,最后发请求 - 注意命名规范:gRPC metadata 键名不支持下划线和空格,推荐用
trace-id连字符格式,避免X-Trace-ID直接传过去被忽略
go-zero 中 logx 怎么配合链路追踪
go-zero 的 logx 天然支持 context,但默认不自动提取 trace_id——你得主动触发。
实操要点:
- 入口处(如 HTTP handler)拿到
trace_id后,用logx.WithContext(ctx)包一层,得到带上下文的 logger 实例 - 后续所有
logx.Info()、logx.Error()都基于这个 logger,它会自动从 ctx 里取trace_id和span_id注入日志字段 - 配置里确保
Log.Mode设为file或volume,配合 promtail/filebeat 收集,否则日志落不到中心系统 - 别依赖
logx.MustSetup()的全局 logger——它没 context,trace_id 字段永远为空
最常被忽略的点是:trace_id 在跨协议(HTTP↔gRPC)、跨协程(go func())、跨中间件(auth → service → repo)这三类边界上,只要漏传一次 context,整条链路就碎成两截。不是埋点代码写了就行,而是每一条调用路径都得检查 context 是否完整携带。

















