OpenTelemetry是唯一兼容Jaeger/Zipkin的链路追踪方案,runtime/trace生成的trace.out因格式不兼容且缺失HTTP、DB等关键字段而无法使用;必须显式透传context.Context,用otelhttp.NewHandler包裹handler、http.NewRequestWithContext发起请求、metadata.MD处理gRPC,并确保span.End()在所有return路径执行。

该用 OpenTelemetry,不是 runtime/trace
runtime/trace 和链路追踪是两套完全不兼容的东西。你调 runtime/trace.Start() 生成的 trace.out 文件,丢进 Jaeger 或 Zipkin 会直接报错 unknown magic number;它压根不记录 HTTP 路径、DB 查询、服务名,只显示灰色 Proc 条和零星 GC 时间点。真要查 “哪个 handler 最慢” 或 “DB 调用在哪断了”,得换 OpenTelemetry。
必须显式透传 context.Context
OpenTelemetry 的 span 生命周期完全绑定 context.Context。不传 ctx,链路就断在第一跳:
- HTTP server 端从
req.Header解析traceparent后,必须用req = req.WithContext(newCtx)覆盖原 request,否则后续 handler 拿不到 span - HTTP client 发起请求前,得调
http.NewRequestWithContext(ctx, ...),原生http.DefaultClient.Do(req)不读 context - 启动 goroutine 时漏传
ctx(比如go func() { ... }()),新协程里的 span 就变成 root,父子关系丢失 - gRPC 场景要用
metadata.MD注入,不能只处理 HTTP header
Span 必须手动结束,defer 不够可靠
常见现象:Jaeger 上看到 span “started but never finished”,或持续运行数小时。根本原因不是 SDK 漏逻辑,而是你没控制好生命周期:
-
span.End()必须出现在所有 return 路径上,panic 或 early return 会让 defer 失效 - 别把 span 存局部变量,下游无法继承;也别在 handler 里 start span——中间件已开,handler 只做业务
- 用
otelhttp.NewHandler()替代手写中间件,它内置 finish 逻辑,避免漏调 - 检查是否误调
span.End()两次,会 panic
跨服务传递 Baggage 需额外注入
Trace ID 和 Span ID 自动传播,但业务字段(如 user_id、tenant_id)不会随 context 自动塞进 HTTP header:
立即学习“go语言免费学习笔记(深入)”;
- 发送方需显式调
otel.Baggage().Set(...),再用otel.GetTextMapPropagator().Inject()写入req.Header - 接收方用
otel.GetTextMapPropagator().Extract()从 header 提取并重建 baggage - Baggage 值默认不参与采样决策,但能被日志、指标系统读取,适合传递轻量上下文
- 注意 baggage 键名长度限制(通常 ≤256 字符),过长会被截断
最易被忽略的一点:链路不是“配好 exporter 就自动通”。哪怕用了 otelhttp.NewHandler(),只要 client 端没用 WithContext(),或者中间某层自定义 http.Client 忘了 propagate,链路就在那断成两截——而且没有任何错误提示,只有空白 span。


















