OpenTelemetry 完全指南:标准化可观测性框架

系统性 OpenTelemetry 实战指南:OTel 架构与核心理念、自动与手动 Instrumentation、Span/Trace/Context 传播、Metrics SDK / Logs SDK / Baggage、语义约定(Semantic Conventions)与资源属性、OTel Collector 架构与 processors/exporters、采样策略(AlwaysOn/Sampler/Parent-based/Rate-limiting)、OTLP 协议、与 Prometheus/Jaeger/Loki 的桥接、Go/Java/Node.js/Python 多语言实战代码。

OpenTelemetry 的使命是终结可观测性领域的「数据孤岛」。 在这之前,每家公司可能同时使用 Zipkin、Jaeger、Prometheus、StatsD、Fluentd 等 5+ 种采集方案,每种都有自己的 SDK 和数据格式。OTel 提供了一个统一的标准,让一次埋点就能产出 Metrics、Logs、Traces 三种信号。


一、OpenTelemetry 架构

1.1 核心组件

OpenTelemetry 架构:

Application Layer
├── Auto Instrumentation   ──→ 零代码入侵(Java Agent / Python Auto / Node.js -require)
├── Manual Instrumentation ──→ 代码显式埋点(优先使用)
│
SDK Layer
├── TracerProvider ──→ 创建 Tracer
├── MeterProvider  ──→ 创建 Meter
├── LoggerProvider ──→ 创建 Logger
├── Context Manager ──→ Context 传播
├── Sampler         ──→ 采样决策
└── Resource        ──→ 服务元数据

Exporter Layer
├── OTLP (gRPC/HTTP)    ──→ Collector / Jaeger / Tempo
├── Prometheus Exporter ──→ Prometheus
├── Console Exporter    ──→ 调试输出
└── Zipkin Exporter     ──→ Zipkin

Collector Layer(可选但推荐)
├── Receivers  ──→ OTLP / Jaeger / Zipkin / Prometheus
├── Processors ──→ Batch / Memory Limit / Tail-based Sampling
└── Exporters  ──→ OTLP / Prometheus / Loki / Elasticsearch / S3

1.2 为什么需要 OTel

维度Before OTelAfter OTel
SDK每种语言 N 个 SDK(Zipkin + Jaeger + Prometheus…)1 个官方 SDK
数据格式各说各话(Thrift / Protobuf / JSON)OTLP 统一协议
埋点成本每换工具重埋一次一次埋点,多处消费
维护成本多个 Agent 打架一个 Collector 代理

二、核心概念

2.1 Trace / Span / Context

Trace = 一次完整的端到端请求链路
  └── Span = 链路中的单个操作单元
      ├── Span ID(当前单元 ID)
      ├── Parent Span ID(父单元 ID)
      ├── Trace ID(全局链路 ID)
      ├── Operation Name(操作名:如 "GET /api/users")
      ├── Start/End Time → Duration
      ├── Status(Unset / Ok / Error)
      ├── Kind(Server / Client / Producer / Consumer / Internal)
      ├── Attributes(键值对元数据)
      ├── Events(时间戳事件:如 "cache miss")
      └── Links(跨 Trace 关联)

2.2 Context 传播

// Go: Context 自动传播
ctx, span := tracer.Start(ctx, "process-payment")
defer span.End()

// HTTP 请求自动注入追踪头
req, _ := http.NewRequestWithContext(ctx, "POST", url, body)
// 自动注入: traceparent: 00-xxx-xxx-01
W3C Trace Context(标准传播头):
  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
               ↑  ↑            ↑                      ↑              ↑
               │  │            │                      │              └── flags
               │  │            │                      └── parent span id
               │  │            └── trace id
               │  └── version
               └── version

  tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
  └── 供应商特定上下文(如 vendor-specific baggage)

2.3 Baggage

// Baggage = 随 Trace 传播的键值对(会出现在所有下游 Span 中)
import "go.opentelemetry.io/otel/baggage"

// 设置 Baggage
member, _ := baggage.NewMember("user.id", "user_123")
bag, _ := baggage.New(member)
ctx = baggage.ContextWithBaggage(ctx, bag)

// 下游 Span 自动读取
userID := baggage.FromContext(ctx).Member("user.id").Value()

三、Go 实战

3.1 初始化 Provider

// otel/setup.go
package otel

