Chapter 02

一条 Trace 的构造

Trace 是一棵树,Span 是树上的节点。每个节点带 trace_idspan_id,父子关系靠 parent_span_id 拼接。学会这几个概念,你就能读懂 Jaeger 上任何一条瀑布图。

核心概念

Trace
一次完整的请求链路,贯穿多个服务。所有 span 共享同一个 trace_id(128 位十六进制字符串)。
Span
Trace 内的一个操作单元:一次 HTTP 调用、一次 DB 查询、一段业务逻辑。带 span_idparent_span_id、起止时间、状态、属性、事件。
SpanContext
span 的"名片"——只含 trace_id + span_id + trace_flags + trace_state,用于跨进程传递(放在 HTTP header 里)。
Tracer
创建 span 的工厂。每个服务(或模块)通常有一个 tracer,通过 getTracer(name, version) 获取。
Baggage
和 trace 一起传递的业务键值对(如 user.id=123)。可以在下游 span 里读到——用于传业务上下文,但不要滥用(每个字段都会进 HTTP header)。

一个 span 的原始结构

{
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "parent_span_id": "00f067aa0ba902b6",
  "name": "HTTP GET /users/:id",
  "kind": "SERVER",
  "start_time": "2026-05-06T10:00:00.000Z",
  "end_time": "2026-05-06T10:00:00.042Z",
  "status": { "code": "OK" },
  "attributes": {
    "http.method": "GET",
    "http.status_code": 200,
    "user.id": "u_123"
  },
  "events": [
    { "time": "2026-05-06T10:00:00.010Z", "name": "cache_miss" }
  ]
}

核心字段就这些——OTLP 里 span 的二进制编码,实际上就是这个 JSON 结构的 Protobuf 版。

Span 的 kind(种类)

kind含义举例
SERVER接收外部请求的入口HTTP handler、gRPC server method
CLIENT向下游发起请求fetch、DB 查询、Redis 命令
PRODUCER向消息队列发消息Kafka publish
CONSUMER从消息队列消费Kafka consume、Worker 接任务
INTERNAL服务内部操作一段纯业务逻辑、缓存读

kind 决定了后端如何展示——SERVER 和 CLIENT 配对出现就是"外部调用",Consumer/Producer 配对出现就是异步消息。

父子关系

trace_id = T
├─ span A (root, parent_span_id=null)   kind=SERVER
│  ├─ span B                            kind=CLIENT  → HTTP /inventory
│  │  └─ span C (不同进程,parent=B)      kind=SERVER  → 收到 /inventory
│  │     └─ span D                      kind=CLIENT  → SELECT ...
│  └─ span E                            kind=INTERNAL → 业务计算

关键观察:B 和 C 不在同一个进程里——这就需要下一章的 Context 传播。OTel 会把 traceparent header 塞到 HTTP 请求里,下游解析后继续以 parent=B 创建自己的 span。

Node.js:第一个 tracer

pnpm add @opentelemetry/api @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http
// tracing.ts  —  在主程序启动前 import
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { Resource } from "@opentelemetry/resources";

