主题
阶段 6:导出与后端集成(Exporters & Backends)
对应学习计划阶段 6,预计学习时间 2-3 天
目录
1. Exporter 概述
1.1 Exporter 在架构中的位置
应用代码 → API → SDK → SpanProcessor / MetricReader / LogProcessor → Exporter → 后端
↑
你在这里Exporter 是 OpenTelemetry 数据管道的最后一环,负责将采集到的遥测数据(Traces、Metrics、Logs)序列化并发送到后端系统。
1.2 Exporter 的设计哲学
OpenTelemetry 的核心设计原则之一是 厂商中立(Vendor-Neutral):
传统方式(耦合): OTel 方式(解耦):
┌──────────────┐ ┌──────────────┐
│ 应用代码 │ │ 应用代码 │
│ import jaeger│ │ import otel │ ← 统一 API
│ import dd │ └──────┬───────┘
│ import nr │ │
└──────────────┘ ┌──────┴───────┐
↓ │ OTel SDK │
多个厂商 SDK 共存 └──────┬───────┘
互相冲突 │
┌──────┴───────┐
│ Exporter │ ← 只换这一层
└──────┬───────┘
│
┌──────┴───────┐
│ Jaeger/DD/NR │
└──────────────┘1.3 Exporter 分类
| 类别 | 示例 | 用途 |
|---|---|---|
| Console Exporter | ConsoleSpanExporter | 开发调试,将数据输出到控制台 |
| OTLP Exporter | OTLPSpanExporter | 标准协议导出,发送到 Collector 或直接到后端 |
| 厂商 Exporter | JaegerExporter, ZipkinExporter | 直接对接特定后端(已逐步废弃) |
| Prometheus Exporter | PrometheusMetricReader | 暴露 /metrics 端点供 Prometheus 拉取 |
| 自定义 Exporter | 继承 SpanExporter | 对接内部系统或特殊需求 |
1.4 数据导出的两种模式
推送模式(Push): 拉取模式(Pull):
┌──────┐ push ┌──────┐ ┌──────┐ pull ┌────────────┐
│ App │ ───────→ │Backend│ │ App │ ←─────── │ Prometheus │
└──────┘ └──────┘ └──────┘ GET └────────────┘
│:8889│ /metrics
OTLP、Jaeger、Zipkin Prometheus Exporter
适用于 Traces、Logs 适用于 Metrics2. OTLP Exporter 详解
2.1 什么是 OTLP?
OTLP(OpenTelemetry Protocol) 是 OpenTelemetry 的原生传输协议,是官方推荐的首选导出方式。
OTLP 的优势:
┌─────────────────────────────────────────────┐
│ ✓ 原生支持三种信号(Traces + Metrics + Logs)│
│ ✓ 支持 gRPC 和 HTTP/protobuf 两种传输方式 │
│ ✓ 高效的 Protobuf 序列化 │
│ ✓ 所有主流后端都支持接收 OTLP 数据 │
│ ✓ 持续演进,由 OTel 社区维护 │
└─────────────────────────────────────────────┘2.2 gRPC vs HTTP 传输方式
| 特性 | gRPC (OTLP/gRPC) | HTTP (OTLP/HTTP) |
|---|---|---|
| 默认端口 | 4317 | 4318 |
| 序列化格式 | Protobuf | Protobuf(默认)/ JSON |
| 连接方式 | HTTP/2 长连接 | HTTP/1.1 短连接 |
| 性能 | 更高(多路复用、流式传输) | 稍低 |
| 防火墙友好 | 可能被阻断(HTTP/2) | 更友好 |
| 负载均衡 | 需要 L7 负载均衡(gRPC) | 标准 L4/L7 均可 |
| 浏览器支持 | 不支持 | 支持 |
| 调试便利 | 较难抓包 | 可用标准 HTTP 工具 |
选择建议:
- 内部网络、高吞吐场景 → gRPC
- 跨网络/防火墙、简单部署 → HTTP
- 不确定时 → gRPC(默认推荐)
2.3 Python 安装依赖
bash
# gRPC 方式
pip install opentelemetry-exporter-otlp-proto-grpc
# HTTP 方式
pip install opentelemetry-exporter-otlp-proto-http
# 一键安装(同时安装 gRPC 和 HTTP)
pip install opentelemetry-exporter-otlp2.4 Traces 导出配置
python
# ============ gRPC 方式 ============
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "order-service",
"service.version": "1.2.0",
"deployment.environment": "production",
})
# gRPC Exporter(发送到 Collector 的 4317 端口)
grpc_exporter = OTLPSpanExporter(
endpoint="localhost:4317", # 注意:gRPC 不需要 http:// 前缀
insecure=True, # 开发环境不使用 TLS
# headers=(("api-key", "xxx"),), # 可选:添加认证头
# timeout=10, # 可选:超时时间(秒)
# compression=Compression.Gzip, # 可选:启用压缩
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(grpc_exporter))
# ============ HTTP 方式 ============
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPSpanExporter
http_exporter = HTTPSpanExporter(
endpoint="http://localhost:4318/v1/traces", # HTTP 需要完整 URL 路径
# headers={"Authorization": "Bearer xxx"}, # 可选:认证头
# timeout=10,
# compression=Compression.Gzip,
)
provider.add_span_processor(BatchSpanProcessor(http_exporter))2.5 Metrics 导出配置
python
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
metric_exporter = OTLPMetricExporter(
endpoint="localhost:4317",
insecure=True,
)
metric_reader = PeriodicExportingMetricReader(
metric_exporter,
export_interval_millis=10000, # 每 10 秒导出一次
export_timeout_millis=5000, # 导出超时 5 秒
)
meter_provider = MeterProvider(
resource=resource,
metric_readers=[metric_reader],
)2.6 Logs 导出配置
python
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
log_exporter = OTLPLogExporter(
endpoint="localhost:4317",
insecure=True,
)
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(log_exporter)
)2.7 统一初始化模式
实际项目中推荐封装统一的初始化函数:
python
"""telemetry_setup.py — 统一初始化 OpenTelemetry"""
from opentelemetry import trace, metrics
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
import logging
import atexit
def setup_telemetry(
service_name: str,
otlp_endpoint: str = "localhost:4317",
insecure: bool = True,
) -> tuple[TracerProvider, MeterProvider, LoggerProvider]:
"""初始化三种信号的导出"""
resource = Resource.create({
"service.name": service_name,
"service.version": "1.0.0",
})
# --- Traces ---
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint=otlp_endpoint, insecure=insecure)
)
)
trace.set_tracer_provider(tracer_provider)
# --- Metrics ---
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=otlp_endpoint, insecure=insecure),
export_interval_millis=10000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
# --- Logs ---
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint=otlp_endpoint, insecure=insecure)
)
)
set_logger_provider(logger_provider)
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
# 优雅关闭
atexit.register(tracer_provider.shutdown)
atexit.register(meter_provider.shutdown)
atexit.register(logger_provider.shutdown)
return tracer_provider, meter_provider, logger_provider2.8 通过环境变量配置(零代码)
OTLP Exporter 支持通过环境变量配置,无需修改任何代码:
bash
# 通用设置
export OTEL_SERVICE_NAME="order-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.2.0"
# OTLP Exporter 端点
export OTEL_EXPORTER_OTLP_ENDPOINT="http://collector:4317"
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc" # 可选: grpc, http/protobuf, http/json
# 分别配置不同信号的端点(可选,覆盖通用端点)
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://trace-collector:4317"
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://metrics-collector:4317"
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://logs-collector:4317"
# 认证
export OTEL_EXPORTER_OTLP_HEADERS="api-key=abc123,x-tenant=mycompany"
# 超时(毫秒)
export OTEL_EXPORTER_OTLP_TIMEOUT=10000
# 压缩
export OTEL_EXPORTER_OTLP_COMPRESSION="gzip"
# TLS 证书
export OTEL_EXPORTER_OTLP_CERTIFICATE="/path/to/ca.pem"
export OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE="/path/to/client.pem"
export OTEL_EXPORTER_OTLP_CLIENT_KEY="/path/to/client-key.pem"环境变量优先级:信号专用变量 > 通用变量 > 代码中的默认值
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ← 最高优先级(只影响 Traces)
↓
OTEL_EXPORTER_OTLP_ENDPOINT ← 中等优先级(影响所有信号)
↓
代码中的 endpoint 参数 ← 最低优先级2.9 OTLP 数据格式解析
OTLP 使用 Protobuf 定义数据结构,理解这些结构有助于调试:
OTLP Trace 数据结构(简化):
ExportTraceServiceRequest
└── resource_spans[]
├── resource
│ └── attributes[] ← service.name, service.version 等
└── scope_spans[]
├── scope
│ ├── name ← Tracer 名称
│ └── version ← Tracer 版本
└── spans[]
├── trace_id ← 16 字节
├── span_id ← 8 字节
├── parent_span_id ← 8 字节
├── name ← Span 名称
├── kind ← CLIENT/SERVER/...
├── start_time_unix_nano
├── end_time_unix_nano
├── attributes[]
├── events[]
├── links[]
└── status3. 其他常用 Exporter
3.1 Console Exporter(开发调试)
python
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk._logs.export import ConsoleLogExporter
# Traces
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
# Metrics
reader = PeriodicExportingMetricReader(ConsoleMetricExporter(), export_interval_millis=5000)
# Logs
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(ConsoleLogExporter())
)Console Exporter 输出示例(Span):
json
{
"name": "HTTP GET /api/users",
"context": {
"trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2",
"span_id": "0x051581bf3cb55c13",
"trace_state": "[]"
},
"kind": "SpanKind.SERVER",
"parent_id": null,
"start_time": "2025-03-15T10:30:00.000000Z",
"end_time": "2025-03-15T10:30:00.150000Z",
"status": { "status_code": "OK" },
"attributes": {
"http.method": "GET",
"http.route": "/api/users",
"http.status_code": 200
},
"events": [],
"links": [],
"resource": {
"service.name": "user-service",
"service.version": "1.0.0"
}
}3.2 Jaeger Exporter(已废弃,仅了解)
python
# ⚠️ Jaeger Exporter 已被官方废弃!
# Jaeger 后端现在原生支持 OTLP,应直接使用 OTLP Exporter
# 以下仅作历史参考
# 旧方式(不推荐)
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
jaeger_exporter = JaegerExporter(
agent_host_name="localhost",
agent_port=6831,
)
# 新方式(推荐):直接用 OTLP 发送到 Jaeger
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
otlp_exporter = OTLPSpanExporter(
endpoint="localhost:4317", # Jaeger 的 OTLP gRPC 端口
insecure=True,
)3.3 Zipkin Exporter
python
from opentelemetry.exporter.zipkin.json import ZipkinExporter
zipkin_exporter = ZipkinExporter(
endpoint="http://localhost:9411/api/v2/spans",
)3.4 Prometheus Exporter(拉取模式)
python
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
# PrometheusMetricReader 同时充当 MetricReader 和 Exporter
prometheus_reader = PrometheusMetricReader()
meter_provider = MeterProvider(
resource=resource,
metric_readers=[prometheus_reader],
)
# 启动 Prometheus HTTP 服务器,暴露 /metrics 端点
start_http_server(port=8889, addr="0.0.0.0")
# 现在 Prometheus 可以通过 http://localhost:8889/metrics 拉取指标3.5 多 Exporter 并行导出
可以同时导出到多个后端:
python
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
SimpleSpanProcessor,
ConsoleSpanExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider(resource=resource)
# 同时导出到控制台和 OTLP
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="localhost:4317", insecure=True,
)))3.6 自定义 Exporter
python
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
from opentelemetry.sdk.trace import ReadableSpan
from typing import Sequence
import json
import requests
class WebhookSpanExporter(SpanExporter):
"""将 Span 数据发送到自定义 Webhook"""
def __init__(self, webhook_url: str):
self._webhook_url = webhook_url
self._shutdown = False
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
if self._shutdown:
return SpanExportResult.FAILURE
try:
payload = []
for span in spans:
payload.append({
"name": span.name,
"trace_id": format(span.context.trace_id, "032x"),
"span_id": format(span.context.span_id, "016x"),
"duration_ms": (span.end_time - span.start_time) / 1e6,
"status": span.status.status_code.name,
"attributes": dict(span.attributes or {}),
})
response = requests.post(
self._webhook_url,
json={"spans": payload},
timeout=5,
)
if response.status_code == 200:
return SpanExportResult.SUCCESS
return SpanExportResult.FAILURE
except Exception:
return SpanExportResult.FAILURE
def shutdown(self) -> None:
self._shutdown = True
def force_flush(self, timeout_millis: int = 30000) -> bool:
return True4. OpenTelemetry Collector 深入
4.1 为什么需要 Collector?
直接从应用导出到后端看起来更简单,但 Collector 提供了重要的解耦和处理能力:
方式 A:应用直连后端(简单但受限)
┌─────┐ OTLP ┌────────┐
│ App │ ─────────→ │ Jaeger │
└─────┘ └────────┘
问题:应用需要知道后端地址,换后端要改代码/配置
方式 B:通过 Collector(推荐)
┌─────┐ OTLP ┌───────────┐ export ┌────────┐
│ App │ ─────────→ │ Collector │ ──────────→ │ Jaeger │
└─────┘ │ │ ──────────→ │ Prom │
│ 处理/过滤 │ ──────────→ │ Loki │
└───────────┘ └────────┘
优势:应用只管发给 Collector,后端变更对应用透明Collector 的核心价值:
| 价值 | 说明 |
|---|---|
| 解耦 | 应用不需要知道后端是什么、在哪里 |
| 数据处理 | 可以做批量处理、过滤、采样、脱敏、enrichment |
| 协议转换 | 接收 OTLP,可以转换输出为 Jaeger/Zipkin/Prometheus 等格式 |
| 可靠性 | 缓冲、重试、背压处理 |
| 多路输出 | 一份数据同时发送到多个后端 |
| 减少应用负载 | 复杂的数据处理在 Collector 中完成,不占用应用资源 |
4.2 Collector 架构
OpenTelemetry Collector 内部架构
┌──────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Receivers │ │ Processors │ │ Exporters │ │
│ │ │ │ │ │ │ │
│ │ ● OTLP │───→│ ● batch │───→│ ● OTLP │ │
│ │ ● Jaeger │ │ ● memory_ │ │ ● jaeger │ │
│ │ ● Zipkin │ │ limiter │ │ ● prometheus │ │
│ │ ● Prom │ │ ● filter │ │ ● logging │ │
│ │ ● ... │ │ ● attributes │ │ ● ... │ │
│ └──────────┘ │ ● tail_ │ └──────────────┘ │
│ │ sampling │ │
│ │ ● transform │ ┌──────────────┐ │
│ │ ● ... │ │ Extensions │ │
│ └──────────────┘ │ ● health │ │
│ │ ● pprof │ │
│ │ ● zpages │ │
│ └──────────────┘ │
│ │
│ Service: Pipeline 定义 (Receivers → Processors → Exporters) │
└──────────────────────────────────────────────────────────┘四大组件:
| 组件 | 职责 | 类比 |
|---|---|---|
| Receiver | 接收遥测数据(入口) | 水管的入水口 |
| Processor | 处理/转换数据(中间件) | 净水器 |
| Exporter | 发送数据到后端(出口) | 水管的出水口 |
| Extension | 辅助功能(健康检查、性能分析等) | 水表、阀门 |
4.3 Collector 配置详解
yaml
# otel-collector-config.yaml
# ==================== Receivers ====================
receivers:
# OTLP Receiver:接收 OTLP 格式数据
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # gRPC 端口
max_recv_msg_size_mib: 4 # 最大消息大小
http:
endpoint: 0.0.0.0:4318 # HTTP 端口
cors:
allowed_origins: ["*"] # CORS 配置(Web 前端需要)
# Prometheus Receiver:拉取 Prometheus 格式指标
prometheus:
config:
scrape_configs:
- job_name: 'my-app'
scrape_interval: 15s
static_configs:
- targets: ['app:8889']
# Host Metrics Receiver:采集主机指标(CPU、内存等)
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
memory: {}
disk: {}
network: {}
# ==================== Processors ====================
processors:
# 批量处理:减少网络请求次数
batch:
timeout: 5s # 超过此时间强制发送
send_batch_size: 1024 # 达到此数量立即发送
send_batch_max_size: 2048 # 单批次最大数量
# 内存限制:防止 Collector OOM
memory_limiter:
check_interval: 1s
limit_mib: 512 # 硬限制
spike_limit_mib: 128 # 瞬时峰值限制
# 当内存使用接近 limit 时,开始丢弃数据
# 属性处理:添加/修改/删除属性
attributes:
actions:
- key: environment
value: production
action: upsert # 添加或更新
- key: internal.secret
action: delete # 删除敏感属性
# 资源属性处理
resource:
attributes:
- key: cloud.region
value: ap-southeast-1
action: upsert
# 过滤处理:丢弃不需要的数据
filter:
error_mode: ignore
traces:
span:
- 'attributes["http.route"] == "/health"' # 过滤健康检查
- 'attributes["http.route"] == "/readiness"'
# 尾部采样:基于完整 Trace 做采样决策
tail_sampling:
decision_wait: 10s # 等待 Trace 完整的时间
policies:
- name: error-policy
type: status_code
status_code: { status_codes: [ERROR] } # 错误 Trace 全部保留
- name: slow-policy
type: latency
latency: { threshold_ms: 1000 } # 慢请求全部保留
- name: default-policy
type: probabilistic
probabilistic: { sampling_percentage: 10 } # 其余保留 10%
# ==================== Exporters ====================
exporters:
# OTLP Exporter:转发给另一个 Collector 或支持 OTLP 的后端
otlp:
endpoint: "tempo:4317"
tls:
insecure: true
otlp/jaeger:
endpoint: "jaeger:4317"
tls:
insecure: true
# Prometheus Exporter:暴露 /metrics 端点
prometheus:
endpoint: 0.0.0.0:8889
namespace: otel
send_timestamps: true
metric_expiration: 5m
resource_to_telemetry_conversion:
enabled: true # 将 Resource 属性转为 Metric 标签
# Loki Exporter:日志发送到 Loki
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
default_labels_enabled:
exporter: false
job: true
# Debug/Logging Exporter:调试用
debug:
verbosity: detailed
sampling_initial: 5 # 初始采样 5 条
sampling_thereafter: 200 # 之后每 200 条采样一条
# ==================== Extensions ====================
extensions:
# 健康检查端点
health_check:
endpoint: 0.0.0.0:13133
# 性能分析
pprof:
endpoint: 0.0.0.0:1777
# zPages:内置诊断页面
zpages:
endpoint: 0.0.0.0:55679
# ==================== Service ====================
service:
extensions: [health_check, pprof, zpages]
# Pipeline 定义:将组件串联起来
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp, hostmetrics]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, attributes, batch]
exporters: [loki]
# Collector 自身的遥测配置
telemetry:
logs:
level: info
metrics:
address: 0.0.0.0:8888 # Collector 自身的 metrics4.4 部署模式
Agent 模式
每台主机/Pod 部署一个 Collector(Sidecar 或 DaemonSet):
┌─────────────────────────┐ ┌─────────────────────────┐
│ Host / Pod 1 │ │ Host / Pod 2 │
│ ┌─────┐ ┌───────────┐ │ │ ┌─────┐ ┌───────────┐ │
│ │ App │→ │ Collector │ │ │ │ App │→ │ Collector │ │
│ └─────┘ │ (Agent) │ │ │ └─────┘ │ (Agent) │ │
│ └─────┬─────┘ │ │ └─────┬─────┘ │
└────────────────┼───────┘ └────────────────┼───────┘
│ │
└──────────┬──────────────────┘
↓
┌──────────┐
│ Backend │
└──────────┘
优势:低延迟、本地缓冲、减少网络跳转
适用:大规模部署、Kubernetes 环境Gateway 模式
集中式 Collector 集群:
┌─────┐ ┌─────┐ ┌─────┐
│App 1│ │App 2│ │App 3│
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
└────────┼────────┘
↓
┌────────────┐
│ Collector │ ← 负载均衡
│ Gateway │
│ (集群) │
└─────┬──────┘
↓
┌──────────┐
│ Backend │
└──────────┘
优势:统一管理、集中处理策略
适用:小规模部署、需要集中处理两级架构(推荐用于大规模)
┌──────┐ ┌─────────┐ ┌─────────────┐
│ App │ → │ Agent │ ──── OTLP ──→│ Gateway │ → Backend
│ │ │Collector│ │ Collector │
└──────┘ └─────────┘ └─────────────┘
(本地轻量处理) (集中重处理)
- 批量 - 尾部采样
- 内存限制 - 属性增强
- 路由分发4.5 Collector 发行版
| 发行版 | 包含组件 | 适用场景 |
|---|---|---|
| Core | 核心组件(OTLP) | 仅需 OTLP 收发 |
| Contrib | 核心 + 大量社区贡献组件 | 需要多种 Receiver/Exporter |
| 自定义构建 | 按需选择组件(ocb 工具) | 生产环境精确控制 |
bash
# Docker 使用 Contrib 版
docker pull otel/opentelemetry-collector-contrib:latest
# 使用 OCB (OpenTelemetry Collector Builder) 自定义构建
# builder-config.yaml 定义需要的组件5. 后端可视化系统
5.1 后端系统全景
┌───────────────────────────────────┐
│ Grafana (可视化统一入口) │
│ Dashboard / Alert / Explore │
└─────┬─────────┬──────────┬───────┘
│ │ │
┌──────────┴──┐ ┌───┴────┐ ┌───┴──────┐
│ Tempo/Jaeger│ │ Prom │ │ Loki │
│ (Traces) │ │(Metrics)│ │ (Logs) │
└──────┬──────┘ └───┬────┘ └───┬──────┘
│ │ │
└────────────┼───────────┘
│
┌────────┴────────┐
│ OTel Collector │
└────────┬────────┘
│
┌────┴────┐
│ App │
└─────────┘5.2 各后端系统详细对比
| 系统 | 信号类型 | 存储方式 | 查询方式 | 特点 | 适用场景 |
|---|---|---|---|---|---|
| Jaeger | Traces | Elasticsearch/Cassandra/Badger | Jaeger UI / API | CNCF 毕业项目,功能成熟,社区活跃 | 中小规模追踪 |
| Grafana Tempo | Traces | 对象存储(S3/GCS/MinIO) | TraceQL | 高性能、低成本、只需 Trace ID 索引 | 大规模追踪 |
| Zipkin | Traces | 内存/MySQL/Cassandra/ES | Zipkin UI | 轻量简单,适合入门 | 小规模/学习 |
| Prometheus | Metrics | 本地 TSDB | PromQL | 生态丰富、告警规则、拉取模式 | 指标监控 |
| Grafana Mimir | Metrics | 对象存储 | PromQL 兼容 | 水平扩展的 Prometheus | 大规模指标 |
| Grafana Loki | Logs | 对象存储 | LogQL | 只索引标签不索引内容、低成本 | 日志聚合 |
| SigNoz | All | ClickHouse | 内置 UI | 开源 APM,原生 OTel 支持,一站式 | 全栈可观测性 |
| Langfuse | Traces | PostgreSQL | 内置 UI | LLM 专用,追踪 Token 和 Prompt | AI/LLM 应用 |
5.3 Jaeger 详解
架构
┌──────────────────────────────────────────────────┐
│ Jaeger 架构 │
│ │
│ ┌────────┐ ┌───────────┐ ┌───────────────┐ │
│ │Collector│ → │ Store │ ← │ Query/UI │ │
│ │:4317 │ │(ES/Badger)│ │ :16686 │ │
│ └────────┘ └───────────┘ └───────────────┘ │
│ │
│ Jaeger All-in-One(开发):所有组件在一个进程 │
│ Jaeger 分布式(生产):各组件独立部署 │
└──────────────────────────────────────────────────┘Jaeger All-in-One 快速启动
bash
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest端口说明:
16686:Jaeger UI4317:OTLP gRPC Receiver4318:OTLP HTTP Receiver
5.4 Prometheus + Grafana
Prometheus 配置
yaml
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
# 从 OTel Collector 的 Prometheus Exporter 拉取
- job_name: 'otel-collector'
static_configs:
- targets: ['collector:8889']
# 直接从应用拉取(如果应用暴露了 /metrics)
- job_name: 'my-app'
static_configs:
- targets: ['app:8889']常用 PromQL 查询
promql
# 请求速率(QPS)
rate(http_server_request_count_total[5m])
# P99 延迟
histogram_quantile(0.99, rate(http_server_request_duration_bucket[5m]))
# 错误率
sum(rate(http_server_request_count_total{http_status_code=~"5.."}[5m]))
/
sum(rate(http_server_request_count_total[5m]))
# 按路由分组的延迟
histogram_quantile(0.95,
sum by(le, http_route) (
rate(http_server_request_duration_bucket[5m])
)
)5.5 Grafana Tempo(高性能 Trace 后端)
Tempo 的独特设计:
┌─────────────────────────────────────────┐
│ 传统 Trace 后端(Jaeger with ES) │
│ - 索引所有 Span 字段 │
│ - 存储成本高 │
│ - 搜索灵活但昂贵 │
├─────────────────────────────────────────┤
│ Tempo │
│ - 只索引 Trace ID(极低开销) │
│ - Trace 数据存对象存储(低成本) │
│ - 用 TraceQL 查询 │
│ - 通过 Metrics 找 Trace ID 再查详情 │
│ - 大规模场景下成本是 ES 的 1/10 │
└─────────────────────────────────────────┘TraceQL 查询示例:
# 查找错误的 HTTP 请求
{ span.http.status_code >= 500 }
# 查找慢的数据库查询
{ span.db.system = "postgresql" && duration > 500ms }
# 查找特定服务的 Root Span
{ resource.service.name = "order-service" && status = error }5.6 Grafana Loki(日志聚合)
Loki 的设计理念:
"Like Prometheus, but for logs"
传统日志系统(ELK): Loki:
- 全文索引 - 只索引标签(Labels)
- 存储大 - 存储小
- 查询灵活 - 通过标签定位 + grep
- 成本高 - 成本低LogQL 查询示例:
logql
# 查找特定服务的错误日志
{service_name="order-service"} |= "error"
# 查找包含特定 trace_id 的日志(Trace-Log 联动)
{service_name="order-service"} | json | trace_id="abc123def456"
# 按级别统计日志数量
sum by(level) (count_over_time({service_name="order-service"} | json [5m]))5.7 信号关联(Traces ↔ Metrics ↔ Logs)
这是可观测性的终极能力——三大信号相互关联:
告警触发(Metrics)
│
↓
"错误率 > 5%" ──→ 查看哪些 Trace 出错(Metrics → Traces)
│ │
│ ↓
│ 查看出错 Trace 的上下文日志
│ (Traces → Logs,通过 trace_id 关联)
│ │
↓ ↓
发现根因:DB 连接池耗尽在 Grafana 中配置数据源关联:
yaml
# Grafana 数据源配置示例
datasources:
- name: Tempo
type: tempo
url: http://tempo:3200
jsonData:
tracesToLogs:
datasourceUid: loki
tags: ['service.name']
tracesToMetrics:
datasourceUid: prometheus
tags: ['service.name']
- name: Prometheus
type: prometheus
url: http://prometheus:9090
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- name: trace_id
matcherRegex: '"trace_id":"(\w+)"'
url: '$${__value.raw}'
datasourceUid: tempo6. 完整集成架构
6.1 生产级架构图
┌──────────────────────────────────────────────────────────────────┐
│ 应用层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ │ Service D │ │
│ │(FastAPI) │ │(FastAPI) │ │(Django) │ │(gRPC) │ │
│ │ OTel SDK │ │ OTel SDK │ │ OTel SDK │ │ OTel SDK │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ └──────────────┼──────────────┼──────────────┘ │
│ │ OTLP (gRPC) │ │
└───────────────────────┼──────────────┼────────────────────────────┘
↓ ↓
┌──────────────────────────────────────────────────────────────────┐
│ Collector 层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ OTel Collector (Gateway) │ │
│ │ Receivers: [otlp] │ │
│ │ Processors: [memory_limiter, filter, batch] │ │
│ │ Exporters: │ │
│ │ traces → Tempo (OTLP) │ │
│ │ metrics → Prometheus (pull) │ │
│ │ logs → Loki (HTTP) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
↓ ↓ ↓
┌──────────────────────────────────────────────────────────────────┐
│ 存储层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Tempo │ │ Prometheus │ │ Loki │ │
│ │ (Traces) │ │ (Metrics) │ │ (Logs) │ │
│ │ MinIO/S3 │ │ 本地 TSDB │ │ MinIO/S3 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼─────────────────┼─────────────────┼───────────────────┘
│ │ │
└─────────────────┼─────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 可视化层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Grafana │ │
│ │ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Dashboard │ │ Explore │ │ Alert │ │ SLO │ │ │
│ │ │ 面板 │ │ 临时查询 │ │ 告警 │ │ 仪表板 │ │ │
│ │ └─────────┘ └──────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘7. 实践 Demo
7.1 Demo 1:ConsoleExporter 快速验证
最简单的验证方式——先确认数据能正常生成:
python
"""demo_console_exporter.py — 控制台输出验证"""
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
from opentelemetry.sdk.resources import Resource
import time
resource = Resource.create({"service.name": "demo-console"})
# Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)
tracer = trace.get_tracer("demo")
# Metrics
metric_reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(), export_interval_millis=5000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
meter = metrics.get_meter("demo")
request_counter = meter.create_counter("demo.requests", unit="1")
request_duration = meter.create_histogram("demo.duration", unit="ms")
# 模拟业务
for i in range(5):
with tracer.start_as_current_span(f"process-item-{i}") as span:
span.set_attribute("item.id", i)
request_counter.add(1, {"method": "GET"})
start = time.time()
time.sleep(0.1)
duration_ms = (time.time() - start) * 1000
request_duration.record(duration_ms, {"method": "GET"})
span.set_attribute("item.duration_ms", duration_ms)
print("等待 Metrics 导出...")
time.sleep(6)
meter_provider.shutdown()
trace_provider.shutdown()7.2 Demo 2:Docker Compose 全栈部署
项目结构
otel-demo/
├── docker-compose.yml # 编排所有服务
├── otel-collector-config.yaml # Collector 配置
├── prometheus.yml # Prometheus 配置
├── grafana/
│ └── provisioning/
│ └── datasources/
│ └── datasources.yaml # Grafana 数据源自动配置
└── app/
├── requirements.txt
└── main.py # 示例 FastAPI 应用docker-compose.yml
yaml
version: '3.8'
services:
# ==================== 应用 ====================
app:
build: ./app
ports:
- "8000:8000"
environment:
- OTEL_SERVICE_NAME=demo-app
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
- OTEL_EXPORTER_OTLP_INSECURE=true
depends_on:
- otel-collector
# ==================== OTel Collector ====================
otel-collector:
image: otel/opentelemetry-collector-contrib:0.96.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus exporter
- "13133:13133" # Health check
depends_on:
- jaeger
- loki
# ==================== Jaeger (Traces) ====================
jaeger:
image: jaegertracing/all-in-one:1.54
ports:
- "16686:16686" # Jaeger UI
- "14250:14250" # gRPC (Collector → Jaeger)
environment:
- COLLECTOR_OTLP_ENABLED=true
# ==================== Prometheus (Metrics) ====================
prometheus:
image: prom/prometheus:v2.50.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090" # Prometheus UI
depends_on:
- otel-collector
# ==================== Loki (Logs) ====================
loki:
image: grafana/loki:2.9.4
ports:
- "3100:3100" # Loki API
command: -config.file=/etc/loki/local-config.yaml
# ==================== Grafana (可视化) ====================
grafana:
image: grafana/grafana:10.3.1
ports:
- "3000:3000" # Grafana UI
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
depends_on:
- prometheus
- jaeger
- lokiotel-collector-config.yaml
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 512
memory_limiter:
check_interval: 1s
limit_mib: 256
spike_limit_mib: 64
# 过滤健康检查请求
filter:
error_mode: ignore
traces:
span:
- 'attributes["http.target"] == "/health"'
# 添加环境信息
resource:
attributes:
- key: deployment.environment
value: demo
action: upsert
exporters:
# Traces → Jaeger
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# Metrics → Prometheus(暴露端点供拉取)
prometheus:
endpoint: 0.0.0.0:8889
resource_to_telemetry_conversion:
enabled: true
# Logs → Loki
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
default_labels_enabled:
exporter: false
job: true
debug:
verbosity: basic
extensions:
health_check:
endpoint: 0.0.0.0:13133
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter, batch]
exporters: [otlp/jaeger, debug]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [loki]prometheus.yml
yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8889']
scrape_interval: 10sgrafana/provisioning/datasources/datasources.yaml
yaml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: jaeger
- name: Jaeger
type: jaeger
access: proxy
url: http://jaeger:16686
uid: jaeger
jsonData:
tracesToLogs:
datasourceUid: loki
tags: ['service.name']
mappedTags:
- key: service.name
value: service_name
mapTagNamesEnabled: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
uid: loki
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"trace_id":"(\w+)"'
url: '$${__value.raw}'
datasourceUid: jaegerapp/requirements.txt
fastapi==0.109.0
uvicorn==0.27.0
opentelemetry-api==1.22.0
opentelemetry-sdk==1.22.0
opentelemetry-exporter-otlp-proto-grpc==1.22.0
opentelemetry-instrumentation-fastapi==0.43b0
opentelemetry-instrumentation-logging==0.43b0app/main.py
python
"""完整的 FastAPI 应用 + OpenTelemetry 三信号集成示例"""
import logging
import random
import time
from fastapi import FastAPI, HTTPException
from opentelemetry import trace, metrics
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
import os
# ---------- 初始化 ----------
OTLP_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317")
SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME", "demo-app")
resource = Resource.create({
"service.name": SERVICE_NAME,
"service.version": "1.0.0",
})
# Traces
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=OTLP_ENDPOINT, insecure=True))
)
trace.set_tracer_provider(tracer_provider)
tracer = trace.get_tracer(__name__)
# Metrics
meter_provider = MeterProvider(
resource=resource,
metric_readers=[
PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=OTLP_ENDPOINT, insecure=True),
export_interval_millis=10000,
)
],
)
metrics.set_meter_provider(meter_provider)
meter = metrics.get_meter(__name__)
# 自定义业务指标
order_counter = meter.create_counter("app.orders.created", unit="1", description="Orders created")
order_amount = meter.create_histogram("app.orders.amount", unit="USD", description="Order amounts")
active_users = meter.create_up_down_counter("app.users.active", unit="1", description="Active users")
# Logs
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint=OTLP_ENDPOINT, insecure=True))
)
set_logger_provider(logger_provider)
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)
logger = logging.getLogger(__name__)
# ---------- FastAPI ----------
app = FastAPI(title="OTel Demo App")
FastAPIInstrumentor.instrument_app(app)
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
with tracer.start_as_current_span("fetch-user-from-db") as span:
span.set_attribute("user.id", user_id)
logger.info("Fetching user", extra={"user.id": user_id})
time.sleep(random.uniform(0.01, 0.05))
if user_id == 0:
span.set_status(trace.StatusCode.ERROR, "User not found")
logger.error("User not found", extra={"user.id": user_id})
raise HTTPException(status_code=404, detail="User not found")
active_users.add(1)
return {"id": user_id, "name": f"User-{user_id}"}
@app.post("/api/orders")
async def create_order():
with tracer.start_as_current_span("create-order") as span:
order_id = random.randint(10000, 99999)
amount = round(random.uniform(10, 500), 2)
span.set_attribute("order.id", order_id)
span.set_attribute("order.amount", amount)
logger.info("Creating order", extra={"order.id": order_id, "amount": amount})
with tracer.start_as_current_span("validate-inventory"):
time.sleep(random.uniform(0.01, 0.03))
with tracer.start_as_current_span("process-payment"):
time.sleep(random.uniform(0.02, 0.08))
if random.random() < 0.1:
span.set_status(trace.StatusCode.ERROR, "Payment failed")
logger.error("Payment failed", extra={"order.id": order_id})
raise HTTPException(status_code=500, detail="Payment failed")
order_counter.add(1, {"status": "success"})
order_amount.record(amount, {"currency": "USD"})
logger.info("Order created", extra={"order.id": order_id})
return {"order_id": order_id, "amount": amount, "status": "created"}
@app.on_event("shutdown")
async def shutdown():
tracer_provider.shutdown()
meter_provider.shutdown()
logger_provider.shutdown()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)app/Dockerfile
dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]7.3 Demo 3:直连 Jaeger(最简部署)
不需要 Collector,应用直接发送到 Jaeger:
python
"""demo_direct_jaeger.py — 应用直连 Jaeger"""
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
resource = Resource.create({"service.name": "direct-to-jaeger"})
provider = TracerProvider(resource=resource)
# Jaeger All-in-One 默认在 4317 端口接收 OTLP
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="localhost:4317", insecure=True))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# 生成 Traces
with tracer.start_as_current_span("main"):
with tracer.start_as_current_span("step-1"):
pass
with tracer.start_as_current_span("step-2"):
with tracer.start_as_current_span("step-2a"):
pass
provider.shutdown()
print("Traces 已发送,访问 http://localhost:16686 查看")7.4 Demo 4:Prometheus Metrics 拉取模式
python
"""demo_prometheus.py — Prometheus 拉取模式"""
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
import time
import random
resource = Resource.create({"service.name": "prometheus-demo"})
reader = PrometheusMetricReader()
provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)
meter = metrics.get_meter("prometheus-demo")
request_count = meter.create_counter("http_requests_total", unit="1")
request_latency = meter.create_histogram("http_request_duration_seconds", unit="s")
active_conns = meter.create_up_down_counter("active_connections", unit="1")
# 启动 HTTP 服务器暴露 /metrics
start_http_server(port=8889, addr="0.0.0.0")
print("Prometheus metrics 暴露在 http://localhost:8889/metrics")
# 模拟请求
while True:
method = random.choice(["GET", "POST", "PUT"])
status = random.choice([200, 200, 200, 200, 404, 500])
latency = random.uniform(0.01, 0.5)
request_count.add(1, {"method": method, "status": str(status)})
request_latency.record(latency, {"method": method})
if random.random() > 0.5:
active_conns.add(1)
else:
active_conns.add(-1)
time.sleep(0.5)7.5 运行 Demo 2(全栈部署)
bash
# 1. 创建项目目录
mkdir -p otel-demo/app otel-demo/grafana/provisioning/datasources
# 2. 创建各配置文件(参考上面的内容)
# 3. 启动所有服务
cd otel-demo
docker-compose up -d
# 4. 检查服务状态
docker-compose ps
# 5. 访问各 UI:
# - 应用: http://localhost:8000/docs (Swagger UI)
# - Jaeger: http://localhost:16686
# - Prometheus:http://localhost:9090
# - Grafana: http://localhost:3000 (admin/admin)
# 6. 生成测试流量
for i in $(seq 1 50); do
curl -s http://localhost:8000/api/users/$((RANDOM % 10)) > /dev/null
curl -s -X POST http://localhost:8000/api/orders > /dev/null
sleep 0.2
done
# 7. 在 Jaeger 中查看 Traces
# 8. 在 Prometheus 中查询指标
# 9. 在 Grafana 中创建 Dashboard
# 10. 清理
docker-compose down -v8. 常见问题 QA
Q1:OTLP gRPC 和 HTTP 如何选择?
A:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 内部网络/Kubernetes | gRPC | HTTP/2 多路复用,性能更好 |
| 跨公网/CDN | HTTP | 防火墙友好,CDN 可缓存 |
| 浏览器端 | HTTP | 浏览器不支持原生 gRPC |
| 代理/负载均衡后面 | 看 LB 能力 | 需要 L7 LB 支持 gRPC |
| 不确定 | gRPC | 社区默认推荐 |
python
# gRPC endpoint 不带协议前缀
grpc_exporter = OTLPSpanExporter(endpoint="collector:4317")
# HTTP endpoint 需要完整 URL(含路径)
http_exporter = OTLPSpanExporter(endpoint="http://collector:4318/v1/traces")Q2:Collector 是否是必须的?能否直接导出到后端?
A:Collector 不是必须的,但强烈推荐在生产环境使用。
直连模式(适合开发/小规模):
App ──OTLP──→ Jaeger
App ──OTLP──→ Tempo
简单,但换后端要改应用配置。
Collector 模式(推荐生产环境):
App ──OTLP──→ Collector ──→ Jaeger/Tempo/Prometheus/Loki
解耦,灵活,可做中间处理。什么时候可以不用 Collector?
- 开发/测试环境
- 只有一个后端
- 不需要数据预处理
- 应用数量很少
Q3:Exporter 发送失败会影响业务吗?
A:不会。这是 OpenTelemetry 的核心设计原则:
python
# BatchSpanProcessor 在后台线程中异步发送
# 如果 Exporter 发送失败:
# 1. 记录错误日志
# 2. 根据重试策略重试
# 3. 超过队列容量时丢弃最早的数据
# 4. 绝不影响业务逻辑的执行
# SimpleSpanProcessor 是同步的,如果 Exporter 失败会增加延迟
# 所以生产环境一定要用 BatchSpanProcessorOTLP Exporter 的内置重试策略:
- 可重试的状态码:
UNAVAILABLE(14)、RESOURCE_EXHAUSTED(8) - 重试间隔:指数退避 + 抖动
- 默认超时:10 秒
Q4:BatchSpanProcessor 参数如何调优?
A:
python
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, # 单批次最大 Span 数(默认 512)
export_timeout_millis=30000, # 单次导出超时(默认 30000ms)
)调优建议:
| 场景 | 调整 | 原因 |
|---|---|---|
| 高吞吐、延迟不敏感 | 增大 max_export_batch_size、schedule_delay_millis | 减少网络请求次数 |
| 低延迟要求 | 减小 schedule_delay_millis | 更快导出 |
| 内存紧张 | 减小 max_queue_size | 控制内存使用 |
| 网络不稳定 | 增大 export_timeout_millis | 容忍更高延迟 |
队列溢出时的行为:
┌─────────────────────────────┐
│ Queue (max_queue_size) │
│ [Span] [Span] [Span] ... │
│ ← 新 Span 入队 旧 Span 被导出 → │
│ 队列满时?丢弃新 Span(不阻塞) │
└─────────────────────────────┘Q5:如何验证 Exporter 是否正常工作?
A:
python
# 方法 1:同时添加 ConsoleExporter 观察输出
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
# 方法 2:检查 Collector 健康状态
# curl http://localhost:13133/ → {"status":"Server available"...}
# 方法 3:查看 Collector 自身的 metrics
# curl http://localhost:8888/metrics | grep otelcol_exporter
# 关键指标:
# otelcol_exporter_sent_spans ← 成功发送的 Span 数
# otelcol_exporter_send_failed_spans ← 发送失败的 Span 数
# otelcol_receiver_accepted_spans ← 接收到的 Span 数
# 方法 4:手动 force_flush
provider.force_flush() # 强制刷新所有缓冲区
# 方法 5:查看 Collector 日志
# docker logs otel-collector 2>&1 | grep -i errorQ6:Collector 的 Processor 顺序有讲究吗?
A:有,顺序非常重要!Processor 按配置顺序依次处理数据。
yaml
# 推荐的 Processor 顺序:
processors:
- memory_limiter # 1. 首先做内存保护(最重要)
- filter # 2. 尽早过滤不需要的数据
- attributes # 3. 添加/修改属性
- resource # 4. 资源属性处理
- tail_sampling # 5. 尾部采样(需要等待完整 Trace)
- batch # 6. 最后做批量处理(提高导出效率)
# ⚠️ 错误的顺序示例:
processors:
- batch # 先 batch 再 filter → 浪费批处理资源
- filter # 应该先过滤数据流经 Processor 链:
Span → memory_limiter → filter → attributes → batch → Exporter
(内存保护) (过滤) (增强) (批量) (导出)
如果内存不足, 丢弃 添加标签 积攒后 发送到
直接丢弃数据 健康检查 脱敏处理 批量发送 后端Q7:如何在 Collector 中实现敏感数据脱敏?
A:
yaml
processors:
# 方法 1:删除敏感属性
attributes/remove-sensitive:
actions:
- key: http.request.header.authorization
action: delete
- key: db.statement
action: delete
- key: user.password
action: delete
# 方法 2:用 transform processor 做正则替换
transform:
trace_statements:
- context: span
statements:
# 将信用卡号码脱敏
- replace_pattern(attributes["payment.card_number"], "\\d{12}(\\d{4})", "****$1")
# 将邮箱脱敏
- replace_pattern(attributes["user.email"], "(.).+(@.+)", "$1***$2")
# 方法 3:使用 redaction processor(需要 Contrib 版本)
redaction:
allow_all_keys: false
allowed_keys:
- http.method
- http.status_code
- http.route
blocked_values:
- '\b\d{3}-\d{2}-\d{4}\b' # SSN 格式
- '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b' # EmailQ8:Prometheus 拉取模式和 OTLP 推送模式有什么区别?
A:
推送模式(OTLP → Collector → Prometheus Remote Write):
┌─────┐ push ┌───────────┐ remote_write ┌────────────┐
│ App │ ─────────→│ Collector │ ───────────────→ │ Prometheus │
└─────┘ OTLP └───────────┘ └────────────┘
应用主动推送数据
拉取模式(Prometheus 主动拉取):
┌─────┐ ┌────────────┐
│ App │ ←────── GET /metrics ────── │ Prometheus │
│:8889│ │ │
└─────┘ └────────────┘
Prometheus 定时拉取 /metrics| 维度 | 推送模式 | 拉取模式 |
|---|---|---|
| 主动方 | 应用 | Prometheus |
| 服务发现 | 不需要 | 需要知道应用地址 |
| 防火墙 | 应用能出站即可 | Prometheus 需要能访问应用 |
| Kubernetes | 通过 Collector 统一推送 | Service Monitor 自动发现 |
| OTel 推荐 | OTLP 推送到 Collector | PrometheusMetricReader + 暴露端点 |
| 数据实时性 | 更实时 | 取决于 scrape_interval |
Q9:如何实现 Trace-Metrics-Logs 三者之间的关联?
A:关联的基础是 共享上下文信息:
三种信号的关联维度:
1. Trace ↔ Logs:通过 trace_id + span_id
- OTel LoggingHandler 自动在日志中注入 trace_id 和 span_id
- 在 Grafana 中可以从 Trace 跳转到对应日志
2. Trace ↔ Metrics:通过 Exemplar
- Exemplar 是附着在 Metric 数据点上的 trace_id
- 在 Prometheus 的 histogram/counter 中记录
3. Metrics → Trace 的跳转流程:
- 在 Grafana 中看到延迟飙升的 Metric
- 点击数据点上的 Exemplar(小菱形标记)
- 自动跳转到对应的 Trace 详情
4. 共同属性:
- Resource 属性(service.name 等)是三者共享的
- 可以通过 service.name 在不同信号间导航python
# Exemplar 示例(Python SDK 自动处理)
# 当在 Span 上下文中记录 Metric 时,SDK 会自动关联 trace_id
with tracer.start_as_current_span("handle-request"):
# 这个 record 调用会自动附带当前 span 的 trace_id 作为 Exemplar
request_duration.record(150.5, {"http.method": "GET"})Q10:Collector 的 Agent 模式和 Gateway 模式怎么选?
A:
| 维度 | Agent 模式 | Gateway 模式 | 两级架构 |
|---|---|---|---|
| 部署方式 | 每个节点一个 | 集中式集群 | Agent + Gateway |
| 资源消耗 | 分散在各节点 | 集中消耗 | 分散 + 集中 |
| 延迟 | 低(本地通信) | 较高(网络传输) | 低 |
| 可靠性 | 节点故障只影响本机 | 单点风险需集群 | 最高 |
| 管理复杂度 | 需管理多实例 | 管理少量实例 | 中等 |
| 适用规模 | 大规模 | 小规模 | 大规模 |
| Kubernetes | DaemonSet | Deployment | DaemonSet + Deployment |
推荐:
- 小团队/项目 → Gateway 模式即可
- 中等规模 → Agent 模式(K8s DaemonSet)
- 大规模/多集群 → 两级架构
Q11:如何监控 Collector 自身的健康状态?
A:
yaml
# Collector 自身的监控配置
service:
telemetry:
logs:
level: info # 日志级别
metrics:
address: 0.0.0.0:8888 # Collector 自身的 metrics 端口
extensions:
health_check:
endpoint: 0.0.0.0:13133 # 健康检查端点关键监控指标:
promql
# Collector 接收的 Span 数量
otelcol_receiver_accepted_spans
# Collector 成功导出的 Span 数量
otelcol_exporter_sent_spans
# 导出失败的 Span 数量(重要告警指标)
otelcol_exporter_send_failed_spans
# Collector 处理队列大小
otelcol_exporter_queue_size
# Collector 内存使用
process_runtime_total_alloc_bytes_totalQ12:如何处理 Exporter 的 TLS 配置?
A:
python
# Python SDK 中配置 TLS
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# 方式 1:使用系统 CA 证书(默认)
exporter = OTLPSpanExporter(
endpoint="collector.example.com:4317",
# insecure=False 是默认值,会使用 TLS
)
# 方式 2:自定义 CA 证书
with open("/path/to/ca.pem", "rb") as f:
ca_cert = f.read()
exporter = OTLPSpanExporter(
endpoint="collector.example.com:4317",
credentials=ssl_channel_credentials(root_certificates=ca_cert),
)
# 方式 3:mTLS(双向 TLS)
from grpc import ssl_channel_credentials
with open("/path/to/ca.pem", "rb") as f:
ca_cert = f.read()
with open("/path/to/client.pem", "rb") as f:
client_cert = f.read()
with open("/path/to/client-key.pem", "rb") as f:
client_key = f.read()
exporter = OTLPSpanExporter(
endpoint="collector.example.com:4317",
credentials=ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
),
)
# 方式 4:通过环境变量
# export OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca.pem
# export OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/path/to/client.pem
# export OTEL_EXPORTER_OTLP_CLIENT_KEY=/path/to/client-key.pemyaml
# Collector 中的 TLS 配置
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tls:
cert_file: /etc/certs/server.pem
key_file: /etc/certs/server-key.pem
client_ca_file: /etc/certs/ca.pem # mTLS
exporters:
otlp:
endpoint: backend:4317
tls:
ca_file: /etc/certs/ca.pem
cert_file: /etc/certs/client.pem
key_file: /etc/certs/client-key.pemQ13:OTLP 的 JSON 格式和 Protobuf 格式有什么区别?
A:
| 维度 | Protobuf | JSON |
|---|---|---|
| 大小 | 小(二进制) | 大(文本,约 3-5 倍) |
| 解析速度 | 快 | 慢 |
| 人类可读 | 否 | 是 |
| 调试便利 | 差 | 好 |
| 生产推荐 | 是 | 否(用于调试) |
| HTTP Content-Type | application/x-protobuf | application/json |
python
# HTTP + Protobuf(默认推荐)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
# HTTP + JSON(调试时使用)
# 通过环境变量切换
# export OTEL_EXPORTER_OTLP_PROTOCOL=http/jsonQ14:如何优雅关闭 Exporter 确保数据不丢失?
A:
python
import atexit
import signal
import sys
tracer_provider = TracerProvider(resource=resource)
meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
logger_provider = LoggerProvider(resource=resource)
def graceful_shutdown():
"""确保所有缓冲区中的数据被导出"""
tracer_provider.force_flush(timeout_millis=5000)
meter_provider.force_flush(timeout_millis=5000)
logger_provider.force_flush(timeout_millis=5000)
tracer_provider.shutdown()
meter_provider.shutdown()
logger_provider.shutdown()
# 方式 1:atexit 注册
atexit.register(graceful_shutdown)
# 方式 2:信号处理(SIGTERM/SIGINT)
def signal_handler(signum, frame):
graceful_shutdown()
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# 方式 3:FastAPI 的 lifespan
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
yield
graceful_shutdown()
app = FastAPI(lifespan=lifespan)关键点:
force_flush()强制导出缓冲区中的所有数据shutdown()关闭 Exporter 和后台线程- 先
force_flush再shutdown - 设置合理的
timeout_millis,避免关闭时间过长
Q15:Collector 的 tail_sampling(尾部采样)和 SDK 的 head sampling(头部采样)有什么区别?
A:
头部采样(Head Sampling)— 在 SDK 中决策:
┌──────────────────────────────────────────────────┐
│ Span 创建时立即决定是否采集 │
│ │
│ 优点: │
│ ✓ 简单,性能好 │
│ ✓ 不需要额外组件 │
│ │
│ 缺点: │
│ ✗ 无法基于结果采样(不知道请求会不会出错) │
│ ✗ 可能错过重要的错误 Trace │
│ │
│ 实现:TraceIdRatioBased, ParentBased │
└──────────────────────────────────────────────────┘
尾部采样(Tail Sampling)— 在 Collector 中决策:
┌──────────────────────────────────────────────────┐
│ 等待整个 Trace 的所有 Span 到达后再决定 │
│ │
│ 优点: │
│ ✓ 可基于完整 Trace 信息决策 │
│ ✓ 100% 保留错误 Trace │
│ ✓ 100% 保留慢请求 │
│ │
│ 缺点: │
│ ✗ 需要 Collector,增加架构复杂度 │
│ ✗ 需要内存缓存等待 Trace 完整 │
│ ✗ 分布式 Collector 需要确保同 Trace 路由到同节点 │
│ │
│ 实现:tail_sampling processor │
└──────────────────────────────────────────────────┘生产环境推荐策略:
yaml
# SDK 层面:全部采集(ALWAYS_ON),让 Collector 做决策
# Collector 层面:尾部采样
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 2000 }
- name: sample-rest
type: probabilistic
probabilistic: { sampling_percentage: 5 }9. 学习检查清单
- [ ] 能解释 Exporter 在 OTel 架构中的位置和作用
- [ ] 能区分推送模式和拉取模式的适用场景
- [ ] 能配置 OTLP gRPC 和 HTTP 两种 Exporter
- [ ] 能通过环境变量配置 OTLP Exporter(零代码修改)
- [ ] 能同时配置多个 Exporter(如 Console + OTLP)
- [ ] 能编写自定义 SpanExporter
- [ ] 理解 Collector 的四大组件:Receiver、Processor、Exporter、Extension
- [ ] 能编写 Collector 的 YAML 配置文件
- [ ] 能解释 Collector Pipeline 的概念和 Processor 顺序的重要性
- [ ] 能区分 Agent 模式和 Gateway 模式的适用场景
- [ ] 能使用 Docker Compose 搭建 Collector + Jaeger + Prometheus + Grafana
- [ ] 能在 Jaeger 中查看和分析 Trace 数据
- [ ] 能在 Prometheus 中查询 Metrics 数据
- [ ] 理解 Traces ↔ Metrics ↔ Logs 三者的关联机制
- [ ] 能配置 Collector 的敏感数据脱敏
- [ ] 理解头部采样和尾部采样的区别与适用场景
- [ ] 能配置 TLS 和 mTLS 保证传输安全
- [ ] 能实现优雅关闭确保数据不丢失
下一阶段:7. 自动检测(Auto Instrumentation)
前一阶段:5. Context 传播