Skip to content

OpenTelemetry Traces 深入学习笔记

对应学习计划阶段 2,预计学习时间 3-4 天


目录

  1. 核心概念
  2. 架构与数据流
  3. TracerProvider 与 Tracer
  4. Span 详解
  5. SpanContext 与传播基础
  6. Span 属性与语义约定
  7. SpanProcessor 详解
  8. 采样策略
  9. 实战场景分析
  10. 实践 Demo
  11. 常见问题 QA
  12. 学习检查清单

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
kindSpan 类型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_id

PRODUCER / 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 自动建立

用于关联没有直接父子关系但逻辑相关的 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.nameSDK 名称(自动填充)"opentelemetry"
telemetry.sdk.languageSDK 语言(自动填充)"python"
host.name主机名"worker-01"
k8s.pod.nameK8s 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 3

4.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 的选择:

特征AttributesEvents
数据模型键值对名称 + 时间戳 + 属性
时间性描述整个 Span描述某个时间点
数量一个 key 一个值可以多个同名事件
适用场景http.status_code=200cache.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")
        raise

record_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 ordersSELECT * FROM orders WHERE id=1
RPC 调用grpc.UserService/GetUsercall GetUser for uid=abc
消息消费process orders.createdprocess 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=sampled
python
# 这就是 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:
        pass

6.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 属性最佳实践

  1. 避免高基数属性:不要用 user.id 这样有无限可能值的属性作为后端的索引维度
  2. 控制属性数量:SDK 默认限制每个 Span 128 个属性
  3. 属性值长度:避免超长字符串(如完整的 SQL 语句、请求体)
  4. 敏感数据:不要在属性中存储密码、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

设计要点:

  1. 使用全局 tracer = trace.get_tracer("trpc.python.agent") 获取 Tracer
  2. 在 Span 上设置 gen_ai.systemgen_ai.operation.name 等语义属性
  3. _safe_json_serialize 安全序列化复杂对象为属性值
  4. 记录输入输出、状态变化等关键信息
  5. 取消操作时设置 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_spanstart_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_spanwith 语句,退出时自动调用 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 不会阻塞应用线程。

应对策略:

  1. 增大 max_queue_size(消耗更多内存)
  2. 减小 schedule_delay_millis(更频繁地导出)
  3. 增大 max_export_batch_size(每次导出更多)
  4. 检查 Exporter 目标(Collector)是否有性能瓶颈
  5. 使用采样减少 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 为错误
    raise

Q8:为什么我在 Jaeger 中看不到某些 Span?

常见原因排查清单:

  1. 采样被拒绝:检查 Sampler 配置,开发时用 ALWAYS_ON
  2. Span 未结束:使用 start_span 但忘记调用 end()
  3. Provider 未设置:没有调用 trace.set_tracer_provider(provider)
  4. Exporter 配置错误:endpoint 地址、端口不对
  5. 未添加 SpanProcessor:Provider 没有 add_span_processor
  6. 优雅关闭缺失:进程退出前 BatchProcessor 中的 Span 未 flush
  7. 网络问题:应用与 Collector/Jaeger 之间的网络不通
  8. 时间窗口: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 的默认限制:

限制项默认值环境变量
最大属性数128OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
属性值最大长度无限制OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT
最大事件数128OTEL_SPAN_EVENT_COUNT_LIMIT
事件最大属性数128OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT
最大 Link 数128OTEL_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. 基础概念与架构