const sdk = new NodeSDK({
  resource: new Resource({
    "service.name": "order-service",
    "service.version": "1.2.0",
  }),
  traceExporter: new OTLPTraceExporter({
    url: "http://localhost:4318/v1/traces",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

// package.json 启动:node -r ./tracing.ts app.ts
// HTTP / fs / pg / mysql / redis / mongodb 都会被自动埋点

启动后随便发一个 HTTP 请求——Jaeger(http://localhost:16686)里就能看到完整的调用链,一行代码都不用改。

手动创建 span

import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("order-service", "1.2.0");

async function placeOrder(userId: string, items: Item[]) {
  return tracer.startActiveSpan("placeOrder", async (span) => {
    span.setAttribute("user.id", userId);
    span.setAttribute("order.item_count", items.length);

    try {
      const order = await createOrder(userId, items);  // 内部的 DB 查询自动成子 span
      span.setAttribute("order.id", order.id);
      span.addEvent("order_created", { total: order.total });
      span.setStatus({ code: SpanStatusCode.OK });
      return order;
    } catch (e) {
      span.recordException(e as Error);
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: (e as Error).message,
      });
      throw e;
    } finally {
      span.end();   // 不调用 end 就不会上报!
    }
  });
}
头号大坑
忘记 span.end() 的 span 不会被 exporter 处理。永远把 end() 放在 finally 里——异步场景尤其容易漏。或者用 startActiveSpan + 返回 Promise 自动管理。

Python:等价写法

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install   # 自动装上已安装库的 instrumentation
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer("order-service", "1.2.0")

def place_order(user_id: str, items: list):
    with tracer.start_as_current_span("place_order") as span:
        span.set_attribute("user.id", user_id)
        span.set_attribute("order.item_count", len(items))
        try:
            order = create_order(user_id, items)
            span.add_event("order_created", {"total": order.total})
            return order
        except Exception as e:
            span.record_exception(e)
            span.set_status(Status(StatusCode.ERROR, str(e)))
            raise
    # with 块自动调用 span.end()

# 启动:opentelemetry-instrument python app.py
# 会自动给 flask/django/requests/sqlalchemy/... 加上埋点

语言不同,模式一致:start span → setAttribute/addEvent → 失败 recordException → setStatus → end。

Attributes 命名:用语义约定

不要随便写 attribute key——OTel 有一套 Semantic Conventions(第 8 章详讲)。常见的:

领域key示例
HTTPhttp.method / http.status_code / http.routeGET / 200 / /users/:id
DBdb.system / db.statement / db.namepostgresql / SELECT ... / orders
RPCrpc.system / rpc.service / rpc.methodgrpc / UserService / GetUser
消息messaging.system / messaging.destinationkafka / order.created

用标准 key 的好处:Jaeger/Datadog/Grafana 都有基于这些字段的开箱即用面板。你自己造 my_status_code 后端就不认识了。

Span Links:跨 trace 关联

父子是一棵树,但有些场景需要"A 引用了 B,但 A 不是 B 的父":批处理里一个 job span 关联了 N 条消息的 span。

tracer.startActiveSpan("process_batch", {
  links: messages.map((m) => ({
    context: m.spanContext,   // 来自消息里的 traceparent
    attributes: { "messaging.message_id": m.id },
  })),
}, async (span) => { /* ... */ });

在 Jaeger 上 link 会显示成虚线箭头——便于追溯异步场景的因果关系。

Status 三态

约定
http.status_code=500 自动让 span 状态变 ERROR(按规范)——你不需要重复设置。但业务逻辑的失败(如库存不足)需要显式 setStatus(ERROR),光看 HTTP 200 是看不出来的。

Baggage:业务上下文传递

import { propagation, context } from "@opentelemetry/api";

// 入口:把 tenant_id 放进 baggage
const baggage = propagation
  .createBaggage({ "tenant.id": { value: "acme" } });
const ctx = propagation.setBaggage(context.active(), baggage);

// 下游任意位置读
const entry = propagation
  .getActiveBaggage()?.getEntry("tenant.id");
console.log(entry?.value);   // "acme"
Baggage 是把双刃剑
它会进每一个出站 HTTP 请求的 header——加多了会撑爆 header 大小、泄露 PII。只放确实需要跨进程的业务 ID(tenant、experiment、feature flag),不要放大对象或敏感数据。

Sampling 简介

每个请求都上报 span 会把后端压垮。OTel 支持多种采样策略:

AlwaysOn / AlwaysOff
开发调试用。生产千万别 AlwaysOn。
TraceIdRatioBased(0.1)
按 trace_id 哈希取 10%——同一 trace 在所有服务里结果一致(否则链路会断)。
ParentBased
尊重上游决定——如果父 span 被采样,子 span 一定被采样。实际中 ParentBased(TraceIdRatioBased(0.1)) 最常用。
Tail-based(Collector 层)
收到整条 trace 再决定——可以"只保留出错或慢的 trace"。但需要 Collector 缓冲整条链路。第 7 章详讲。

本章小结