2. OpenTelemetry 基础:Trace、Span、SpanContext、TraceId、SpanId 核心概念
在将日志与链路关联之前,必须先理解 OpenTelemetry 的数据模型。OpenTelemetry 是 CNCF 孵化的可观测性标准,它定义了 Trace(追踪)、Span(跨度)、SpanContext(跨度上下文)、TraceId(追踪 ID) 和 SpanId(跨度 ID) 五个核心概念。这些概念构成了分布式追踪的骨架,也是后续日志索引关联的基石。
2.1 核心概念速览
| 概念 | 英文 | 一句话定义 | 在 Loki 关联中的角色 |
|---|---|---|---|
| 追踪 | Trace | 一次完整请求从入口到出口的全链路记录 | 作为日志分组的逻辑边界 |
| 跨度 | Span | 追踪中的一个独立工作单元(如一次数据库查询) | 日志中记录的最小可观测单元 |
| 跨度上下文 | SpanContext | 跨进程传递追踪状态的载体 | 确保日志与追踪在同一请求内对齐 |
| 追踪 ID | TraceId | 全局唯一的 128 位标识符,标记一次追踪 | Loki 日志索引的主键字段 |
| 跨度 ID | SpanId | 64 位标识符,标记追踪内的一个跨度 | 日志中定位具体操作步骤的辅助键 |
关键认知:TraceId 是“绳子”,把分散在不同服务、不同时间点的日志串成一条完整的请求故事;SpanId 是“绳结”,标记故事中的每个关键动作。
2.2 Trace(追踪)—— 请求的全景图
Trace 代表一个端到端的请求流。当用户点击一个按钮或调用一个 API 时,这个请求可能会经过网关、认证服务、订单服务、支付服务、数据库等多个组件。Trace 就是这些组件交互的完整记录。
- 生命周期:从请求进入系统开始,到响应返回给客户端结束。
- 唯一标识:每个 Trace 有且只有一个 TraceId。
- 结构:由多个 Span 组成树状结构(DAG,有向无环图)。
- 在 Loki 中的意义:通过 TraceId 可以聚合所有相关日志,实现“一次搜索,全链路可见”。
2.3 Span(跨度)—— 追踪的最小工作单元
Span 是 Trace 的组成部分,代表一个具有开始时间和结束时间的操作。每个 Span 记录了一个特定操作(如 HTTP 请求、数据库查询、消息队列发送)的详细信息。
- 属性:Span 名称、开始时间戳、结束时间戳、状态(OK/Error)、属性(Tags)、事件(Events)。
- 关系:Span 之间通过 Parent Span ID 建立父子关系,形成 Trace 的树状结构。
- 在 Loki 中的意义:每条日志可以关联一个 SpanId,从而精确知道这条日志属于哪个操作步骤。
实战要点:在微服务架构中,一个请求通常会产生 10~50 个 Span。每个 Span 的开始和结束都可以输出一条日志,配合 SpanId 即可在 Loki 中还原出该 Span 的完整执行时间线。
2.4 SpanContext(跨度上下文)—— 跨进程传递的“信使”
SpanContext 是 OpenTelemetry 中用于跨进程传播追踪信息的对象。它包含了在当前 Span 中识别后续 Span 所需的所有元数据。
| SpanContext 字段 | 说明 | 是否必须 |
|---|---|---|
| TraceId | 当前追踪的全局唯一 ID | 是 |
| SpanId | 当前 Span 的 ID | 是 |
| TraceFlags | 追踪选项(如是否采样) | 是 |
| TraceState | 供应商特定的追踪元数据 | 否 |
SpanContext 通过 HTTP 头(如 traceparent)或 gRPC 元数据在服务之间传递。当服务 A 调用服务 B 时,服务 A 将 SpanContext 序列化后附加到请求中,服务 B 反序列化后创建子 Span,从而保证 TraceId 一致。
2.5 TraceId 与 SpanId —— 关联日志的“钥匙”
TraceId 和 SpanId 是 OpenTelemetry 中最核心的两个标识符,也是与 Loki 日志索引关联的直接桥梁。
TraceId(追踪 ID)
- 格式:128 位,通常表示为 32 个十六进制字符(如
0af7651916cd43dd8448eb211c80319c)。 - 生成规则:由入口服务(如 API 网关)在请求开始时生成,全局唯一。
- 在 Loki 中的用法:作为日志的索引标签(Label),实现“一次 TraceId 搜索,召回全链路日志”。
SpanId(跨度 ID)
- 格式:64 位,通常表示为 16 个十六进制字符(如
a3f4b8c9d0e1f2a3)。 - 生成规则:每个 Span 在创建时自动生成,在 Trace 内唯一。
- 在 Loki 中的用法:作为日志的附加字段,用于在 TraceId 结果集中进一步过滤到具体操作。
2.6 代码示例:在 Go 中创建 Trace 和 Span
以下代码展示了如何使用 OpenTelemetry Go SDK 创建一个 Trace 及其子 Span,并获取 TraceId 和 SpanId:
package main
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
func main() {
// 创建 Tracer
tracer := otel.Tracer("example-tracer")
// 创建根 Span(代表一个 Trace 的起点)
ctx, rootSpan := tracer.Start(context.Background(), "root-operation")
defer rootSpan.End()
// 获取 TraceId 和 SpanId
rootSpanContext := rootSpan.SpanContext()
traceID := rootSpanContext.TraceID().String()
spanID := rootSpanContext.SpanID().String()
fmt.Printf("TraceId: %s\n", traceID)
fmt.Printf("SpanId: %s\n", spanID)
// 创建子 Span(代表 Trace 内的一个子操作)
_, childSpan := tracer.Start(ctx, "child-operation")
defer childSpan.End()
childSpanContext := childSpan.SpanContext()
childSpanID := childSpanContext.SpanID().String()
fmt.Printf("Child SpanId: %s\n", childSpanID)
// 输出示例:
// TraceId: 0af7651916cd43dd8448eb211c80319c
// SpanId: a3f4b8c9d0e1f2a3
// Child SpanId: b4c5d6e7f8a9b0c1
}
常见误区:不要将 TraceId 和 SpanId 混淆。TraceId 在整个请求生命周期内保持不变,用于关联所有相关日志;SpanId 在每个操作步骤中变化,用于区分日志属于哪个具体操作。在 Loki 中,TraceId 应作为索引标签,SpanId 作为日志内容字段即可,不需要为 SpanId 建立索引。
2.7 本章小结
- Trace 是请求的全景图,由多个 Span 组成树状结构。
- Span 是追踪的最小工作单元,记录单个操作的开始、结束和状态。
- SpanContext 是跨进程传递追踪信息的载体,包含 TraceId、SpanId 等关键字段。
- TraceId 是 128 位全局唯一标识,用于在 Loki 中聚合全链路日志。
- SpanId 是 64 位标识,用于在 Trace 内定位具体操作步骤的日志。