Span是Trace的原子单元,必须依附于Trace存在;依赖traceID保证全局唯一,spanID和parentSpanID构建父子拓扑;脱离上下文传递或错误设置SpanKind会导致链路断裂或分析失真。

Span 是 Trace 的原子单元,不是独立存在
一个 Trace 必须至少包含一个 Span,但 Span 无法脱离 Trace 生存——它依赖 traceID 建立全局唯一性,靠 spanID 和 parentSpanID 构建父子拓扑。没有 traceID 的 Span 在上报时会被丢弃或降级为 noop。
常见错误现象:
- 手动构造
Span但未从context.Context中提取或注入上下文 → 上报后各服务 Span 显示为孤立节点 - 用
otel.Tracer("x").Start(ctx, "y")时传入空ctx(如context.Background())→ 新建根Trace,打断原有调用链 - HTTP 客户端未调用
propagator.Inject()→ 下游服务收不到traceparent头,无法延续Trace
父子 Span 必须通过 context 传递,不能靠参数或全局变量
Span 的父子关系不是靠传 span 变量实现的,而是靠 context.Context 携带 SpanContext 向下流动。OpenTelemetry 的 Tracer.Start() 方法会自动从入参 ctx 中提取父上下文,并在返回的新 ctx 中注入当前 Span 的上下文。
实操要点:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 服务端入口:用
propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))从 HTTP header 恢复父上下文 - 客户端出口:用
propagator.Inject(spanCtx, propagation.HeaderCarrier(req.Header))把当前Span的上下文写入请求头 - 本地异步任务(如 goroutine):必须显式传入带
Span的ctx,不能直接用context.Background() - 数据库调用、Redis 等中间件埋点:需确保其上下文来自上游
Span,而非新建
Span.Kind 决定 span 在 UI 中的语义分类,影响采样与告警逻辑
SpanKind 不是装饰字段,它直接影响后端分析行为。例如 Jaeger 和 Tempo 对 SpanKindServer 和 SpanKindClient 做不同着色;某些采样策略会按 SpanKind 设置不同采样率。
典型取值与使用场景:
-
oteltrace.SpanKindServer:HTTP handler、gRPC server 方法 —— 必须设,否则无法识别服务端入口 -
oteltrace.SpanKindClient:HTTP client.Do、gRPC client 调用 —— 用于标记出向调用 -
oteltrace.SpanKindConsumer:消息队列消费者(如 Kafka、RabbitMQ) -
oteltrace.SpanKindProducer:消息队列生产者 - 不要对数据库查询硬编码
SpanKindServer,应根据实际角色选Client或Internal
Trace ID 全局一致,但 Span ID 必须每跳唯一,且不可重用
traceID 在一次请求生命周期中全程不变,而每个新 Span 必须生成全新 spanID。重复使用同一 spanID(比如在循环中复用变量)会导致后端聚合异常,表现为 Span 覆盖、时序错乱或丢失。
容易被忽略的细节:
- Go-zero 的
Fork()方法内部调用s.forkSpanId()生成新 ID,这是安全的;但自己手写spanID := s.ctx.spanId就是严重 bug - 测试中 mock tracer 时若固定返回相同
spanID,会导致多个测试用例污染彼此 trace 视图 - 跨 goroutine 共享
Span实例本身不危险,但若多个 goroutine 并发调用span.SetTag(),需注意 OpenTelemetry 默认实现是否线程安全(go.opentelemetry.io/otel/sdk/trace的Span是线程安全的,但自定义 wrapper 可能不是)

















