主题
OpenTelemetry Traces 深入学习笔记
对应学习计划阶段 2,预计学习时间 3-4 天
目录
- 核心概念
- 架构与数据流
- TracerProvider 与 Tracer
- Span 详解
- SpanContext 与传播基础
- Span 属性与语义约定
- SpanProcessor 详解
- 采样策略
- 实战场景分析
- 实践 Demo
- 常见问题 QA
- 学习检查清单
1. 核心概念
1.1 什么是 Trace?
Trace(追踪) 表示一次完整请求在分布式系统中的执行路径。它由多个 Span 组成,形成一棵有向无环图(DAG),通常表现为树结构。
一次用户请求的 Trace:
[Trace: abc123]
├── [Span: API Gateway] 耗时 200ms
│ ├── [Span: Auth Service] 耗时 30ms
│ ├── [Span: User Service] 耗时 80ms
│ │ └── [Span: DB Query] 耗时 25ms
│ └── [Span: Cache Lookup] 耗时 5ms关键特征:
- 一个 Trace 有唯一的
trace_id(128 位,32 个十六进制字符) - Trace 中所有 Span 共享同一个
trace_id - Trace 本身不是一个显式对象,它由 Span 的集合隐式定义
1.2 什么是 Span?
Span 是 Trace 中的基本工作单元,代表一个有时间范围的操作。每个 Span 包含:
| 字段 | 说明 | 示例 |
|---|---|---|
name | 操作名称 | "GET /api/users" |
span_id | 唯一标识(64 位) | "6e0c63257de34c92" |
trace_id | 所属 Trace 的 ID | "5b8aa5a2d2c872e8..." |
parent_span_id | 父 Span 的 ID | "051581bf3cb55c13" |
start_time | 开始时间戳 | 2025-01-01T10:00:00Z |
end_time | 结束时间戳 | 2025-01-01T10:00:00.2Z |
status | 状态码 | OK, ERROR, UNSET |
kind | Span 类型 | SERVER, CLIENT 等 |
attributes | 键值对属性 | {"http.method": "GET"} |
events | 时间点事件列表 | 异常记录、日志标记 |
links | 关联其他 Span | 批处理中的关联 |
1.3 SpanKind(Span 类型)
SpanKind 描述了 Span 在分布式调用中的角色:
| SpanKind | 含义 | 使用场景 |
|---|---|---|
INTERNAL | 内部操作,无远程调用 | 本地函数调用、内部处理逻辑 |
SERVER | 处理远程请求的服务端 | HTTP 服务器处理请求、gRPC 服务端 |
CLIENT | 发起远程请求的客户端 | HTTP 客户端、数据库客户端、gRPC 客户端 |
PRODUCER | 异步消息的生产者 | 消息队列生产者(Kafka、RabbitMQ) |
CONSUMER | 异步消息的消费者 | 消息队列消费者 |
CLIENT / SERVER 配对示例:
Service A Service B
┌──────────────────┐ ┌──────────────────┐
│ [Span: CLIENT] │ ──HTTP──> │ [Span: SERVER] │
│ kind=CLIENT │ │ kind=SERVER │
│ name="GET /users"│ │ name="GET /users"│
└──────────────────┘ └──────────────────┘
同一个 trace_id,不同的 span_idPRODUCER / CONSUMER 配对示例:
Service A Queue Service B
┌─────────────────┐ ┌────────┐ ┌──────────────────┐
│ [Span: PRODUCER]│──msg──│ Kafka │──consume──│ [Span: CONSUMER] │
│ kind=PRODUCER │ │ │ │ kind=CONSUMER │
└─────────────────┘ └────────┘ └──────────────────┘
可以是同一 trace_id(通过 Context 传播),也可以用 Link 关联1.4 Span 之间的关系
Parent-Child(父子关系)
最常见的关系,子 Span 在父 Span 的执行期间创建:
python
with tracer.start_as_current_span("parent") as parent:
# child 自动成为 parent 的子 Span
with tracer.start_as_current_span("child") as child:
pass特征:
- 子 Span 的
parent_span_id指向父 Span 的span_id - 子 Span 的生命周期通常在父 Span 之内
- 通过 Context 自动建立
Link(链接关系)
用于关联没有直接父子关系但逻辑相关的 Span:
python
# 场景:批处理任务关联到触发它的多个请求
link1 = trace.Link(span_context_of_request_1)
link2 = trace.Link(span_context_of_request_2)
with tracer.start_as_current_span("batch-process", links=[link1, link2]):
process_batch()典型使用场景:
- 批处理:一个批量操作关联多个触发请求
- 消息队列:Consumer 关联 Producer 的 Span
- 多 Trace 关联:跨 Trace 的因果关系
1.5 Span Status(状态)
| 状态码 | 含义 | 使用场景 |
|---|---|---|
UNSET | 默认状态,未显式设置 | 操作正常完成时通常不需设置 |
OK | 操作成功 | 显式标记成功(一般不需要,UNSET 即可) |
ERROR | 操作出错 | 捕获异常、业务错误 |
重要原则: 只在确实出错时设置 ERROR,正常情况下保持 UNSET 即可。HTTP 4xx 通常不应标记为 ERROR(因为这是客户端问题,不是服务端错误)。
2. 架构与数据流
2.1 Trace 数据流全景
应用代码
│
▼
┌──────────┐ ┌───────────────┐ ┌──────────┐ ┌─────────┐
│ Tracer │────▶│ SpanProcessor │────▶│ Exporter │────▶│ Backend │
│ (API) │ │ (SDK) │ │ (SDK) │ │(Jaeger) │
└──────────┘ └───────────────┘ └──────────┘ └─────────┘
▲ │
│ ▼
┌──────────────┐ ┌──────────────┐
│TracerProvider│ │ 可视化 UI │
│ + Resource │ │ (查询/分析) │
│ + Sampler │ └──────────────┘
└──────────────┘2.2 核心组件职责
| 组件 | 职责 | 类比 |
|---|---|---|
TracerProvider | 管理 Tracer 的工厂,持有配置 | 印刷厂(持有设备和配置) |
Tracer | 创建 Span 的入口 | 印刷机(生产产品) |
Span | 记录操作信息的载体 | 印刷品(承载内容) |
SpanProcessor | 处理 Span 的管道(导出前) | 质检和包装线 |
SpanExporter | 将 Span 发送到后端 | 物流配送 |
Resource | 标识产生 Span 的实体信息 | 工厂的营业执照 |
Sampler | 决定是否采集 Span | 抽样检查员 |
2.3 API vs SDK 分层
┌────────────────────────────────────────────┐
│ 应用代码 │
├────────────────────────────────────────────┤
│ opentelemetry-api │ ← 只定义接口,无实现
│ trace.get_tracer() │ 可以安全引入,
│ tracer.start_as_current_span() │ 没有 SDK 时生成 NoOp
├────────────────────────────────────────────┤
│ opentelemetry-sdk │ ← 实际实现
│ TracerProvider, BatchSpanProcessor │ 处理采集、采样、导出
│ Resource, Sampler │
├────────────────────────────────────────────┤
│ opentelemetry-exporter-xxx │ ← 具体导出实现
│ OTLP, Jaeger, Zipkin, Console │
└────────────────────────────────────────────┘分层设计的意义:
- 库作者只依赖
api包添加 instrumentation,不关心用户选择哪个 SDK/Exporter - 应用开发者在启动时配置 SDK 和 Exporter
- 如果没有安装 SDK,API 调用会自动变成 No-Op(零开销)
3. TracerProvider 与 Tracer
3.1 TracerProvider 初始化
TracerProvider 是整个 Tracing 系统的核心入口,通常在应用启动时全局配置一次。
python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
SimpleSpanProcessor,
ConsoleSpanExporter,
)
from opentelemetry.sdk.resources import Resource
# ==============================
# 步骤 1:创建 Resource
# ==============================
# Resource 描述了产生遥测数据的实体(服务)信息
# 所有从这个 Provider 创建的 Span 都会携带这些信息
resource = Resource.create({
"service.name": "order-service", # 必填:服务名
"service.version": "2.1.0", # 服务版本
"service.namespace": "shop", # 服务命名空间
"deployment.environment": "production", # 部署环境
"host.name": "worker-node-3", # 主机名
})
# ==============================
# 步骤 2:创建 TracerProvider
# ==============================
provider = TracerProvider(
resource=resource,
# sampler=..., # 可选,后面详讲
)
# ==============================
# 步骤 3:添加 SpanProcessor
# ==============================
# 开发环境:SimpleSpanProcessor + ConsoleSpanExporter(同步输出到控制台)
if is_dev:
provider.add_span_processor(
SimpleSpanProcessor(ConsoleSpanExporter())
)
# 生产环境:BatchSpanProcessor + OTLPExporter(异步批量发送)
if is_prod:
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="collector:4317", insecure=True),
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
# ==============================
# 步骤 4:设置为全局 Provider
# ==============================
trace.set_tracer_provider(provider)3.2 获取 Tracer
python
# 获取 Tracer,参数为 instrumentation name 和 version
# 建议使用模块名作为 name,便于后续在后端按来源过滤
tracer = trace.get_tracer(
instrumenting_module_name="my_app.order_service", # 必填
instrumenting_library_version="0.1.0", # 可选
schema_url="https://opentelemetry.io/schemas/1.11.0", # 可选
)Tracer 的命名约定:
- 使用反向域名或包名:
com.mycompany.service.orders - 使用 Python 模块名:
my_app.order_service - 自动 instrumentation 库使用库名:
opentelemetry.instrumentation.fastapi
3.3 Resource 详解
Resource 是一组描述产生遥测数据的实体的键值对,对于后端的数据组织和查询至关重要。
python
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
# 方式 1:直接创建
resource = Resource.create({
SERVICE_NAME: "my-service",
"service.version": "1.0.0",
})
# 方式 2:合并多个 Resource
base_resource = Resource.create({"service.name": "my-service"})
extra_resource = Resource.create({"cloud.provider": "aws", "cloud.region": "us-east-1"})
merged = base_resource.merge(extra_resource)
# 方式 3:通过环境变量
# export OTEL_RESOURCE_ATTRIBUTES="service.name=my-service,service.version=1.0.0"
# SDK 会自动读取常用 Resource 属性(语义约定):
| 属性 | 说明 | 示例 |
|---|---|---|
service.name | 服务名称(必填) | "order-service" |
service.version | 服务版本 | "2.1.0" |
service.namespace | 服务命名空间 | "shop" |
deployment.environment | 部署环境 | "production" |
telemetry.sdk.name | SDK 名称(自动填充) | "opentelemetry" |
telemetry.sdk.language | SDK 语言(自动填充) | "python" |
host.name | 主机名 | "worker-01" |
k8s.pod.name | K8s Pod 名 | "order-svc-abc123" |
3.4 优雅关闭
python
import atexit
import signal
provider = TracerProvider(resource=resource)
# 方式 1:使用 atexit
atexit.register(provider.shutdown)
# 方式 2:信号处理(更精细的控制)
def graceful_shutdown(signum, frame):
provider.shutdown() # 会调用所有 processor 的 shutdown,flush 未导出的 Span
sys.exit(0)
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)
# 方式 3:在 FastAPI 的 lifespan 中
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
yield
provider.shutdown()
app = FastAPI(lifespan=lifespan)4. Span 详解
4.1 创建 Span 的方式
方式 1:上下文管理器(推荐)
python
# 自动管理 Span 的开始和结束,异常时自动记录
with tracer.start_as_current_span("process-order") as span:
span.set_attribute("order.id", "ORD-12345")
result = process_order()
span.set_attribute("order.total", result.total)
# Span 在退出 with 块时自动结束方式 2:装饰器
python
# 将整个函数封装为一个 Span
@tracer.start_as_current_span("calculate-discount")
def calculate_discount(order):
# 函数体内可以通过 trace.get_current_span() 获取当前 Span
span = trace.get_current_span()
span.set_attribute("discount.type", "percentage")
return order.total * 0.9方式 3:手动管理(需要更精细控制时)
python
# 手动开始和结束 Span
span = tracer.start_span("manual-operation")
try:
# 如果需要将 span 设为 current(让子操作能自动找到父 Span)
ctx = trace.set_span_in_context(span)
token = context.attach(ctx)
try:
do_something()
finally:
context.detach(token)
except Exception as e:
span.set_status(trace.StatusCode.ERROR, str(e))
span.record_exception(e)
raise
finally:
span.end() # 必须手动结束!方式 4:指定 SpanKind
python
from opentelemetry.trace import SpanKind
# 作为 HTTP 服务端
with tracer.start_as_current_span("handle-request", kind=SpanKind.SERVER) as span:
span.set_attribute("http.method", "POST")
span.set_attribute("http.route", "/api/orders")
# 作为 HTTP 客户端
with tracer.start_as_current_span("call-payment-service", kind=SpanKind.CLIENT) as span:
span.set_attribute("http.method", "POST")
span.set_attribute("http.url", "http://payment-svc/charge")
response = requests.post("http://payment-svc/charge", json=data)
span.set_attribute("http.status_code", response.status_code)4.2 嵌套 Span(自动父子关系)
python
# OpenTelemetry 通过 Context 自动维护 Span 的父子关系
def handle_order_request(order_data):
with tracer.start_as_current_span("handle-order") as root_span:
root_span.set_attribute("order.id", order_data["id"])
# 自动成为 handle-order 的子 Span
with tracer.start_as_current_span("validate-order") as validate_span:
validate_order(order_data)
# 自动成为 handle-order 的子 Span(与 validate-order 平级)
with tracer.start_as_current_span("save-order") as save_span:
# 自动成为 save-order 的子 Span
with tracer.start_as_current_span("db-insert") as db_span:
db_span.set_attribute("db.system", "postgresql")
db_span.set_attribute("db.statement", "INSERT INTO orders ...")
save_to_db(order_data)
with tracer.start_as_current_span("send-notification"):
notify_user(order_data["user_id"])生成的 Span 树结构:
[handle-order] # root
├── [validate-order] # child 1
├── [save-order] # child 2
│ └── [db-insert] # grandchild
└── [send-notification] # child 34.3 Span Events(事件)
Events 是 Span 内的时间点标记,类似于结构化日志:
python
with tracer.start_as_current_span("process-payment") as span:
# 添加简单事件
span.add_event("payment.started")
# 添加带属性的事件
span.add_event("payment.gateway.request", {
"gateway": "stripe",
"amount": 99.99,
"currency": "USD",
})
result = call_payment_gateway()
if result.retry_count > 0:
span.add_event("payment.retried", {
"retry_count": result.retry_count,
"retry_reason": result.last_error,
})
# 添加带自定义时间戳的事件
import time
span.add_event(
"payment.completed",
{"transaction_id": result.txn_id},
timestamp=int(time.time_ns()),
)Events vs Attributes 的选择:
| 特征 | Attributes | Events |
|---|---|---|
| 数据模型 | 键值对 | 名称 + 时间戳 + 属性 |
| 时间性 | 描述整个 Span | 描述某个时间点 |
| 数量 | 一个 key 一个值 | 可以多个同名事件 |
| 适用场景 | http.status_code=200 | cache.miss at T1 |
4.4 记录异常
python
with tracer.start_as_current_span("risky-operation") as span:
try:
result = perform_operation()
except ValueError as e:
# record_exception 会自动添加一个 "exception" 事件
# 包含 exception.type, exception.message, exception.stacktrace
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR, f"ValueError: {e}")
raise
except ConnectionError as e:
span.record_exception(e, attributes={
"exception.escaped": True, # 异常是否会传播到外部
"retry.attempt": retry_count, # 自定义的异常属性
})
span.set_status(trace.StatusCode.ERROR, "Connection failed")
raiserecord_exception 做了什么?
等效于:
python
span.add_event("exception", {
"exception.type": type(e).__name__, # "ValueError"
"exception.message": str(e), # "invalid order"
"exception.stacktrace": traceback.format_exc(), # 完整堆栈
})4.5 Span 命名最佳实践
| 场景 | 好的命名 | 不好的命名 |
|---|---|---|
| HTTP 服务端 | GET /api/users/{id} | GET /api/users/12345 |
| 数据库查询 | SELECT orders | SELECT * FROM orders WHERE id=1 |
| RPC 调用 | grpc.UserService/GetUser | call GetUser for uid=abc |
| 消息消费 | process orders.created | process msg #4567 |
核心原则: Span 名应是 低基数(low cardinality) 的,不要包含变量值(ID、参数等),这些应放在 Attributes 中。
5. SpanContext 与传播基础
5.1 SpanContext 结构
SpanContext 是跨进程传播 Trace 信息的最小数据集:
python
span = trace.get_current_span()
ctx = span.get_span_context()
print(f"trace_id: {ctx.trace_id:032x}") # 128 位,32 个十六进制字符
print(f"span_id: {ctx.span_id:016x}") # 64 位,16 个十六进制字符
print(f"trace_flags: {ctx.trace_flags:02x}") # 8 位,01 = 采样
print(f"trace_state: {ctx.trace_state}") # 厂商特定的追踪状态
print(f"is_remote: {ctx.is_remote}") # 是否来自远程进程
print(f"is_valid: {ctx.is_valid}") # 是否有效5.2 W3C TraceContext(传播格式)
当 Trace 跨服务传播时,SpanContext 被编码到 HTTP 头中:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-^^^^^^^^^^^^^^^^-^^
│ │ │ │
│ trace-id (32 hex) span-id (16 hex) trace-flags
version 01=sampledpython
# 这就是 Context Propagation 的基础(阶段 5 深入学习)
# 简单预览:
from opentelemetry.propagate import inject, extract
# 发送端:将当前 Span 信息注入 HTTP 头
headers = {}
inject(headers)
# headers = {"traceparent": "00-abc...-def...-01"}
# 接收端:从 HTTP 头恢复 Span 信息
ctx = extract(request.headers)
with tracer.start_as_current_span("handle", context=ctx, kind=SpanKind.SERVER):
pass # 这个 Span 会自动成为远程 Span 的子节点6. Span 属性与语义约定
6.1 设置属性
python
with tracer.start_as_current_span("operation") as span:
# 支持的属性值类型:str, bool, int, float, 以及它们的序列
span.set_attribute("http.method", "GET") # str
span.set_attribute("http.status_code", 200) # int
span.set_attribute("http.request.size", 1024.5) # float
span.set_attribute("http.retry", False) # bool
span.set_attribute("cache.regions", ["us-east", "eu"]) # Sequence[str]
# 也可以在创建时批量设置
with tracer.start_as_current_span(
"db-query",
attributes={
"db.system": "postgresql",
"db.name": "orders_db",
"db.statement": "SELECT * FROM orders WHERE status = ?",
}
) as db_span:
pass6.2 语义约定(Semantic Conventions)
OpenTelemetry 定义了标准化的属性命名规范,确保不同语言/框架的 Span 数据一致。
HTTP 相关
python
# HTTP 服务端(SERVER Span)
span.set_attribute("http.method", "POST")
span.set_attribute("http.scheme", "https")
span.set_attribute("http.target", "/api/v2/orders")
span.set_attribute("http.route", "/api/v2/orders") # 路由模板
span.set_attribute("http.status_code", 201)
span.set_attribute("http.request_content_length", 256)
span.set_attribute("http.response_content_length", 128)
span.set_attribute("http.user_agent", "Mozilla/5.0...")
span.set_attribute("net.host.name", "api.example.com")
span.set_attribute("net.host.port", 443)
# HTTP 客户端(CLIENT Span)
span.set_attribute("http.method", "GET")
span.set_attribute("http.url", "https://payment.example.com/charge")
span.set_attribute("http.status_code", 200)
span.set_attribute("net.peer.name", "payment.example.com")
span.set_attribute("net.peer.port", 443)数据库相关
python
span.set_attribute("db.system", "postgresql") # 数据库类型
span.set_attribute("db.name", "orders_db") # 数据库名
span.set_attribute("db.user", "app_user") # 用户名
span.set_attribute("db.statement", "SELECT * FROM orders WHERE id = ?") # 语句
span.set_attribute("db.operation", "SELECT") # 操作类型
span.set_attribute("db.sql.table", "orders") # 表名
span.set_attribute("net.peer.name", "db-host.internal")
span.set_attribute("net.peer.port", 5432)RPC / gRPC 相关
python
span.set_attribute("rpc.system", "grpc")
span.set_attribute("rpc.service", "UserService")
span.set_attribute("rpc.method", "GetUser")
span.set_attribute("rpc.grpc.status_code", 0) # OK消息队列相关
python
span.set_attribute("messaging.system", "kafka")
span.set_attribute("messaging.destination", "orders.created")
span.set_attribute("messaging.destination_kind", "topic")
span.set_attribute("messaging.operation", "receive")
span.set_attribute("messaging.message_id", "msg-123")
span.set_attribute("messaging.kafka.partition", 3)GenAI / LLM 相关(扩展)
参考 trpc-agent 项目的实际用法:
python
# GenAI 语义约定(社区正在标准化)
span.set_attribute("gen_ai.system", "openai")
span.set_attribute("gen_ai.operation.name", "call_llm")
span.set_attribute("gen_ai.request.model", "gpt-4")
span.set_attribute("gen_ai.usage.input_tokens", 150)
span.set_attribute("gen_ai.usage.output_tokens", 320)
span.set_attribute("gen_ai.tool.name", "search_database")
span.set_attribute("gen_ai.tool.call.id", "call_abc123")6.3 属性最佳实践
- 避免高基数属性:不要用
user.id这样有无限可能值的属性作为后端的索引维度 - 控制属性数量:SDK 默认限制每个 Span 128 个属性
- 属性值长度:避免超长字符串(如完整的 SQL 语句、请求体)
- 敏感数据:不要在属性中存储密码、Token、PII 等敏感信息
python
# ❌ 不好的做法
span.set_attribute("user.password", password)
span.set_attribute("request.body", json.dumps(huge_body)) # 可能很大
span.set_attribute("http.url", f"/users/{user_id}") # 高基数
# ✅ 好的做法
span.set_attribute("user.id", user_id)
span.set_attribute("request.body.size", len(body))
span.set_attribute("http.route", "/users/{id}") # 低基数模板7. SpanProcessor 详解
7.1 SpanProcessor 在数据流中的位置
Span 结束
│
▼
SpanProcessor.on_end(span)
│
├── SimpleSpanProcessor: 立即调用 exporter.export([span])
│
└── BatchSpanProcessor: 放入队列,定时批量调用 exporter.export(batch)7.2 SimpleSpanProcessor
python
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)特点:
- 同步导出:Span 结束时立即调用 Exporter
- 会阻塞应用线程
- 适用场景: 开发调试、单元测试、需要立即看到输出
7.3 BatchSpanProcessor
python
from opentelemetry.sdk.trace.export import BatchSpanProcessor
processor = BatchSpanProcessor(
span_exporter=OTLPSpanExporter(endpoint="collector:4317"),
max_queue_size=2048, # 队列最大容量(默认 2048)
max_export_batch_size=512, # 每次导出的最大批次大小(默认 512)
schedule_delay_millis=5000, # 导出间隔毫秒数(默认 5000)
export_timeout_millis=30000, # 单次导出超时时间(默认 30000)
)
provider.add_span_processor(processor)特点:
- 异步导出:Span 放入内存队列,后台线程定时批量导出
- 不阻塞应用线程
- 适用场景: 生产环境
参数调优指南:
| 参数 | 调小 | 调大 |
|---|---|---|
max_queue_size | 节省内存 | 防止高峰期丢数据 |
max_export_batch_size | 降低单次导出延迟 | 减少网络请求次数 |
schedule_delay_millis | 更快看到数据 | 减少网络开销 |
export_timeout_millis | 快速失败 | 容忍网络波动 |
7.4 多 Processor 链
python
# 可以添加多个 Processor,每个 Span 会经过所有 Processor
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) # 控制台输出
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # 发送到后端7.5 自定义 SpanProcessor
python
from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan
class FilteringSpanProcessor(SpanProcessor):
"""过滤掉健康检查的 Span,只导出业务 Span"""
def __init__(self, next_processor: SpanProcessor):
self._next = next_processor
def on_start(self, span, parent_context=None):
self._next.on_start(span, parent_context)
def on_end(self, span: ReadableSpan):
# 过滤掉健康检查的 Span
if span.name in ("GET /health", "GET /ready"):
return
self._next.on_end(span)
def shutdown(self):
self._next.shutdown()
def force_flush(self, timeout_millis=30000):
return self._next.force_flush(timeout_millis)
# 使用
provider.add_span_processor(
FilteringSpanProcessor(BatchSpanProcessor(OTLPSpanExporter()))
)8. 采样策略
8.1 为什么需要采样?
在高流量系统中,全量采集所有 Trace 会带来:
- 存储成本激增:每秒数千个 Trace,后端存储压力巨大
- 网络带宽消耗:大量遥测数据占用带宽
- 性能影响:虽然 BatchProcessor 是异步的,但创建 Span 对象本身也有开销
采样器在 Span 创建之前 做出决策,被拒绝的 Span 不会产生任何开销(No-Op)。
8.2 内置采样器
python
from opentelemetry.sdk.trace.sampling import (
ALWAYS_ON,
ALWAYS_OFF,
TraceIdRatioBased,
ParentBased,
StaticSampler,
Decision,
)ALWAYS_ON / ALWAYS_OFF
python
# 全部采样(开发/测试环境)
provider = TracerProvider(sampler=ALWAYS_ON)
# 全部不采样(完全禁用 Tracing)
provider = TracerProvider(sampler=ALWAYS_OFF)TraceIdRatioBased
python
# 基于 trace_id 的哈希值决定是否采样,保证同一 Trace 的所有 Span 一致
provider = TracerProvider(
sampler=TraceIdRatioBased(rate=0.1) # 采样 10%
)注意: TraceIdRatioBased 基于 trace_id 做决策,所以同一个 Trace 中的所有 Span 要么全部被采样,要么全部不被采样。
ParentBased(生产推荐)
python
# 根据父 Span 的采样决策来决定子 Span 的采样
# 如果父 Span 被采样了,子 Span 也被采样;反之亦然
# 如果没有父 Span(root Span),使用 root 参数指定的采样器
provider = TracerProvider(
sampler=ParentBased(
root=TraceIdRatioBased(0.1), # 根 Span:10% 采样率
# 以下为可选参数:
# remote_parent_sampled=ALWAYS_ON, # 远程父已采样 → 采样
# remote_parent_not_sampled=ALWAYS_OFF, # 远程父未采样 → 不采样
# local_parent_sampled=ALWAYS_ON, # 本地父已采样 → 采样
# local_parent_not_sampled=ALWAYS_OFF, # 本地父未采样 → 不采样
)
)ParentBased 决策流程:
收到新 Span 创建请求
│
▼
有父 Span?
├── 否 → 使用 root 采样器决策
│
└── 是 → 父 Span 是远程的?
├── 是 → 父 Span 已被采样?
│ ├── 是 → remote_parent_sampled(默认 ALWAYS_ON)
│ └── 否 → remote_parent_not_sampled(默认 ALWAYS_OFF)
│
└── 否 → 父 Span 已被采样?
├── 是 → local_parent_sampled(默认 ALWAYS_ON)
└── 否 → local_parent_not_sampled(默认 ALWAYS_OFF)8.3 自定义采样器
python
from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult, Decision
from opentelemetry.trace import SpanKind
from opentelemetry.util.types import Attributes
class PriorityBasedSampler(Sampler):
"""根据请求优先级动态调整采样率"""
def __init__(self, default_rate: float = 0.1):
self._default_rate = default_rate
self._high_priority_sampler = ALWAYS_ON
self._default_sampler = TraceIdRatioBased(default_rate)
def should_sample(
self,
parent_context,
trace_id,
name,
kind=None,
attributes=None,
links=None,
) -> SamplingResult:
# 错误相关的 Span 始终采样
if attributes and attributes.get("error"):
return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes)
# 高优先级请求始终采样
if attributes and attributes.get("priority") == "high":
return self._high_priority_sampler.should_sample(
parent_context, trace_id, name, kind, attributes, links
)
# 默认采样率
return self._default_sampler.should_sample(
parent_context, trace_id, name, kind, attributes, links
)
def get_description(self):
return f"PriorityBasedSampler(default_rate={self._default_rate})"8.4 通过环境变量配置采样
bash
# 使用环境变量配置(零代码修改)
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"
# 可选值:
# always_on → ALWAYS_ON
# always_off → ALWAYS_OFF
# traceidratio → TraceIdRatioBased
# parentbased_always_on → ParentBased(root=ALWAYS_ON)
# parentbased_always_off → ParentBased(root=ALWAYS_OFF)
# parentbased_traceidratio → ParentBased(root=TraceIdRatioBased)8.5 采样决策类型
| Decision | 含义 | 效果 |
|---|---|---|
DROP | 丢弃 | Span 不记录、不导出 |
RECORD_ONLY | 仅记录 | Span 被创建和记录,但 trace_flags 不设采样位,下游不继承 |
RECORD_AND_SAMPLE | 记录并采样 | Span 被创建、记录、导出,下游继承采样决策 |
9. 实战场景分析
9.1 trpc-agent 项目中的 Trace 实践
结合 trpc-agent 项目,分析真实项目中 Trace 的使用模式:
[Runner Span] ← trace_runner()
├── [Agent Span] ← trace_agent()
│ ├── [LLM Call Span] ← trace_call_llm()
│ ├── [Tool Call Span] ← trace_tool_call()
│ │ └── [Tool Execution]
│ ├── [LLM Call Span] ← 第二轮对话
│ └── ...
└── State: begin → end设计要点:
- 使用全局
tracer = trace.get_tracer("trpc.python.agent")获取 Tracer - 在 Span 上设置
gen_ai.system、gen_ai.operation.name等语义属性 - 用
_safe_json_serialize安全序列化复杂对象为属性值 - 记录输入输出、状态变化等关键信息
- 取消操作时设置
ERROR状态和取消原因
9.2 Web 应用全链路追踪
python
# 一个典型的 FastAPI 全链路追踪示例
from fastapi import FastAPI, Request
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
import requests
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
RequestsInstrumentor().instrument()
@app.post("/api/orders")
async def create_order(order: OrderSchema):
with tracer.start_as_current_span("validate-order") as span:
validate(order)
with tracer.start_as_current_span("save-to-db", kind=SpanKind.CLIENT) as span:
span.set_attribute("db.system", "postgresql")
db.save(order)
with tracer.start_as_current_span("call-payment", kind=SpanKind.CLIENT) as span:
# requests 库自动 instrumentation 会将 traceparent 注入到 HTTP 头
resp = requests.post("http://payment-svc/charge", json={"amount": order.total})
span.set_attribute("http.status_code", resp.status_code)
return {"status": "created"}生成的 Trace 视图(在 Jaeger 中):
[FastAPI: POST /api/orders] 200ms (auto-instrumented, SERVER)
├── [validate-order] 15ms (manual, INTERNAL)
├── [save-to-db] 40ms (manual, CLIENT)
└── [call-payment] 120ms (manual, CLIENT)
└── [requests: POST payment-svc] 115ms (auto-instrumented, CLIENT)
└── [FastAPI: POST /charge] 100ms (payment-svc, SERVER)10. 实践 Demo
Demo 1:基础 Trace 入门
文件路径:
examples/trace_basic.py
展示 TracerProvider 配置、创建 Span、嵌套 Span、设置属性、记录异常。
Demo 2:模拟电商下单链路
文件路径:
examples/trace_ecommerce.py
模拟一个完整的电商下单流程:验证订单 → 扣库存 → 支付 → 发通知,展示 SpanKind、Events、Links 的使用。
Demo 3:采样策略对比
文件路径:
examples/trace_sampling.py
演示不同采样策略的效果,对比 ALWAYS_ON、TraceIdRatioBased、ParentBased 的行为。
Demo 4:自定义 SpanProcessor(过滤与增强)
文件路径:
examples/trace_custom_processor.py
实现过滤健康检查、添加通用属性的自定义 SpanProcessor。
Demo 5:FastAPI 手动 + 自动 Instrumentation
文件路径:
examples/trace_fastapi_demo.py
展示如何在 FastAPI 应用中结合自动 instrumentation 和手动 Span 创建。
11. 常见问题 QA
Q1:start_as_current_span 和 start_span 有什么区别?
start_as_current_span(name)
- 创建 Span 并将其设为当前 Context 中的 active Span
- 后续在同一上下文中创建的 Span 会自动成为其子 Span
- 退出
with块时自动结束 Span - 绝大多数场景使用这个
start_span(name)
- 只创建 Span,不设为 current
- 需要手动调用
span.end() - 需要手动
context.attach()才能建立父子关系 - 适用场景: 需要手动控制 Span 生命周期,或在异步环境中跨 Task 传递 Span
python
# start_as_current_span:自动管理一切
with tracer.start_as_current_span("parent"):
with tracer.start_as_current_span("child"): # 自动成为 parent 的子 Span
pass
# start_span:需要手动管理
span = tracer.start_span("manual")
# 此时没有父子关系,除非手动 attach
try:
do_work()
finally:
span.end()Q2:Span 什么时候结束?忘记结束会怎样?
自动结束: 使用 start_as_current_span 的 with 语句,退出时自动调用 span.end()。
手动结束: 使用 start_span 时必须调用 span.end()。
忘记结束的后果:
- Span 不会被 SpanProcessor 处理(
on_end不会被调用) - Span 永远不会被导出
- 可能导致内存泄漏
- 在 Jaeger 等后端看不到这个 Span
Q3:如何在异步代码(asyncio)中正确使用 Trace?
python
import asyncio
from opentelemetry import trace, context
tracer = trace.get_tracer(__name__)
async def main():
# ✅ 正确:with 语句在 async 中也能正常工作
with tracer.start_as_current_span("async-parent"):
# 并发任务会自动继承当前 Context
results = await asyncio.gather(
fetch_user(),
fetch_orders(),
)
async def fetch_user():
with tracer.start_as_current_span("fetch-user"):
await asyncio.sleep(0.1)
async def fetch_orders():
with tracer.start_as_current_span("fetch-orders"):
await asyncio.sleep(0.2)注意: Python 的 contextvars 在 asyncio 中天然支持 Context 传播。每个 Task 会复制父 Task 的 Context,所以 asyncio.gather 中的子任务能正确找到父 Span。
但如果使用 ThreadPoolExecutor,则需要手动传播 Context:
python
from concurrent.futures import ThreadPoolExecutor
def sync_work(ctx):
# 在线程中恢复 Context
token = context.attach(ctx)
try:
with tracer.start_as_current_span("thread-work"):
do_heavy_computation()
finally:
context.detach(token)
with tracer.start_as_current_span("parent"):
current_ctx = context.get_current()
with ThreadPoolExecutor() as pool:
pool.submit(sync_work, current_ctx)Q4:TracerProvider 可以创建多个吗?
可以,但通常只全局设置一个。多个 TracerProvider 的使用场景:
- 测试:每个测试用例独立的 Provider
- 多租户:不同租户的数据发送到不同后端
python
# 全局 Provider
trace.set_tracer_provider(main_provider)
# 另一个 Provider(不设为全局,直接传给 Tracer)
special_tracer = trace.get_tracer("special", tracer_provider=special_provider)Q5:BatchSpanProcessor 的队列满了会怎样?
当队列达到 max_queue_size 时,新的 Span 会被 丢弃(Drop)。SDK 不会阻塞应用线程。
应对策略:
- 增大
max_queue_size(消耗更多内存) - 减小
schedule_delay_millis(更频繁地导出) - 增大
max_export_batch_size(每次导出更多) - 检查 Exporter 目标(Collector)是否有性能瓶颈
- 使用采样减少 Span 产生量
Q6:如何在 Span 中安全地记录大对象?
python
import json
def safe_serialize(obj, max_length=4096):
"""安全序列化,防止属性值过大"""
try:
result = json.dumps(obj, ensure_ascii=False, default=str)
if len(result) > max_length:
return result[:max_length] + "...(truncated)"
return result
except (TypeError, ValueError):
return "<not serializable>"
with tracer.start_as_current_span("process") as span:
span.set_attribute("request.body", safe_serialize(request_body))
span.set_attribute("response.body.size", len(json.dumps(response_body)))Q7:Span 的 set_status(ERROR) 和 record_exception 有什么区别?
| 方法 | 作用 | 效果 |
|---|---|---|
set_status(ERROR, msg) | 设置 Span 整体状态为错误 | 后端 UI 中该 Span 显示为红色/错误 |
record_exception(e) | 记录一个异常事件 | 在 Span 的 Events 列表中添加异常详情 |
最佳实践: 两者配合使用:
python
try:
result = risky_operation()
except Exception as e:
span.record_exception(e) # 记录异常详情
span.set_status(trace.StatusCode.ERROR, str(e)) # 标记 Span 为错误
raiseQ8:为什么我在 Jaeger 中看不到某些 Span?
常见原因排查清单:
- 采样被拒绝:检查 Sampler 配置,开发时用
ALWAYS_ON - Span 未结束:使用
start_span但忘记调用end() - Provider 未设置:没有调用
trace.set_tracer_provider(provider) - Exporter 配置错误:endpoint 地址、端口不对
- 未添加 SpanProcessor:Provider 没有
add_span_processor - 优雅关闭缺失:进程退出前 BatchProcessor 中的 Span 未 flush
- 网络问题:应用与 Collector/Jaeger 之间的网络不通
- 时间窗口:Jaeger UI 查询的时间范围不对
快速排查方法:
python
# 先用 ConsoleSpanExporter 验证 Span 是否被创建
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))Q9:如何跨线程/进程传递 Trace 上下文?
python
# 跨线程:手动传递 Context
import threading
def worker(ctx):
token = context.attach(ctx)
try:
with tracer.start_as_current_span("worker-task"):
pass
finally:
context.detach(token)
with tracer.start_as_current_span("main"):
ctx = context.get_current()
t = threading.Thread(target=worker, args=(ctx,))
t.start()
t.join()
# 跨进程:通过 Propagator 注入/提取(详见阶段 5)
headers = {}
inject(headers) # 序列化到 dict
# 通过网络传递 headers...
ctx = extract(headers) # 反序列化恢复Q10:Resource 中的 service.name 不设置会怎样?
如果不设置 service.name:
- SDK 会使用默认值
"unknown_service"或"unknown_service:python" - 在 Jaeger 等后端中,所有来自未命名服务的 Span 会混在一起
- 强烈建议始终设置
service.name,这是最重要的 Resource 属性
Q11:如何在不修改代码的情况下禁用 Tracing?
python
# 方式 1:不安装 SDK(API 自动变为 No-Op)
# 只安装 opentelemetry-api,不安装 opentelemetry-sdk
# 方式 2:使用 ALWAYS_OFF 采样器
provider = TracerProvider(sampler=ALWAYS_OFF)
# 方式 3:环境变量
# export OTEL_TRACES_SAMPLER=always_off
# 方式 4:不设置 TracerProvider(默认是 NoOpTracerProvider)
# 不调用 trace.set_tracer_provider() 即可Q12:Span 属性限制有哪些?
OpenTelemetry SDK 的默认限制:
| 限制项 | 默认值 | 环境变量 |
|---|---|---|
| 最大属性数 | 128 | OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT |
| 属性值最大长度 | 无限制 | OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT |
| 最大事件数 | 128 | OTEL_SPAN_EVENT_COUNT_LIMIT |
| 事件最大属性数 | 128 | OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT |
| 最大 Link 数 | 128 | OTEL_SPAN_LINK_COUNT_LIMIT |
超出限制时,最早添加的数据会被丢弃。
12. 学习检查清单
- [ ] 能解释 Trace、Span、SpanContext 的概念和关系
- [ ] 能说出 5 种 SpanKind 及其适用场景
- [ ] 能创建 TracerProvider 并配置 Resource
- [ ] 能使用
start_as_current_span创建嵌套 Span - [ ] 能为 Span 添加 Attributes、Events、Status
- [ ] 能正确处理 Span 中的异常(
record_exception+set_status) - [ ] 理解 BatchSpanProcessor 和 SimpleSpanProcessor 的区别和参数
- [ ] 能配置不同的采样策略(ALWAYS_ON、TraceIdRatioBased、ParentBased)
- [ ] 知道 Span 命名和属性设置的最佳实践
- [ ] 能在 Jaeger 中查看和分析 Trace 数据
- [ ] 理解 Trace 数据流:Tracer → SpanProcessor → Exporter → Backend
- [ ] 能编写自定义 SpanProcessor
- [ ] 了解异步环境下 Context 传播的注意事项
下一阶段:3. Metrics 深入
前一阶段:1. 基础概念与架构