Skip to content

阶段 6:导出与后端集成(Exporters & Backends)

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


目录

  1. Exporter 概述
  2. OTLP Exporter 详解
  3. 其他常用 Exporter
  4. OpenTelemetry Collector 深入
  5. 后端可视化系统
  6. 完整集成架构
  7. 实践 Demo
  8. 常见问题 QA
  9. 学习检查清单

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 ExporterConsoleSpanExporter开发调试,将数据输出到控制台
OTLP ExporterOTLPSpanExporter标准协议导出,发送到 Collector 或直接到后端
厂商 ExporterJaegerExporter, ZipkinExporter直接对接特定后端(已逐步废弃)
Prometheus ExporterPrometheusMetricReader暴露 /metrics 端点供 Prometheus 拉取
自定义 Exporter继承 SpanExporter对接内部系统或特殊需求

1.4 数据导出的两种模式

推送模式(Push):                    拉取模式(Pull):
┌──────┐   push    ┌──────┐       ┌──────┐   pull    ┌────────────┐
│ App  │ ───────→ │Backend│       │ App  │ ←─────── │ Prometheus │
└──────┘          └──────┘       └──────┘   GET     └────────────┘
                                  │:8889│  /metrics
OTLP、Jaeger、Zipkin              Prometheus Exporter
适用于 Traces、Logs               适用于 Metrics

2. 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)
默认端口43174318
序列化格式ProtobufProtobuf(默认)/ 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-otlp

2.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_provider

2.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[]
            └── status

3. 其他常用 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 True

4. 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 自身的 metrics

4.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 各后端系统详细对比

系统信号类型存储方式查询方式特点适用场景
JaegerTracesElasticsearch/Cassandra/BadgerJaeger UI / APICNCF 毕业项目,功能成熟,社区活跃中小规模追踪
Grafana TempoTraces对象存储(S3/GCS/MinIO)TraceQL高性能、低成本、只需 Trace ID 索引大规模追踪
ZipkinTraces内存/MySQL/Cassandra/ESZipkin UI轻量简单,适合入门小规模/学习
PrometheusMetrics本地 TSDBPromQL生态丰富、告警规则、拉取模式指标监控
Grafana MimirMetrics对象存储PromQL 兼容水平扩展的 Prometheus大规模指标
Grafana LokiLogs对象存储LogQL只索引标签不索引内容、低成本日志聚合
SigNozAllClickHouse内置 UI开源 APM,原生 OTel 支持,一站式全栈可观测性
LangfuseTracesPostgreSQL内置 UILLM 专用,追踪 Token 和 PromptAI/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 UI
  • 4317:OTLP gRPC Receiver
  • 4318: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: tempo

6. 完整集成架构

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
      - loki

otel-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: 10s

grafana/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: jaeger

app/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.43b0

app/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 -v

8. 常见问题 QA

Q1:OTLP gRPC 和 HTTP 如何选择?

A

场景推荐原因
内部网络/KubernetesgRPCHTTP/2 多路复用,性能更好
跨公网/CDNHTTP防火墙友好,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 失败会增加延迟
# 所以生产环境一定要用 BatchSpanProcessor

OTLP 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_sizeschedule_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 error

Q6: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'  # Email

Q8: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 推送到 CollectorPrometheusMetricReader + 暴露端点
数据实时性更实时取决于 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
资源消耗分散在各节点集中消耗分散 + 集中
延迟低(本地通信)较高(网络传输)
可靠性节点故障只影响本机单点风险需集群最高
管理复杂度需管理多实例管理少量实例中等
适用规模大规模小规模大规模
KubernetesDaemonSetDeploymentDaemonSet + 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_total

Q12:如何处理 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.pem
yaml
# 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.pem

Q13:OTLP 的 JSON 格式和 Protobuf 格式有什么区别?

A

维度ProtobufJSON
大小小(二进制)大(文本,约 3-5 倍)
解析速度
人类可读
调试便利
生产推荐否(用于调试)
HTTP Content-Typeapplication/x-protobufapplication/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/json

Q14:如何优雅关闭 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_flushshutdown
  • 设置合理的 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 传播