主题
阶段 1:OpenTelemetry 基础概念与架构
一、可观测性(Observability)基础
1.1 什么是可观测性?
可观测性是指通过系统的外部输出来推断系统内部状态的能力。它回答的核心问题是:
"我的系统现在怎么了?为什么会这样?"
可观测性 vs 传统监控
| 维度 | 传统监控(Monitoring) | 可观测性(Observability) |
|---|---|---|
| 目标 | 发现已知问题 | 理解未知问题 |
| 方式 | 预定义指标 + 告警阈值 | 丰富的遥测数据 + 灵活查询 |
| 问题类型 | "CPU 超过 90% 了" | "为什么这个用户的请求延迟突然从 50ms 飙升到 5s?" |
| 数据维度 | 低维度、聚合数据 | 高基数、高维度、细粒度数据 |
| 适用场景 | 单体应用 | 分布式微服务架构 |
| 类比 | 汽车仪表盘(只看油量、速度) | 汽车 OBD 诊断系统(可查任何内部状态) |
为什么微服务时代需要可观测性?
单体应用时代:
用户 → [应用服务器] → [数据库]
出了问题?看日志就行。
微服务时代:
用户 → [API Gateway] → [用户服务] → [订单服务] → [支付服务] → [通知服务]
↓ ↓ ↓
[用户DB] [订单DB] [Redis缓存]
↓
[消息队列] → [库存服务]
出了问题?请求经过了哪些服务?在哪一步出的问题?为什么?在分布式系统中,一个用户请求可能跨越 10+ 个服务、涉及多种中间件。没有可观测性,排查问题就像大海捞针。
1.2 可观测性的三大支柱
┌─────────────────────────────────────┐
│ Observability │
│ 可观测性 │
└──────────┬──────────────────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Traces │ │ Metrics │ │ Logs │
│ 追踪 │ │ 指标 │ │ 日志 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
"请求经过了 "系统现在的 "具体发生了
哪些步骤?" 运行状况?" 什么事?"(1) Traces(分布式追踪)
解决的问题:一个请求在分布式系统中走了哪条路径?每一步花了多长时间?
核心概念:
Trace(一次完整的请求链路)
│
├── Span A: API Gateway (20ms)
│ ├── Span B: User Service - 鉴权 (5ms)
│ └── Span C: Order Service - 创建订单 (15ms)
│ ├── Span D: Database - INSERT (3ms)
│ └── Span E: Redis - SET cache (1ms)- Trace:一条完整的请求链路,由多个 Span 组成
- Span:一个操作单元(如一次 HTTP 请求、一次数据库查询)
- 每个 Span 包含:名称、开始/结束时间、属性(Attributes)、事件(Events)、状态(Status)
日常类比:快递追踪。一个包裹从发货到签收的完整路径:发货→到达分拣中心→装车→到达目的网点→派送→签收。每一步就是一个 Span。
(2) Metrics(指标)
解决的问题:系统的整体健康状况如何?趋势如何变化?
常见指标类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| Counter | 只增不减的计数器 | 请求总数、错误总数 |
| Gauge | 可上可下的瞬时值 | CPU 使用率、内存占用、活跃连接数 |
| Histogram | 数据分布统计 | 请求延迟的 P50/P90/P99 |
日常类比:体检报告。血压、心率、体温等指标告诉你身体的整体状况和趋势。
(3) Logs(日志)
解决的问题:在某个时间点具体发生了什么?
示例:
2025-03-15 10:23:45 [ERROR] OrderService - Failed to create order for user_id=12345:
InsufficientBalance(balance=50.00, required=100.00)
trace_id=abc123def456 span_id=789ghi日常类比:病历记录。详细记载了每次就诊的具体症状和诊断。
三者的协作关系
场景:用户投诉"下单失败"
1. Metrics 告警 → "订单服务的错误率从 0.1% 升至 5%"(发现问题)
2. Traces 追踪 → "失败的请求链路:Gateway → OrderService → PaymentService(ERROR)"(定位问题)
3. Logs 详情 → "PaymentService: Connection timeout to payment gateway at 10.0.1.5:443"(明确原因)1.3 为什么需要 OpenTelemetry?
遥测数据采集的历史困境
以前的状况:
应用A ──→ Jaeger SDK ──→ Jaeger (追踪)
应用A ──→ Prometheus SDK ──→ Prometheus (指标)
应用A ──→ Fluentd ──→ ELK Stack (日志)
应用B ──→ Zipkin SDK ──→ Zipkin (追踪)
应用B ──→ StatsD SDK ──→ Graphite (指标)
应用B ──→ 自定义方案 ──→ Splunk (日志)
问题:
- 每个后端系统都有自己的 SDK,代码高度耦合
- 换后端 = 改代码 = 高成本
- 不同 SDK 之间的数据格式不兼容
- 同一个应用要集成多个 SDKOpenTelemetry 的解决方案
现在的状况(OpenTelemetry):
应用A ─┐ ┌─→ Jaeger
应用B ─┤ ├─→ Prometheus
应用C ─┼─→ OpenTelemetry ─┼─→ Grafana
应用D ─┤ (统一标准) ├─→ Datadog
应用E ─┘ ├─→ Langfuse
└─→ 任何兼容 OTLP 的后端
优势:
- 一套 SDK,适配所有后端
- 切换后端不需要改代码
- Traces + Metrics + Logs 统一采集
- CNCF 孵化项目,厂商中立OpenTelemetry 是什么?
OpenTelemetry(简称 OTel)是一个开源的可观测性框架,提供:
- 统一的 API 和 SDK:用于生成、收集、处理遥测数据
- 标准的传输协议(OTLP):厂商中立的数据传输格式
- 自动检测库:零代码修改即可为常用框架添加遥测
- Collector:独立的数据收集、处理、导出服务
OpenTelemetry 不是什么:它不是后端存储或可视化系统。它只负责"产生和传输数据",不负责"存储和展示数据"。
二、OpenTelemetry 架构详解
2.1 整体架构
┌─────────────────────── 你的应用 ─────────────────────────┐
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ OTel API │───→│ OTel SDK │───→│ Exporter │─┼──→ 直接发送到后端
│ │ (接口定义) │ │ (具体实现) │ │ (数据导出) │ │ 或发送到 Collector
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ 定义操作接口 采集 + 处理数据 │
│ (与厂商无关) (采样、批量等) │
│ │
└───────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ OTel Collector │ ← 可选,但推荐
│ (独立部署的服务) │
│ │
│ Receivers │ 接收数据
│ ↓ │
│ Processors │ 处理数据(过滤/转换/采样)
│ ↓ │
│ Exporters │ 导出数据
└────────┬────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌────────┐
│ Jaeger │ │Prometheus│ │Grafana │
│(Traces)│ │(Metrics) │ │ Tempo │
└────────┘ └──────────┘ └────────┘2.2 核心组件详解
(1) API —— 接口层(与厂商无关)
API 是 OpenTelemetry 的接口定义层,只定义"做什么",不管"怎么做"。
python
# opentelemetry-api 包提供的接口
from opentelemetry import trace
# 获取一个 Tracer(此时还没有配置 SDK)
tracer = trace.get_tracer("my-app")
# 使用 Tracer 创建 Span
with tracer.start_as_current_span("my-operation"):
pass # 做一些事情设计意义:
- 库的作者可以只依赖
opentelemetry-api,不强制依赖任何具体实现 - 如果应用没有配置 SDK,API 调用会变成 No-op(空操作),零开销
- 实现了关注点分离:写业务代码时只用 API,配置遥测时才用 SDK
(2) SDK —— 实现层
SDK 是 API 的具体实现,负责真正的数据采集和处理。
python
# opentelemetry-sdk 包提供的实现
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.resources import Resource
# 1. 创建 Resource(标识你的服务)
resource = Resource.create({
"service.name": "order-service",
"service.version": "1.0.0",
})
# 2. 创建 TracerProvider(SDK 层的核心)
provider = TracerProvider(resource=resource)
# 3. 添加 SpanProcessor(决定如何处理 Span)
processor = BatchSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
# 4. 注册为全局 Provider
trace.set_tracer_provider(provider)SDK 中的关键组件:
| 组件 | 作用 | 类比 |
|---|---|---|
| Resource | 标识产生遥测数据的实体(服务名、版本等) | 寄件人地址 |
| Provider | 管理配置和创建 Tracer/Meter | 快递公司 |
| Processor | 在导出前处理数据(批量、过滤等) | 分拣中心 |
| Sampler | 决定是否采集某条 Trace | 抽检策略 |
(3) Exporter —— 导出层
Exporter 负责将遥测数据发送到后端系统。
python
# Console Exporter(输出到控制台,调试用)
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
# OTLP Exporter(发送到 OTel Collector 或兼容 OTLP 的后端)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(endpoint="localhost:4317")
# Jaeger Exporter
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831)常见 Exporter:
| Exporter | 协议 | 目标后端 |
|---|---|---|
ConsoleSpanExporter | stdout | 控制台(调试) |
OTLPSpanExporter (gRPC) | OTLP/gRPC | Collector / 任何 OTLP 后端 |
OTLPSpanExporter (HTTP) | OTLP/HTTP | Collector / Langfuse / 其他 |
JaegerExporter | Thrift/gRPC | Jaeger |
ZipkinExporter | HTTP/JSON | Zipkin |
(4) Collector —— 独立的数据中转服务
Collector 是一个独立部署的进程,负责接收、处理和转发遥测数据。
┌─────────────── OTel Collector ──────────────┐
│ │
│ ┌──────────┐ ┌───────────┐ ┌─────────┐│
│ │Receivers │──→│Processors │──→│Exporters ││
│ │ │ │ │ │ ││
│ │• OTLP │ │• batch │ │• OTLP ││
│ │• Jaeger │ │• filter │ │• Jaeger ││
│ │• Zipkin │ │• sampling │ │• Prom ││
│ │• Kafka │ │• transform│ │• Kafka ││
│ └──────────┘ └───────────┘ └─────────┘│
│ │
└──────────────────────────────────────────────┘为什么推荐使用 Collector?
- 解耦:应用只需发送到 Collector,不关心最终后端是谁
- 缓冲:Collector 可以缓冲和批量处理数据,减轻后端压力
- 处理:在 Collector 中做数据过滤、采样、转换,不影响应用性能
- 多路输出:一份数据可以同时发送到多个后端
(5) OTLP 协议
OTLP(OpenTelemetry Protocol)是 OpenTelemetry 定义的标准数据传输协议。
支持两种传输方式:
- OTLP/gRPC:基于 gRPC,高性能,适合服务间通信
端口:4317
- OTLP/HTTP:基于 HTTP + Protobuf,更易于穿越防火墙
端口:4318
路径:/v1/traces, /v1/metrics, /v1/logs三、Python SDK 安装与环境搭建
3.1 包结构概览
opentelemetry-python 的包结构:
opentelemetry-api ← 核心 API(接口定义)
opentelemetry-sdk ← SDK 实现(数据采集处理)
opentelemetry-exporter-otlp ← OTLP 导出器(推荐)
opentelemetry-exporter-otlp-proto-grpc ← OTLP/gRPC
opentelemetry-exporter-otlp-proto-http ← OTLP/HTTP
opentelemetry-semantic-conventions ← 语义约定常量
opentelemetry-instrumentation-* ← 各种自动检测库3.2 安装
bash
# 方式一:最小安装(手动检测)
pip install opentelemetry-api opentelemetry-sdk
# 方式二:带 OTLP 导出器
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
# 方式三:完整安装(推荐学习使用)
pip install opentelemetry-api \
opentelemetry-sdk \
opentelemetry-exporter-otlp \
opentelemetry-semantic-conventions
# 方式四:使用 distro 一键安装(含自动检测)
pip install opentelemetry-distro
opentelemetry-bootstrap -a install # 自动检测并安装相关 instrumentation 库3.3 验证安装
python
import opentelemetry
print(f"OpenTelemetry version: {opentelemetry.version.__version__}")
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
print("SDK imported successfully!")四、实践 Demo
Demo 1:Hello OpenTelemetry —— 第一个 Trace
python
"""
demo1_hello_trace.py
最简单的 OpenTelemetry Trace 示例
"""
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
from opentelemetry.sdk.resources import Resource
# ========== 第一步:配置 SDK ==========
resource = Resource.create({
"service.name": "hello-otel-service",
"service.version": "0.1.0",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
# ========== 第二步:获取 Tracer ==========
tracer = trace.get_tracer("hello.tracer", "0.1.0")
# ========== 第三步:创建 Span ==========
def main():
# 使用上下文管理器自动管理 Span 的开始和结束
with tracer.start_as_current_span("main-operation") as span:
# 给 Span 添加属性
span.set_attribute("user.id", "12345")
span.set_attribute("user.name", "张三")
print("Hello, OpenTelemetry!")
# 添加一个事件(Event)
span.add_event("processing_started", {"step": "init"})
# 嵌套 Span(自动建立父子关系)
with tracer.start_as_current_span("sub-operation") as child_span:
child_span.set_attribute("operation.type", "database_query")
print(" 执行子操作...")
child_span.add_event("query_executed", {"rows": 42})
span.add_event("processing_completed")
# 确保所有数据都被导出
provider.shutdown()
if __name__ == "__main__":
main()运行效果(ConsoleExporter 会输出 JSON 格式的 Span 数据):
Hello, OpenTelemetry!
执行子操作...
{
"name": "sub-operation",
"context": {
"trace_id": "0xabcdef1234567890...",
"span_id": "0x1234567890abcdef",
"trace_state": "[]"
},
"parent_id": "0xfedcba0987654321",
"kind": "SpanKind.INTERNAL",
"start_time": "2025-03-15T10:00:00.100Z",
"end_time": "2025-03-15T10:00:00.150Z",
"attributes": {
"operation.type": "database_query"
},
"events": [
{
"name": "query_executed",
"timestamp": "...",
"attributes": {"rows": 42}
}
],
"resource": {
"service.name": "hello-otel-service",
"service.version": "0.1.0"
}
}要点理解:
trace_id:同一个 Trace 下所有 Span 共享相同的 trace_idspan_id:每个 Span 有唯一的 span_idparent_id:子 Span 的 parent_id 等于父 Span 的 span_id
Demo 2:理解 API 与 SDK 的分离
python
"""
demo2_api_vs_sdk.py
展示 API 和 SDK 分离的设计
"""
from opentelemetry import trace
# ========== 场景1:只有 API,没有配置 SDK ==========
tracer = trace.get_tracer("no-sdk-tracer")
with tracer.start_as_current_span("operation-without-sdk") as span:
print(f"Span 是否在记录: {span.is_recording()}")
# 输出: False(因为没有配置 SDK,所有操作都是 No-op)
span.set_attribute("key", "value") # 不会报错,但也不会做任何事
print("---")
# ========== 场景2:配置了 SDK ==========
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("with-sdk-tracer")
with tracer.start_as_current_span("operation-with-sdk") as span:
print(f"Span 是否在记录: {span.is_recording()}")
# 输出: True(SDK 已配置,Span 会被真正记录和导出)
span.set_attribute("key", "value")
provider.shutdown()设计哲学:
- 库的开发者可以安心使用
opentelemetry-api,不引入 SDK 依赖 - 最终的应用负责配置 SDK
- 没有 SDK 时,API 调用是零开销的 No-op
Demo 3:Resource —— 标识你的服务
python
"""
demo3_resource.py
理解 Resource 的作用和配置
"""
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
# ========== Resource 定义了"谁"产生了这些遥测数据 ==========
resource = Resource.create({
SERVICE_NAME: "payment-service", # 服务名(必填)
SERVICE_VERSION: "2.1.0", # 服务版本
"deployment.environment": "staging", # 部署环境
"host.name": "payment-host-01", # 主机名
"cloud.provider": "tencent", # 云厂商
"cloud.region": "ap-guangzhou", # 区域
"team.name": "payment-team", # 自定义属性:团队名
})
# Resource 也可以通过合并来组合
base_resource = Resource.create({"service.name": "my-service"})
extra_resource = Resource.create({"deployment.environment": "production"})
merged_resource = base_resource.merge(extra_resource)
print("Resource attributes:")
for key, value in merged_resource.attributes.items():
print(f" {key}: {value}")
# ========== 将 Resource 关联到 TracerProvider ==========
provider = TracerProvider(resource=resource)
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process-payment") as span:
span.set_attribute("payment.amount", 99.99)
# 导出的 Span 会自动包含 Resource 信息
# 在后端系统中,可以通过 Resource 筛选和分组数据
provider.shutdown()Resource 的最佳实践:
service.name是最重要的属性,后端系统通常以此来区分不同服务- 使用
deployment.environment区分生产/测试/开发环境 service.version方便追踪版本更新引入的问题
Demo 4:用环境变量配置(零代码修改)
python
"""
demo4_env_config.py
展示如何通过环境变量配置 OpenTelemetry(无需修改代码)
运行前先设置环境变量:
export OTEL_SERVICE_NAME="my-cool-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=dev,service.version=1.0.0"
export OTEL_TRACES_EXPORTER="console"
export OTEL_METRICS_EXPORTER="none"
export OTEL_LOGS_EXPORTER="none"
然后运行:
python demo4_env_config.py
或者使用 opentelemetry-instrument 自动配置:
opentelemetry-instrument python demo4_env_config.py
"""
import os
# 模拟设置环境变量(实际中应在启动脚本或 K8s 配置中设置)
os.environ["OTEL_SERVICE_NAME"] = "env-configured-service"
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "deployment.environment=dev,service.version=1.0.0"
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.sdk.resources import Resource
# Resource.create() 会自动读取 OTEL_SERVICE_NAME 和 OTEL_RESOURCE_ATTRIBUTES 环境变量
resource = Resource.create()
print("Auto-detected resource attributes:")
for key, value in resource.attributes.items():
print(f" {key}: {value}")
provider = TracerProvider(resource=resource)
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("env-config-demo"):
print("\nThis span will have the auto-detected resource attributes!")
provider.shutdown()常用环境变量一览:
| 环境变量 | 作用 | 默认值 |
|---|---|---|
OTEL_SERVICE_NAME | 服务名 | unknown_service |
OTEL_RESOURCE_ATTRIBUTES | 额外的 Resource 属性(逗号分隔) | 无 |
OTEL_TRACES_EXPORTER | Traces 导出器 | otlp |
OTEL_METRICS_EXPORTER | Metrics 导出器 | otlp |
OTEL_LOGS_EXPORTER | Logs 导出器 | otlp |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP 导出端点 | http://localhost:4317 |
OTEL_EXPORTER_OTLP_PROTOCOL | OTLP 传输协议 | grpc |
OTEL_TRACES_SAMPLER | 采样器类型 | parentbased_always_on |
OTEL_TRACES_SAMPLER_ARG | 采样器参数 | 1.0 |
Demo 5:真实项目中的 OpenTelemetry 用法
以下是一个来自真实 Agent 项目的简化示例,展示了在 LLM Agent 框架中如何使用 OpenTelemetry:
python
"""
demo5_real_world_trace.py
模拟真实 LLM Agent 框架中的 OpenTelemetry 用法
参考自 trpc-agent 项目的 telemetry 模块
"""
import json
import time
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
BatchSpanProcessor,
)
from opentelemetry.sdk.resources import Resource
# ========== 配置 ==========
resource = Resource.create({
"service.name": "llm-agent-service",
"service.version": "1.0.0",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("my.agent.framework")
# ========== 模拟 Agent 框架的各个环节 ==========
def trace_llm_call(model: str, prompt: str, response: str, tokens_in: int, tokens_out: int):
"""追踪 LLM 调用"""
span = trace.get_current_span()
span.set_attribute("gen_ai.system", "my-agent")
span.set_attribute("gen_ai.operation.name", "call_llm")
span.set_attribute("gen_ai.request.model", model)
span.set_attribute("gen_ai.usage.input_tokens", tokens_in)
span.set_attribute("gen_ai.usage.output_tokens", tokens_out)
span.set_attribute("my.agent.llm_request", json.dumps({"prompt": prompt[:200]}))
span.set_attribute("my.agent.llm_response", json.dumps({"text": response[:200]}))
def trace_tool_call(tool_name: str, tool_args: dict, tool_result: dict):
"""追踪工具调用"""
span = trace.get_current_span()
span.set_attribute("gen_ai.system", "my-agent")
span.set_attribute("gen_ai.operation.name", "execute_tool")
span.set_attribute("gen_ai.tool.name", tool_name)
span.set_attribute("my.agent.tool_call_args", json.dumps(tool_args))
span.set_attribute("my.agent.tool_response", json.dumps(tool_result))
def simulate_agent_run(user_input: str):
"""模拟一次完整的 Agent 运行"""
# 顶层 Span:Runner(整个调用链路)
with tracer.start_as_current_span("invocation") as runner_span:
runner_span.set_attribute("gen_ai.system", "my-agent")
runner_span.set_attribute("gen_ai.operation.name", "run_runner")
runner_span.set_attribute("my.agent.runner.input", user_input)
runner_span.set_attribute("my.agent.runner.user_id", "user-001")
runner_span.set_attribute("my.agent.runner.session_id", "session-abc")
# 第二层 Span:Agent 推理
with tracer.start_as_current_span("agent-reasoning") as agent_span:
agent_span.set_attribute("gen_ai.operation.name", "run_agent")
agent_span.set_attribute("my.agent.agent.name", "travel-planner")
agent_span.set_attribute("my.agent.agent.input", user_input)
# 第三层 Span:LLM 调用
with tracer.start_as_current_span("call-llm") as llm_span:
time.sleep(0.1) # 模拟 LLM 调用延迟
trace_llm_call(
model="gpt-4",
prompt=f"用户问: {user_input}",
response="我需要先搜索一下航班信息...",
tokens_in=50,
tokens_out=30,
)
# 第三层 Span:工具调用
with tracer.start_as_current_span("tool-search-flights") as tool_span:
time.sleep(0.05) # 模拟工具调用
trace_tool_call(
tool_name="search_flights",
tool_args={"from": "北京", "to": "上海", "date": "2025-04-01"},
tool_result={"flights": [{"id": "CA1234", "price": 800}]},
)
# 第三层 Span:第二次 LLM 调用(处理工具结果)
with tracer.start_as_current_span("call-llm-2") as llm_span2:
time.sleep(0.08)
trace_llm_call(
model="gpt-4",
prompt="根据搜索结果,为用户推荐航班...",
response="为您找到北京到上海的航班 CA1234,价格 800 元。",
tokens_in=120,
tokens_out=50,
)
agent_span.set_attribute(
"my.agent.agent.output",
"为您找到北京到上海的航班 CA1234,价格 800 元。"
)
runner_span.set_attribute(
"my.agent.runner.output",
"为您找到北京到上海的航班 CA1234,价格 800 元。"
)
print("\n✅ Agent 运行完成!以上 JSON 输出就是导出的 Span 数据。")
print("在 Jaeger/Langfuse 等系统中,这些 Span 会被组织成如下的调用树:")
print("""
invocation (Runner) ~250ms
└── agent-reasoning (Agent) ~240ms
├── call-llm (LLM Call #1) ~100ms
├── tool-search-flights (Tool Call) ~50ms
└── call-llm-2 (LLM Call #2) ~80ms
""")
if __name__ == "__main__":
simulate_agent_run("帮我查一下明天北京到上海的航班")
provider.shutdown()五、各组件关系的深入理解
5.1 数据流转全景图
你的代码
│
│ tracer.start_as_current_span("operation")
▼
┌──────────────────────────────────────────────────────┐
│ Tracer │
│ ├── 从 TracerProvider 获取配置(Resource、Sampler 等) │
│ └── 创建 Span 对象 │
└──────────┬───────────────────────────────────────────┘
│ span.end()(Span 结束时触发)
▼
┌──────────────────────────────────────────────────────┐
│ SpanProcessor │
│ ├── on_start(span): Span 开始时调用 │
│ ├── on_end(span): Span 结束时调用 │
│ │ ├── SimpleSpanProcessor: 立即同步导出 │
│ │ └── BatchSpanProcessor: 放入队列,批量异步导出 │
│ └── shutdown(): 刷新缓冲区并关闭 │
└──────────┬───────────────────────────────────────────┘
│ exporter.export([span1, span2, ...])
▼
┌──────────────────────────────────────────────────────┐
│ SpanExporter │
│ ├── ConsoleSpanExporter → 输出到 stdout │
│ ├── OTLPSpanExporter → 通过 OTLP 协议发送 │
│ └── 自定义 Exporter → 发送到任何目标 │
└──────────┬───────────────────────────────────────────┘
│ OTLP/gRPC or OTLP/HTTP
▼
┌────────────┐
│ 后端系统 │
└────────────┘5.2 Provider → Tracer → Span 的关系
python
# 一个应用只需要一个 TracerProvider
provider = TracerProvider(resource=resource)
# 一个 TracerProvider 可以创建多个 Tracer(按模块/功能区分)
user_tracer = trace.get_tracer("myapp.users", "1.0.0")
order_tracer = trace.get_tracer("myapp.orders", "1.0.0")
payment_tracer = trace.get_tracer("myapp.payments", "1.0.0")
# 每个 Tracer 可以创建多个 Span
with user_tracer.start_as_current_span("authenticate"):
with order_tracer.start_as_current_span("create_order"):
with payment_tracer.start_as_current_span("process_payment"):
pass
# 虽然用了不同的 Tracer,但这些 Span 共享同一个 trace_id
# 因为它们属于同一个请求链路5.3 SimpleSpanProcessor vs BatchSpanProcessor
python
# ========== SimpleSpanProcessor ==========
# 每个 Span 结束时立即同步导出
# 优点:数据实时可见
# 缺点:阻塞业务线程,性能差
# 适用:开发和调试
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
processor = SimpleSpanProcessor(exporter)
# ========== BatchSpanProcessor ==========
# 将 Span 放入队列,定期批量导出
# 优点:不阻塞业务线程,高性能
# 缺点:数据有延迟(默认最多 5 秒)
# 适用:生产环境
from opentelemetry.sdk.trace.export import BatchSpanProcessor
processor = BatchSpanProcessor(
exporter,
max_queue_size=2048, # 队列最大容量(默认 2048)
schedule_delay_millis=5000, # 导出间隔(默认 5000ms)
max_export_batch_size=512, # 单次导出最大数量(默认 512)
export_timeout_millis=30000, # 导出超时(默认 30000ms)
)六、常见问题 QA
Q1:OpenTelemetry 和 OpenTracing / OpenCensus 是什么关系?
A:OpenTelemetry 是 OpenTracing 和 OpenCensus 两个项目合并的产物。
时间线:
2016 OpenTracing(CNCF,专注 Traces)
2018 OpenCensus(Google,Traces + Metrics)
2019 两者合并 → OpenTelemetry(CNCF,Traces + Metrics + Logs)
现状:
- OpenTracing 和 OpenCensus 已停止维护
- OpenTelemetry 提供了兼容桥(bridge),方便迁移
- 所有新项目应该直接使用 OpenTelemetryQ2:API 和 SDK 为什么要分离?我直接用 SDK 不行吗?
A:可以直接用 SDK,但分离设计有其深层原因:
库作者的困境:假设你开发了一个
requests-retry库,你想给它加上追踪功能。如果你依赖 SDK,那所有使用你库的人都必须安装 SDK。但他们可能根本不需要追踪。用 API 就没有这个问题——API 包很轻量,没有 SDK 时所有调用都是 No-op。避免冲突:一个应用可能使用多个库,每个库都依赖了不同版本的 SDK,就可能产生冲突。但 API 是稳定的接口层,很少冲突。
python
# 库作者这样写(只依赖 api)
# requirements.txt: opentelemetry-api>=1.0
from opentelemetry import trace
tracer = trace.get_tracer("my-awesome-library")
def my_library_function():
with tracer.start_as_current_span("library-operation"):
# 如果用户配置了 SDK → 会记录 Span
# 如果用户没配置 SDK → No-op,零开销
do_something()Q3:Collector 是必须部署的吗?
A:不是必须的,但强烈推荐在生产环境使用。
不使用 Collector(简单场景):
应用 ──→ OTLPSpanExporter ──→ Jaeger/Langfuse
使用 Collector(推荐):
应用 ──→ OTLPSpanExporter ──→ Collector ──→ Jaeger
├──→ Prometheus
└──→ Langfuse
什么时候可以不用 Collector?
- 开发/调试阶段
- 只有一个后端系统
- 不需要数据预处理
什么时候应该用 Collector?
- 生产环境
- 需要发送到多个后端
- 需要数据过滤、采样、转换
- 需要缓冲和重试Q4:opentelemetry-api 和 opentelemetry-sdk 的版本必须一致吗?
A:不需要完全一致,但建议保持相近的版本。
bash
# 推荐:使用同一个大版本
pip install opentelemetry-api==1.25.0 opentelemetry-sdk==1.25.0
# 可以但不推荐:不同的小版本
pip install opentelemetry-api==1.25.0 opentelemetry-sdk==1.23.0
# 不可以:不同的大版本
pip install opentelemetry-api==1.25.0 opentelemetry-sdk==0.46b0 # ❌Q5:trace.get_tracer() 的参数应该填什么?
A:
python
tracer = trace.get_tracer(
instrumenting_module_name="myapp.users", # 产生 Span 的模块/库名
instrumenting_library_version="1.0.0", # 模块版本(可选)
tracer_provider=None, # 使用特定 Provider(可选,默认用全局)
)
# 常见做法:
tracer = trace.get_tracer(__name__) # 使用模块的全限定名
tracer = trace.get_tracer("myapp.orders") # 使用自定义名称instrumenting_module_name 的作用是在后端系统中标识 Span 是由哪个模块/库产生的,方便排查和过滤。
Q6:Span 的 Attributes 和 Events 有什么区别?
A:
| 维度 | Attributes(属性) | Events(事件) |
|---|---|---|
| 是什么 | 键值对,描述 Span 的特征 | 带时间戳的日志点 |
| 个数 | 整个 Span 生命周期内可设置多个 | 可以有多个,每个都带时间戳 |
| 覆盖 | 同 key 后设置的会覆盖前面的 | 不会覆盖,每次调用都新增一条 |
| 用途 | 描述 "是什么"(请求方法、URL、状态码) | 描述 "发生了什么"(缓存命中、重试) |
python
with tracer.start_as_current_span("process-order") as span:
# Attributes:描述这个操作的特征
span.set_attribute("order.id", "ORD-12345")
span.set_attribute("order.amount", 299.99)
span.set_attribute("http.method", "POST")
# Events:记录操作过程中发生的事
span.add_event("inventory_checked", {"available": True})
span.add_event("payment_processed", {"method": "wechat"})
span.add_event("order_confirmed")Q7:为什么导出的 Span 中有些属性名很长(如 gen_ai.usage.input_tokens)?
A:这些是 语义约定(Semantic Conventions),是 OpenTelemetry 社区定义的标准化属性名。
python
# 语义约定的好处:不同系统、不同语言产生的数据,属性名一致
# 后端系统可以根据这些标准名称自动解析和展示
# GenAI 相关的语义约定:
span.set_attribute("gen_ai.system", "openai") # AI 系统名
span.set_attribute("gen_ai.operation.name", "chat") # 操作类型
span.set_attribute("gen_ai.request.model", "gpt-4") # 请求的模型
span.set_attribute("gen_ai.usage.input_tokens", 100) # 输入 Token 数
span.set_attribute("gen_ai.usage.output_tokens", 50) # 输出 Token 数
# HTTP 相关的语义约定:
span.set_attribute("http.request.method", "GET")
span.set_attribute("url.full", "https://api.example.com/users")
span.set_attribute("http.response.status_code", 200)
# 安装语义约定包查看所有定义:
# pip install opentelemetry-semantic-conventions
from opentelemetry.semconv.trace import SpanAttributes
# SpanAttributes.HTTP_METHOD → "http.request.method"Q8:在异步代码(asyncio)中使用 OpenTelemetry 有什么注意事项?
A:OpenTelemetry Python SDK 对 asyncio 有良好的支持,start_as_current_span 在异步上下文中也能正确传播 Context。
python
import asyncio
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
async def fetch_user(user_id: str):
with tracer.start_as_current_span("fetch-user") as span:
span.set_attribute("user.id", user_id)
await asyncio.sleep(0.1) # 模拟异步 IO
return {"id": user_id, "name": "张三"}
async def fetch_orders(user_id: str):
with tracer.start_as_current_span("fetch-orders") as span:
span.set_attribute("user.id", user_id)
await asyncio.sleep(0.05)
return [{"order_id": "001", "amount": 100}]
async def main():
with tracer.start_as_current_span("handle-request"):
# 并发执行,两个子 Span 共享同一个父 Span
user, orders = await asyncio.gather(
fetch_user("user-123"),
fetch_orders("user-123"),
)注意:如果你使用了
threading或线程池,需要手动传播 Context,因为 Context 默认是线程局部的。这部分将在阶段 5(Context 传播)中详细讲解。
Q9:OpenTelemetry 的性能开销有多大?
A:取决于配置:
| 场景 | 开销 | 说明 |
|---|---|---|
| 只有 API(无 SDK) | ~0 | No-op,几乎零开销 |
| SDK + BatchProcessor | 很低(<1%) | 异步批量导出,不阻塞业务 |
| SDK + SimpleProcessor | 中等(1-5%) | 同步导出,会阻塞业务线程 |
| SDK + 大量 Attributes | 较高 | 每个 Attribute 都需要内存和序列化 |
| SDK + 100% 采样 + 高 QPS | 需评估 | 高流量下建议使用采样降低开销 |
优化建议:
- 生产环境使用
BatchSpanProcessor - 根据流量调整采样率(如 10% 采样)
- 控制 Attribute 的数量和大小
- 不要在 Attribute 中存储大对象
Q10:如何在不修改代码的情况下给应用添加 OpenTelemetry?
A:使用自动检测(Auto-instrumentation):
bash
# 1. 安装
pip install opentelemetry-distro opentelemetry-exporter-otlp
# 2. 自动安装所有检测到的 instrumentation 库
opentelemetry-bootstrap -a install
# 3. 用 opentelemetry-instrument 启动你的应用
OTEL_SERVICE_NAME=my-app \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
opentelemetry-instrument python app.py这会自动为 Flask、FastAPI、requests、SQLAlchemy 等常用库添加追踪,完全不需要修改代码。详细内容将在阶段 7(自动检测)中展开。
七、小结与知识图谱
OpenTelemetry
│
┌─────────────────┼─────────────────┐
│ │ │
Signals 架构 工具
(信号类型) (组件) (生态)
│ │ │
┌─────┼─────┐ ┌────┼────┐ ┌────┼────┐
│ │ │ │ │ │ │ │ │
Traces Metrics Logs API SDK Export Coll. Auto Sem.
│ │ Inst. Conv.
┌─────┼─────┐ │
│ │ │ Receivers
Provider│ Sampler Processors
│ Processor Exporters
Resource本阶段核心收获:
- 理解可观测性与传统监控的本质区别
- 掌握 Traces、Metrics、Logs 三大支柱的定位和协作
- 理解 OpenTelemetry 的架构:API → SDK → Exporter → Collector → Backend
- 理解 API 和 SDK 分离的设计哲学
- 能够使用 Python SDK 创建基本的 Trace
- 理解 Resource、Provider、Processor、Exporter 的关系和配置
- 了解环境变量配置方式
下一阶段预告:阶段 2 将深入 Traces,学习 Span 的高级用法(Status、SpanKind、Link)、采样策略和 SpanProcessor 的定制。