Skip to content

阶段 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 之间的数据格式不兼容
  - 同一个应用要集成多个 SDK

OpenTelemetry 的解决方案

现在的状况(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协议目标后端
ConsoleSpanExporterstdout控制台(调试)
OTLPSpanExporter (gRPC)OTLP/gRPCCollector / 任何 OTLP 后端
OTLPSpanExporter (HTTP)OTLP/HTTPCollector / Langfuse / 其他
JaegerExporterThrift/gRPCJaeger
ZipkinExporterHTTP/JSONZipkin

(4) Collector —— 独立的数据中转服务

Collector 是一个独立部署的进程,负责接收、处理和转发遥测数据。

┌─────────────── OTel Collector ──────────────┐
│                                              │
│  ┌──────────┐   ┌───────────┐   ┌─────────┐│
│  │Receivers │──→│Processors │──→│Exporters ││
│  │          │   │           │   │          ││
│  │• OTLP    │   │• batch    │   │• OTLP   ││
│  │• Jaeger  │   │• filter   │   │• Jaeger ││
│  │• Zipkin  │   │• sampling │   │• Prom   ││
│  │• Kafka   │   │• transform│   │• Kafka  ││
│  └──────────┘   └───────────┘   └─────────┘│
│                                              │
└──────────────────────────────────────────────┘

为什么推荐使用 Collector?

  1. 解耦:应用只需发送到 Collector,不关心最终后端是谁
  2. 缓冲:Collector 可以缓冲和批量处理数据,减轻后端压力
  3. 处理:在 Collector 中做数据过滤、采样、转换,不影响应用性能
  4. 多路输出:一份数据可以同时发送到多个后端

(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_id
  • span_id:每个 Span 有唯一的 span_id
  • parent_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_EXPORTERTraces 导出器otlp
OTEL_METRICS_EXPORTERMetrics 导出器otlp
OTEL_LOGS_EXPORTERLogs 导出器otlp
OTEL_EXPORTER_OTLP_ENDPOINTOTLP 导出端点http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOLOTLP 传输协议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),方便迁移
  - 所有新项目应该直接使用 OpenTelemetry

Q2:API 和 SDK 为什么要分离?我直接用 SDK 不行吗?

A:可以直接用 SDK,但分离设计有其深层原因:

  1. 库作者的困境:假设你开发了一个 requests-retry 库,你想给它加上追踪功能。如果你依赖 SDK,那所有使用你库的人都必须安装 SDK。但他们可能根本不需要追踪。用 API 就没有这个问题——API 包很轻量,没有 SDK 时所有调用都是 No-op。

  2. 避免冲突:一个应用可能使用多个库,每个库都依赖了不同版本的 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-apiopentelemetry-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)~0No-op,几乎零开销
SDK + BatchProcessor很低(<1%)异步批量导出,不阻塞业务
SDK + SimpleProcessor中等(1-5%)同步导出,会阻塞业务线程
SDK + 大量 Attributes较高每个 Attribute 都需要内存和序列化
SDK + 100% 采样 + 高 QPS需评估高流量下建议使用采样降低开销

优化建议

  1. 生产环境使用 BatchSpanProcessor
  2. 根据流量调整采样率(如 10% 采样)
  3. 控制 Attribute 的数量和大小
  4. 不要在 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

本阶段核心收获

  1. 理解可观测性与传统监控的本质区别
  2. 掌握 Traces、Metrics、Logs 三大支柱的定位和协作
  3. 理解 OpenTelemetry 的架构:API → SDK → Exporter → Collector → Backend
  4. 理解 API 和 SDK 分离的设计哲学
  5. 能够使用 Python SDK 创建基本的 Trace
  6. 理解 Resource、Provider、Processor、Exporter 的关系和配置
  7. 了解环境变量配置方式

下一阶段预告:阶段 2 将深入 Traces,学习 Span 的高级用法(Status、SpanKind、Link)、采样策略和 SpanProcessor 的定制。