核心概念
trace_id(128 位十六进制字符串)。span_id、parent_span_id、起止时间、状态、属性、事件。trace_id + span_id + trace_flags + trace_state,用于跨进程传递(放在 HTTP header 里)。getTracer(name, version) 获取。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 | 示例 |
|---|---|---|
| HTTP | http.method / http.status_code / http.route | GET / 200 / /users/:id |
| DB | db.system / db.statement / db.name | postgresql / SELECT ... / orders |
| RPC | rpc.system / rpc.service / rpc.method | grpc / UserService / GetUser |
| 消息 | messaging.system / messaging.destination | kafka / 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 三态
UNSET(默认):span 没显式设置状态,后端通常展示为中性OK:显式表示成功——你确认没有错误ERROR:失败,message字段可以带原因
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"
它会进每一个出站 HTTP 请求的 header——加多了会撑爆 header 大小、泄露 PII。只放确实需要跨进程的业务 ID(tenant、experiment、feature flag),不要放大对象或敏感数据。
Sampling 简介
每个请求都上报 span 会把后端压垮。OTel 支持多种采样策略:
本章小结
- Trace 是树,Span 是节点;trace_id 128 位,span_id 64 位
- Span 五要素:name / kind / start-end time / attributes / status
- 自动 instrumentation 让你零改动拿到 HTTP/DB/Redis 的 span
- 手动埋点模式:startActiveSpan → setAttribute → recordException → setStatus → end(finally)
- Baggage 用于业务上下文传递,克制使用
- 采样:头部 ParentBased(TraceIdRatio) 入门,尾部在 Collector 做