import (
    "context"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

func InitTracer(endpoint string) (*sdktrace.TracerProvider, error) {
    ctx := context.Background()

    // 创建 OTLP gRPC Exporter
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint(endpoint),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    // 资源信息(服务元数据)
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("payment-service"),
            semconv.ServiceVersion("1.2.3"),
            semconv.DeploymentEnvironment("production"),
            semconv.HostName("web-01"),
        ),
    )
    if err != nil {
        return nil, err
    }

    // TracerProvider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,
            sdktrace.WithBatchTimeout(5*time.Second),
        ),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.ParentBased(
            sdktrace.TraceIDRatioBased(0.1), // 10% 采样
        )),
    )

    otel.SetTracerProvider(tp)
    return tp, nil
}

3.2 HTTP 服务手动埋点

// main.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("payment-service")

func main() {
    tp, _ := otel.InitTracer("otel-collector:4317")
    defer tp.Shutdown(context.Background())

    http.HandleFunc("/api/payment", handlePayment)
    http.ListenAndServe(":8080", nil)
}

func handlePayment(w http.ResponseWriter, r *http.Request) {
    ctx, span := tracer.Start(r.Context(), "handle-payment",
        trace.WithSpanKind(trace.SpanKindServer),
        trace.WithAttributes(
            attribute.String("http.method", r.Method),
            attribute.String("http.route", "/api/payment"),
            attribute.String("http.target", r.URL.Path),
        ),
    )
    defer span.End()

    // 解析参数
    userID := r.URL.Query().Get("user_id")
    span.SetAttributes(attribute.String("user.id", userID))

    // 验证
    if err := validateUser(ctx, userID); err != nil {
        span.SetStatus(codes.Error, err.Error())
        span.RecordError(err)
        http.Error(w, err.Error(), 400)
        return
    }

    // 处理支付
    if err := processPayment(ctx, userID); err != nil {
        span.SetStatus(codes.Error, "payment failed")
        span.RecordError(err)
        http.Error(w, "Payment failed", 500)
        return
    }

    span.SetAttributes(attribute.Bool("payment.success", true))
    w.Write([]byte("Payment successful"))
}

func processPayment(ctx context.Context, userID string) error {
    ctx, span := tracer.Start(ctx, "process-payment",
        trace.WithSpanKind(trace.SpanKindInternal),
    )
    defer span.End()

    start := time.Now()

    // 调用银行接口
    if err := callBankAPI(ctx); err != nil {
        span.SetAttributes(
            attribute.Bool("bank.api.success", false),
            attribute.Float64("bank.api.duration_ms", float64(time.Since(start).Milliseconds())),
        )
        return err
    }

    span.SetAttributes(
        attribute.Bool("bank.api.success", true),
        attribute.Float64("bank.api.duration_ms", float64(time.Since(start).Milliseconds())),
    )
    return nil
}

3.3 gRPC 自动拦截器

// gRPC 服务端自动埋点
import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
)

server := grpc.NewServer(
    grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
    grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)

// gRPC 客户端自动传播
conn, _ := grpc.Dial(addr,
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
)

四、OTel Collector

4.1 架构

OTel Collector:
┌─────────────────────────────────────────┐
│           Receivers(接收)              │
│  OTLP  Jaeger  Zipkin  Prometheus  File │
│       (gRPC/HTTP)                       │
└─────────────────┬───────────────────────┘
                  │
          ┌───────▼────────┐
          │   Processors   │
          │  Batch/Memory  │
          │  Limit/Tail    │
          │  Sampling/Attr │
          └───────┬────────┘
                  │
┌─────────────────▼───────────────────────┐
│           Exporters(导出)              │
│  OTLP  Prometheus  Jaeger  Loki  S3    │
│  Kafka  Elasticsearch  Datadog  etc.   │
└─────────────────────────────────────────┘

4.2 配置

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  prometheus:
    config:
      scrape_configs:
        - job_name: 'otel-collector'
          scrape_interval: 10s
          static_configs:
            - targets: ['localhost:8888']

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024

  memory_limiter:
    limit_mib: 512
    spike_limit_mib: 128

  resource:
    attributes:
      - key: environment
        value: production
        action: upsert

  tail_sampling:
    decision_wait: 10s
    num_traces: 100
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow_requests
        type: latency
        latency: { threshold_ms: 1000 }

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write

  loki:
    endpoint: http://loki:3100/loki/api/v1/push

  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp, jaeger, zipkin]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/jaeger, otlp/tempo]

    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, resource, batch]
      exporters: [prometheusremotewrite]

    logs:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [loki]

五、语义约定(Semantic Conventions)

5.1 HTTP 语义

属性说明示例
http.request.methodHTTP 方法GET, POST
http.response.status_code状态码200, 404
http.route路由模板/api/users/{id}
server.address服务器地址api.example.com
url.pathURL 路径/api/users
user_agent.originalUser-AgentMozilla/5.0

5.2 数据库语义

属性说明
db.system数据库类型(postgresql, mysql, redis…)
db.statementSQL 语句(脱敏)
db.operation操作(SELECT, INSERT)
db.sql.table表名

六、采样策略

// AlwaysOn — 全部采样(开发环境)
sdktrace.AlwaysSample()

// AlwaysOff — 不采样(仅埋点测试)
sdktrace.NeverSample()

// TraceIDRatioBased — 按比例采样
sdktrace.TraceIDRatioBased(0.01) // 1%

// ParentBased — 跟随父 Span 采样决策(推荐)
sdktrace.ParentBased(
    sdktrace.TraceIDRatioBased(0.1),
    // 可自定义父 Span 状态的行为
)

// 按 Span 属性采样(Collector Tail Sampling)
// 保留所有错误和慢请求,其他随机 1%

七、多语言快速参考

Java

// Spring Boot Auto Instrumentation(零代码)
// java -javaagent:opentelemetry-javaagent.jar \
//      -Dotel.service.name=order-service \
//      -Dotel.traces.exporter=otlp \
//      -jar app.jar

// 手动埋点
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

Span span = tracer.spanBuilder("process-order").startSpan();
try (Scope scope = span.makeCurrent()) {
    span.setAttribute("order.id", orderId);
    processOrder();
} catch (Exception e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR);
    throw e;
} finally {
    span.end();
}

Node.js

// Auto Instrumentation
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4317' }),
  serviceName: 'api-service'
});
sdk.start();

// Express 自动拦截
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');

registerInstrumentations({
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()]
});

Python

# Auto Instrumentation
# opentelemetry-instrument --traces_exporter otlp \
#     --service_name payment-service \
#     python app.py

# 手动埋点
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317"))
trace.get_tracer_provider().add_span_processor(span_processor)

with tracer.start_as_current_span("process-payment") as span:
    span.set_attribute("user.id", user_id)
    process_payment()

八、OTel → 现有生态桥接

OTel 与现有工具集成:
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Application │ →  │ OTel SDK    │ →  │ Collector   │
│  (OTel)     │    │ (OTLP)      │    │             │
└─────────────┘    └─────────────┘    └──────┬──────┘
                                             │
          ┌──────────────────────────────────┼──────┐
          ↓                                  ↓      ↓
    ┌──────────┐  ┌──────────┐  ┌──────────┐ │ ┌──────────┐
    │Prometheus│  │  Jaeger  │  │  Loki    │ │ │ Tempo    │
    │ (metrics)│  │ (traces) │  │ (logs)   │ │ │ (traces) │
    └──────────┘  └──────────┘  └──────────┘ │ └──────────┘
          ↓                                  ↓
    ┌──────────┐                    ┌──────────┐
    │  Grafana │ ←─────────────────┘          │
    └──────────┘                               │
          ↓                                    │
    ┌──────────┐                               │
    │Dashboards│                               │
    └──────────┘                               │
          ↓                                    │
    ┌──────────┐                               │
    │Alerting  │ ←─────────────────────────────┘
    └──────────┘ (Collector 也能直接输出到告警系统)

九、OTel Checklist

检查项配置
Service Name 必填OTEL_SERVICE_NAME
Resource 属性env, version, host, instance
Context 传播W3C Trace Context 标准
Baggage 敏感信息不传播 PII(个人身份信息)
采样率生产 1-10%,开发 100%
Batch Exporter启用批量导出,防止频繁网络请求
Error 记录span.RecordError(err) + span.SetStatus(Error)
SQL 脱敏不记录原始 SQL 参数

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 可观测性数据存储选型:TSDB、列式存储、对象存储与成本优化
  2. 云原生 APM 与性能剖析:Continuous Profiling 与火焰图
  3. Kubernetes 可观测性实战:集群、Pod、网络、存储全链路监控