Skip to content

阶段 3:Metrics(指标)深入

一、Metrics 核心概念

1.1 什么是 Metrics?

Metrics 是对系统运行状况的数值化度量,它关注的是"系统现在怎么样?趋势如何?"

Traces 回答:  "这个请求走了哪条路径?每步耗时多少?"  → 单个请求的细节
Metrics 回答: "过去 5 分钟平均延迟是多少?错误率是多少?" → 全局的统计视角
Logs 回答:    "10:23:45 发生了什么具体事件?"           → 单个事件的详情

Metrics 的核心价值

价值说明示例
告警基于阈值触发通知错误率 > 5% → 告警
趋势分析观察指标随时间的变化延迟 P99 从 100ms 逐渐升到 500ms
容量规划预测资源需求QPS 每月增长 20%,三个月后需扩容
SLA/SLO 监控衡量服务质量99.9% 请求延迟 < 200ms
业务洞察用数据驱动决策哪个 API 最热门?哪个地区流量最大?

Metrics vs Traces 的定位差异

Traces(显微镜):                    Metrics(仪表盘):
┌────────────────────────┐         ┌────────────────────────┐
│ Trace ID: abc123       │         │ 请求总数:   125,430    │
│ ├─ Gateway    20ms     │         │ 错误率:     0.3%       │
│ ├─ UserSvc    5ms      │         │ P50 延迟:   45ms       │
│ └─ OrderSvc   15ms     │         │ P99 延迟:   230ms      │
│     ├─ DB     3ms      │         │ 活跃连接:   342        │
│     └─ Redis  1ms      │         │ CPU:        67%        │
└────────────────────────┘         └────────────────────────┘
看单个请求的完整路径                   看全局的整体健康状况
数据量大(每个请求一条 Trace)         数据量小(聚合后的数值)
排查具体问题                          监控和告警

日常类比

Metrics 就像汽车仪表盘:
  - 速度表(Gauge)       → 当前速度是多少?
  - 里程表(Counter)     → 总共跑了多少公里?
  - 油量表(Gauge)       → 还剩多少油?
  - 转速分布(Histogram) → 引擎在各转速段运行了多少时间?
  
你不需要看每秒钟的行驶记录(Traces),
只需要看仪表盘(Metrics)就能知道车的状态。

1.2 OpenTelemetry Metrics 架构

                    ┌────────────────────────────────────┐
                    │         MeterProvider               │
                    │  ├── Resource(服务标识信息)          │
                    │  ├── Views(视图,自定义聚合规则)      │
                    │  └── MetricReader(读取+导出)        │
                    └──────────┬─────────────────────────┘
                               │ get_meter("name", "ver")

                    ┌────────────────────────────────────┐
                    │         Meter                       │
                    │  创建各种 Instrument(仪器)           │
                    └──────────┬─────────────────────────┘
                               │ create_counter() / create_histogram() / ...

                    ┌────────────────────────────────────┐
                    │      Instruments(仪器)             │
                    │  ├── Counter                        │
                    │  ├── UpDownCounter                  │
                    │  ├── Histogram                      │
                    │  ├── Gauge                          │
                    │  ├── ObservableCounter              │
                    │  ├── ObservableUpDownCounter        │
                    │  └── ObservableGauge                │
                    └──────────┬─────────────────────────┘
                               │ add() / record() / 回调

                    ┌────────────────────────────────────┐
                    │      Aggregation(聚合)             │
                    │  ├── Sum(求和,默认用于 Counter)     │
                    │  ├── LastValue(取最新值,用于 Gauge) │
                    │  └── ExplicitBucketHistogram        │
                    │       (桶分布,用于 Histogram)       │
                    └──────────┬─────────────────────────┘
                               │ MetricReader 定时读取

                    ┌────────────────────────────────────┐
                    │      MetricExporter(导出器)        │
                    │  ├── ConsoleMetricExporter          │
                    │  ├── OTLPMetricExporter             │
                    │  └── PrometheusMetricReader         │
                    └──────────┬─────────────────────────┘


                         ┌────────────┐
                         │  后端系统   │
                         │ Prometheus │
                         │ Grafana    │
                         └────────────┘

与 Traces 架构的对比

维度TracesMetrics
ProviderTracerProviderMeterProvider
创建者TracerMeter
数据单元SpanMeasurement(测量值)
处理器SpanProcessorMetricReader
导出器SpanExporterMetricExporter
数据特点高基数、单条记录低基数、聚合后导出
导出方式每条 Span 独立导出聚合后按时间间隔批量导出

二、MeterProvider 与 Meter

2.1 MeterProvider 配置

MeterProvider 是 Metrics 子系统的入口,一个应用通常只需要一个 MeterProvider。

python
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
    PeriodicExportingMetricReader,
    ConsoleMetricExporter,
)
from opentelemetry.sdk.resources import Resource

# 1. 定义 Resource(标识你的服务)
resource = Resource.create({
    "service.name": "order-service",
    "service.version": "1.2.0",
    "deployment.environment": "production",
})

# 2. 创建 MetricReader
#    PeriodicExportingMetricReader 会定时从 MeterProvider 读取聚合数据并导出
reader = PeriodicExportingMetricReader(
    ConsoleMetricExporter(),
    export_interval_millis=10000,  # 每 10 秒导出一次(默认 60000ms)
    export_timeout_millis=30000,   # 导出超时 30 秒
)

# 3. 创建 MeterProvider
provider = MeterProvider(
    resource=resource,
    metric_readers=[reader],
)

# 4. 设置为全局 MeterProvider
metrics.set_meter_provider(provider)

2.2 获取 Meter

Meter 是创建 Instrument 的工厂,类似 Tracer 对于 Span 的关系。

python
# 获取 Meter,传入模块名和版本
meter = metrics.get_meter(
    name="order-service.metrics",   # 标识产生指标的模块
    version="1.0.0",                # 模块版本(可选)
)

# 常见做法:按功能模块区分 Meter
http_meter = metrics.get_meter("myapp.http")
db_meter = metrics.get_meter("myapp.database")
biz_meter = metrics.get_meter("myapp.business")

注意:多个 Meter 共享同一个 MeterProvider 的配置(Resource、Reader、Views),它们之间的区别仅在于 name 和 version,用于在后端标识指标来源。

2.3 MetricReader 的工作机制

MetricReader 的职责:定时从 MeterProvider 读取聚合好的指标数据,并交给 Exporter 导出。

时间轴示意:

  t=0s     t=10s    t=20s    t=30s
  ──┬────────┬────────┬────────┬──
    │        │        │        │
    │  采集期 │  采集期 │  采集期 │
    │   #1   │   #2   │   #3   │
    │        │        │        │
    └────────┘────────┘────────┘
         ↓        ↓        ↓
      导出 #1   导出 #2   导出 #3

每个采集期内的所有 add() / record() 调用都会被聚合,
在导出时作为一个数据点输出。
python
# PeriodicExportingMetricReader 参数详解
reader = PeriodicExportingMetricReader(
    exporter=ConsoleMetricExporter(),
    export_interval_millis=60000,   # 导出间隔,默认 60 秒
    export_timeout_millis=30000,    # 单次导出的超时时间
)

# 也可以使用多个 Reader,将指标同时发送到多个后端
console_reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
otlp_reader = PeriodicExportingMetricReader(OTLPMetricExporter())

provider = MeterProvider(
    metric_readers=[console_reader, otlp_reader],
)

三、Instrument 类型详解

3.1 Instrument 分类总览

OpenTelemetry 定义了 7 种 Instrument,分为同步异步两大类:

Instruments(仪器)
├── 同步(Synchronous)—— 在业务代码中主动调用
│   ├── Counter            只增不减的累计计数器
│   ├── UpDownCounter      可增可减的计数器
│   ├── Histogram          记录值的分布
│   └── Gauge              记录瞬时值(Python SDK 1.x 新增)

└── 异步(Asynchronous / Observable)—— 通过回调函数按需采集
    ├── ObservableCounter         异步只增计数器
    ├── ObservableUpDownCounter   异步可增减计数器
    └── ObservableGauge           异步瞬时值

同步 vs 异步的核心差异

维度同步 Instrument异步 Instrument
调用时机业务代码中主动调用MetricReader 导出时回调
调用者你的业务代码OpenTelemetry SDK
适用场景事件驱动(每次请求计数、每次操作记录)轮询采集(CPU 使用率、内存、队列深度)
代码位置分散在业务逻辑各处集中在回调函数中
类比每次投篮时记分每隔 10 秒看一下比分板

3.2 Counter(计数器)

适用场景:累计值,只增不减。如请求总数、处理的消息数、错误总数。

python
# 创建 Counter
request_counter = meter.create_counter(
    name="http.server.request.count",
    description="Total number of HTTP requests received",
    unit="1",  # 无量纲单位用 "1"
)

# 使用:每次请求时调用 add()
def handle_request(method, route, status_code):
    # add() 的第一个参数必须是非负数
    request_counter.add(
        1,
        attributes={
            "http.method": method,
            "http.route": route,
            "http.status_code": status_code,
        },
    )

# 示例调用
handle_request("GET", "/api/users", 200)
handle_request("POST", "/api/orders", 201)
handle_request("GET", "/api/users", 500)
输出示例(ConsoleMetricExporter):

{
  "resource_metrics": [{
    "scope_metrics": [{
      "metrics": [{
        "name": "http.server.request.count",
        "data": {
          "data_points": [
            { "attributes": {"http.method": "GET", "http.route": "/api/users", "http.status_code": 200}, "value": 1 },
            { "attributes": {"http.method": "POST", "http.route": "/api/orders", "http.status_code": 201}, "value": 1 },
            { "attributes": {"http.method": "GET", "http.route": "/api/users", "http.status_code": 500}, "value": 1 }
          ]
        }
      }]
    }]
  }]
}

关键理解:每组唯一的 attributes 组合就是一个独立的时间序列(Time Series)。{"method": "GET", "route": "/api/users"}{"method": "POST", "route": "/api/orders"} 是两条不同的时间序列。

3.3 UpDownCounter(可增减计数器)

适用场景:值可以增加也可以减少的指标。如当前活跃连接数、队列中的消息数、线程池活跃线程数。

python
# 创建 UpDownCounter
active_connections = meter.create_up_down_counter(
    name="http.server.active_connections",
    description="Number of active HTTP connections",
    unit="1",
)

# 连接建立时 +1
def on_connection_open(client_ip):
    active_connections.add(1, {"net.peer.ip": client_ip})

# 连接关闭时 -1
def on_connection_close(client_ip):
    active_connections.add(-1, {"net.peer.ip": client_ip})

# 模拟
on_connection_open("192.168.1.10")
on_connection_open("192.168.1.20")
on_connection_open("192.168.1.30")
on_connection_close("192.168.1.10")
# 此时 active_connections 净值为 2

Counter vs UpDownCounter 的选择

问自己一个问题:这个值能减少吗?

  总请求数     → Counter        (只可能增加)
  总错误数     → Counter        (只可能增加)
  总处理字节   → Counter        (只可能增加)
  
  活跃连接数   → UpDownCounter  (连接打开+1,关闭-1)
  队列深度     → UpDownCounter  (入队+1,出队-1)
  线程池大小   → UpDownCounter  (创建+1,销毁-1)
  
  CPU 使用率   → Gauge          (这不是累计值,是瞬时测量值)

3.4 Histogram(直方图)

适用场景:记录值的分布,不仅关心平均值,更关心分位数(P50、P90、P99)。如请求延迟分布、响应体大小分布、数据库查询耗时分布。

python
# 创建 Histogram
request_duration = meter.create_histogram(
    name="http.server.request.duration",
    description="HTTP request duration in milliseconds",
    unit="ms",
)

# 每次请求结束时记录耗时
import time

def handle_request(method, route):
    start = time.time()
    
    # ... 处理请求 ...
    process_request()
    
    duration_ms = (time.time() - start) * 1000
    request_duration.record(
        duration_ms,
        attributes={
            "http.method": method,
            "http.route": route,
        },
    )

# 或者用上下文管理器封装
import contextlib

@contextlib.contextmanager
def measure_duration(histogram, attributes=None):
    start = time.time()
    try:
        yield
    finally:
        duration_ms = (time.time() - start) * 1000
        histogram.record(duration_ms, attributes or {})

# 使用
with measure_duration(request_duration, {"http.method": "GET", "http.route": "/api/users"}):
    process_request()

Histogram 的默认桶边界

OpenTelemetry 的 Histogram 使用桶(Bucket) 来统计分布:

默认桶边界:[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]

假设记录了以下延迟值:[3, 8, 15, 42, 42, 78, 120, 350, 1200]

桶分布结果:
  bucket ≤ 0:     0 个
  bucket ≤ 5:     1 个 (3)
  bucket ≤ 10:    2 个 (3, 8)
  bucket ≤ 25:    3 个 (3, 8, 15)
  bucket ≤ 50:    5 个 (3, 8, 15, 42, 42)
  bucket ≤ 75:    5 个
  bucket ≤ 100:   6 个 (+ 78)
  bucket ≤ 250:   7 个 (+ 120)
  bucket ≤ 500:   8 个 (+ 350)
  bucket ≤ 1000:  8 个
  bucket ≤ 2500:  9 个 (+ 1200)
  ...
  bucket ≤ +Inf:  9 个

同时还会记录:
  count = 9        (总记录数)
  sum = 1858       (所有值的总和)
  min = 3          (最小值)
  max = 1200       (最大值)

为什么用桶而不是精确值? 因为 Metrics 追求的是低存储开销的统计视图。存储每个请求的精确延迟代价太大(那是 Traces 的事),用桶就可以用固定的内存估算出 P50/P90/P99 等分位数。

3.5 Gauge(瞬时值)

适用场景:当前时刻的值,不需要累计。如温度、CPU 使用率、内存占用。

python
# 同步 Gauge(Python SDK 1.x 支持)
cpu_gauge = meter.create_gauge(
    name="system.cpu.utilization",
    description="CPU utilization percentage",
    unit="%",
)

# 定期记录当前值
import psutil

def report_system_metrics():
    cpu_gauge.set(psutil.cpu_percent())

# 注意:Gauge 使用 set(),直接设置当前值
# 与 Counter 的 add() 不同,Gauge 不是累加的

注意:在 Python SDK 的早期版本中没有同步 Gauge,需要用 ObservableGauge(异步回调)来实现。从 opentelemetry-sdk >= 1.22.0 开始,同步 Gauge 已可用。

3.6 Observable Instruments(异步仪器)

异步 Instrument 不在业务代码中主动调用,而是在 MetricReader 导出时,SDK 自动调用你注册的回调函数来采集数据。

python
import psutil
from opentelemetry import metrics

# ============ ObservableGauge ============
# 适合:轮询式获取瞬时值

def cpu_usage_callback(options):
    """每次 MetricReader 导出时,SDK 会调用此函数"""
    cpu_percent = psutil.cpu_percent(percpu=True)
    for i, usage in enumerate(cpu_percent):
        yield metrics.Observation(
            value=usage,
            attributes={"cpu.core": str(i)},
        )

meter.create_observable_gauge(
    name="system.cpu.usage",
    callbacks=[cpu_usage_callback],
    description="Per-core CPU usage percentage",
    unit="%",
)


# ============ ObservableCounter ============
# 适合:轮询式获取只增累计值(如系统的网络收发字节数)

def network_bytes_callback(options):
    net_io = psutil.net_io_counters()
    yield metrics.Observation(
        value=net_io.bytes_sent,
        attributes={"direction": "sent"},
    )
    yield metrics.Observation(
        value=net_io.bytes_recv,
        attributes={"direction": "received"},
    )

meter.create_observable_counter(
    name="system.network.bytes",
    callbacks=[network_bytes_callback],
    description="Total network bytes transferred",
    unit="By",
)


# ============ ObservableUpDownCounter ============
# 适合:轮询式获取可增减值(如线程数、队列长度)

import threading

def thread_count_callback(options):
    yield metrics.Observation(
        value=threading.active_count(),
    )

meter.create_observable_up_down_counter(
    name="process.thread.count",
    callbacks=[thread_count_callback],
    description="Number of active threads",
)

同步 vs 异步的选择决策树

你能在事件发生时立即记录吗?

├── 是 → 使用同步 Instrument
│   ├── 每次请求/事件时记录 → Counter / UpDownCounter / Histogram
│   └── 需要直接设置当前值 → Gauge

└── 否(需要定时采集)→ 使用异步 Instrument
    ├── 采集的是累计值 → ObservableCounter
    ├── 采集的值可增可减 → ObservableUpDownCounter
    └── 采集的是瞬时值 → ObservableGauge
    
常见例子:
  HTTP 请求计数      → Counter(每次请求时 add(1))
  请求延迟           → Histogram(每次请求结束时 record())
  CPU 使用率         → ObservableGauge(轮询 psutil)
  网络发送字节总数    → ObservableCounter(轮询系统计数器)
  线程池活跃线程      → ObservableUpDownCounter(轮询线程池状态)

四、Attributes(属性)与基数控制

4.1 Attributes 的作用

Attributes 是附加在测量值上的键值对,用于维度拆分。同一个指标名 + 不同的 Attributes 组合 = 不同的时间序列。

python
# 同一个 counter,不同的 attributes 产生不同的时间序列
counter.add(1, {"http.method": "GET",  "http.route": "/api/users"})
counter.add(1, {"http.method": "POST", "http.route": "/api/users"})
counter.add(1, {"http.method": "GET",  "http.route": "/api/orders"})

# 在后端查询时可以灵活过滤和聚合:
#   sum(http_request_count) where method="GET"
#   sum(http_request_count) group by route

4.2 基数爆炸(Cardinality Explosion)

这是 Metrics 领域最常见也最危险的问题。

什么是基数(Cardinality)?
  = 一个指标的所有 attributes 组合产生的时间序列数量

低基数(安全):
  http.method × http.route = 5 × 20 = 100 个时间序列 ✅

高基数(危险):
  http.method × http.route × user.id = 5 × 20 × 100000 = 10,000,000 个时间序列 ❌

基数爆炸的后果

  • 内存占用暴涨(每个时间序列都需要维护聚合状态)
  • 导出数据量激增,网络带宽和后端存储压力增大
  • 后端查询变慢(Prometheus 查询超时)
  • 最终可能导致 OOM 或服务不可用

如何避免基数爆炸

python
# ❌ 错误做法:把高基数值放入 attributes
counter.add(1, {
    "user.id": "user-12345",           # 用户 ID 基数可能上百万
    "request.id": "req-abc-123",       # 每个请求都不同,基数无限
    "http.url": "/api/users/12345",    # URL 包含变量,基数随用户数增长
})

# ✅ 正确做法:使用低基数的分类值
counter.add(1, {
    "http.method": "GET",              # 基数 ~7(GET/POST/PUT/DELETE/PATCH...)
    "http.route": "/api/users/{id}",   # 路由模板,基数 = 路由数
    "http.status_code": 200,           # 基数 ~20
})

# 💡 原则:user_id 这样的高基数标识符应该放在 Traces 的 Span Attributes 中,
#         而不是 Metrics 的 Attributes 中。

基数控制清单

检查项说明
不要用 user_id / session_id高基数标识符放 Traces
使用路由模板而非实际路径/api/users/{id} 而非 /api/users/12345
限制 attribute 值的种类确保每个 attribute 的取值范围有限
监控时间序列数量关注 Prometheus 的 scrape_series_added 指标
使用 Views 过滤不需要的 attributes参见下文 Views 章节

五、Views(视图)

5.1 Views 的作用

Views 是 MeterProvider 级别的配置,允许你自定义指标的聚合方式,而无需修改业务代码。

Views 能做什么?

  • 重命名指标
  • 过滤掉不需要的 attributes(降低基数)
  • 修改 Histogram 的桶边界
  • 更改聚合方式
  • 丢弃(Drop)不需要的指标

5.2 Views 实战

python
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation

# ============ 示例 1:自定义 Histogram 桶边界 ============
# 默认桶边界对于毫秒级延迟不够精细,自定义更合适的边界
latency_view = View(
    instrument_name="http.server.request.duration",
    aggregation=ExplicitBucketHistogramAggregation(
        boundaries=[1, 5, 10, 25, 50, 100, 250, 500, 1000, 5000]
    ),
)


# ============ 示例 2:过滤 Attributes 降低基数 ============
# 只保留 method 和 status_code,丢弃 route 等高基数属性
filter_view = View(
    instrument_name="http.server.request.count",
    attribute_keys=["http.method", "http.status_code"],  # 只保留这些 key
)


# ============ 示例 3:重命名指标 ============
rename_view = View(
    instrument_name="http.server.request.count",
    name="total_requests",  # 导出时的指标名变为 total_requests
)


# ============ 示例 4:丢弃不需要的指标 ============
from opentelemetry.sdk.metrics.view import DropAggregation

drop_view = View(
    instrument_name="debug.*",  # 支持通配符匹配
    aggregation=DropAggregation(),
)


# ============ 将 Views 配置到 MeterProvider ============
provider = MeterProvider(
    metric_readers=[reader],
    views=[latency_view, filter_view, drop_view],
)

5.3 Views 的匹配规则

python
# View 可以按以下条件匹配 Instrument:
View(
    instrument_name="http.*",               # 按名称匹配(支持 * 通配符)
    instrument_type=Counter,                # 按类型匹配
    meter_name="myapp.http",                # 按 Meter 名称匹配
    meter_version="1.0.0",                  # 按 Meter 版本匹配
    meter_schema_url="https://...",         # 按 schema URL 匹配
)

# 匹配优先级:精确匹配 > 通配符匹配 > 默认行为
# 如果没有 View 匹配到某个 Instrument,则使用默认聚合方式

六、Temporality(时间性)

6.1 什么是 Temporality?

Temporality 定义了导出的数据点表示的是累计值还是增量值

假设一个 Counter 每秒 add(1),导出间隔 10 秒:

Cumulative(累计)模式:
  t=10s → 导出 value=10   (从程序启动以来的总和)
  t=20s → 导出 value=20
  t=30s → 导出 value=30

Delta(增量)模式:
  t=10s → 导出 value=10   (最近 10 秒内的增量)
  t=20s → 导出 value=10
  t=30s → 导出 value=10

6.2 Cumulative vs Delta

维度Cumulative(累计)Delta(增量)
表示从启动到现在的总值本导出周期内的增量
优点数据不会丢失,天然单调递增,易于告警数据量小,不受重启影响
缺点重启后从 0 开始,需要后端处理丢失一个导出周期就丢数据
典型后端Prometheus(只支持 Cumulative)OTLP / Datadog / Dynatrace
默认Counter、Histogram 默认用 Cumulative
python
from opentelemetry.sdk.metrics.export import (
    PeriodicExportingMetricReader,
    ConsoleMetricExporter,
)

# 使用 Delta Temporality
reader = PeriodicExportingMetricReader(
    ConsoleMetricExporter(
        preferred_temporality={
            Counter: AggregationTemporality.DELTA,
            Histogram: AggregationTemporality.DELTA,
            UpDownCounter: AggregationTemporality.CUMULATIVE,
        }
    ),
)

七、实践 Demo

Demo 1:完整的 Web 服务 Metrics

python
"""
demo_web_metrics.py
一个模拟 Web 服务的 Metrics 完整示例,展示所有常用 Instrument 类型。

运行:pip install opentelemetry-api opentelemetry-sdk psutil
      python demo_web_metrics.py
"""
import time
import random
import threading
import psutil
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
    PeriodicExportingMetricReader,
    ConsoleMetricExporter,
)
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
from opentelemetry.sdk.resources import Resource


def setup_metrics():
    resource = Resource.create({
        "service.name": "demo-web-service",
        "service.version": "1.0.0",
    })

    latency_view = View(
        instrument_name="http.server.request.duration",
        aggregation=ExplicitBucketHistogramAggregation(
            boundaries=[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
        ),
    )

    reader = PeriodicExportingMetricReader(
        ConsoleMetricExporter(),
        export_interval_millis=5000,
    )

    provider = MeterProvider(
        resource=resource,
        metric_readers=[reader],
        views=[latency_view],
    )
    metrics.set_meter_provider(provider)
    return metrics.get_meter("demo-web-service", "1.0.0")


def create_instruments(meter):
    """创建所有 Instrument"""
    instruments = {
        "request_counter": meter.create_counter(
            name="http.server.request.count",
            description="Total HTTP requests",
            unit="1",
        ),
        "error_counter": meter.create_counter(
            name="http.server.error.count",
            description="Total HTTP errors",
            unit="1",
        ),
        "request_duration": meter.create_histogram(
            name="http.server.request.duration",
            description="HTTP request latency",
            unit="ms",
        ),
        "active_connections": meter.create_up_down_counter(
            name="http.server.active_connections",
            description="Current active connections",
            unit="1",
        ),
        "response_size": meter.create_histogram(
            name="http.server.response.size",
            description="HTTP response body size",
            unit="By",
        ),
    }

    # 异步 Instrument:系统指标
    def cpu_callback(options):
        yield metrics.Observation(psutil.cpu_percent())

    def memory_callback(options):
        mem = psutil.virtual_memory()
        yield metrics.Observation(mem.percent)

    meter.create_observable_gauge(
        name="system.cpu.usage",
        callbacks=[cpu_callback],
        description="CPU usage percentage",
        unit="%",
    )
    meter.create_observable_gauge(
        name="system.memory.usage",
        callbacks=[memory_callback],
        description="Memory usage percentage",
        unit="%",
    )

    return instruments


ROUTES = ["/api/users", "/api/orders", "/api/products", "/api/health"]
METHODS = ["GET", "POST", "PUT", "DELETE"]


def simulate_request(instruments):
    """模拟一次 HTTP 请求"""
    method = random.choice(METHODS)
    route = random.choice(ROUTES)

    attrs = {"http.method": method, "http.route": route}

    instruments["active_connections"].add(1, attrs)

    base_latency = {"GET": 30, "POST": 80, "PUT": 60, "DELETE": 40}
    latency = base_latency.get(method, 50) + random.uniform(-20, 100)
    time.sleep(latency / 1000)

    is_error = random.random() < 0.05
    status_code = random.choice([500, 502, 503]) if is_error else 200

    attrs["http.status_code"] = status_code

    instruments["request_counter"].add(1, attrs)
    instruments["request_duration"].record(latency, attrs)
    instruments["response_size"].record(random.randint(100, 10000), attrs)

    if is_error:
        instruments["error_counter"].add(1, attrs)

    instruments["active_connections"].add(-1, {"http.method": method, "http.route": route})


def main():
    meter = setup_metrics()
    instruments = create_instruments(meter)

    print("Starting metrics demo... (will export every 5 seconds)")
    print("Press Ctrl+C to stop.\n")

    try:
        while True:
            threads = []
            for _ in range(random.randint(3, 10)):
                t = threading.Thread(target=simulate_request, args=(instruments,))
                t.start()
                threads.append(t)
            for t in threads:
                t.join()
            time.sleep(0.5)
    except KeyboardInterrupt:
        print("\nShutting down...")
        metrics.get_meter_provider().shutdown()


if __name__ == "__main__":
    main()

Demo 2:与 Prometheus 集成

python
"""
demo_prometheus_metrics.py
将 Metrics 导出到 Prometheus 的示例。

安装:pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-prometheus
运行:python demo_prometheus_metrics.py
然后访问 http://localhost:8000 查看 Prometheus 格式的指标
"""
from prometheus_client import start_http_server
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.prometheus import PrometheusMetricReader
import time
import random


def setup_prometheus_metrics():
    resource = Resource.create({"service.name": "prometheus-demo"})

    # PrometheusMetricReader 会在 /metrics 端点暴露数据
    prometheus_reader = PrometheusMetricReader()

    provider = MeterProvider(
        resource=resource,
        metric_readers=[prometheus_reader],
    )
    metrics.set_meter_provider(provider)
    return metrics.get_meter("prometheus-demo", "1.0.0")


def main():
    # 启动 Prometheus HTTP server(端口 8000)
    start_http_server(8000)
    print("Prometheus metrics available at http://localhost:8000")

    meter = setup_prometheus_metrics()

    request_counter = meter.create_counter(
        name="app_requests_total",
        description="Total requests",
    )
    request_latency = meter.create_histogram(
        name="app_request_duration_seconds",
        description="Request duration in seconds",
        unit="s",
    )

    while True:
        method = random.choice(["GET", "POST"])
        status = random.choice([200, 200, 200, 200, 500])
        latency = random.uniform(0.01, 0.5)

        request_counter.add(1, {"method": method, "status": str(status)})
        request_latency.record(latency, {"method": method})
        time.sleep(0.1)


if __name__ == "__main__":
    main()
Prometheus 端点输出示例(http://localhost:8000):

# HELP app_requests_total_total Total requests
# TYPE app_requests_total_total counter
app_requests_total_total{method="GET",status="200"} 142.0
app_requests_total_total{method="POST",status="200"} 98.0
app_requests_total_total{method="GET",status="500"} 12.0

# HELP app_request_duration_seconds Request duration in seconds
# TYPE app_request_duration_seconds histogram
app_request_duration_seconds_bucket{le="0.005",method="GET"} 0.0
app_request_duration_seconds_bucket{le="0.01",method="GET"} 3.0
app_request_duration_seconds_bucket{le="0.025",method="GET"} 15.0
app_request_duration_seconds_bucket{le="0.05",method="GET"} 32.0
...
app_request_duration_seconds_count{method="GET"} 154.0
app_request_duration_seconds_sum{method="GET"} 38.72

Demo 3:Metrics 与 Traces 关联

python
"""
demo_metrics_with_traces.py
展示如何同时使用 Metrics 和 Traces,并通过 Exemplar 关联两者。

安装:pip install opentelemetry-api opentelemetry-sdk
运行:python demo_metrics_with_traces.py
"""
import time
import random
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


resource = Resource.create({"service.name": "metrics-traces-demo"})

# 配置 Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)

# 配置 Metrics
metric_reader = PeriodicExportingMetricReader(
    ConsoleMetricExporter(),
    export_interval_millis=5000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)

tracer = trace.get_tracer("demo")
meter = metrics.get_meter("demo")

request_counter = meter.create_counter("demo.request.count")
request_duration = meter.create_histogram("demo.request.duration", unit="ms")


def handle_order(order_id):
    with tracer.start_as_current_span("handle_order") as span:
        span.set_attribute("order.id", order_id)

        latency = random.uniform(10, 500)
        time.sleep(latency / 1000)

        attrs = {"operation": "create_order"}
        request_counter.add(1, attrs)
        request_duration.record(latency, attrs)

        # 如果延迟过高,在 Span 中记录事件
        if latency > 300:
            span.add_event("high_latency_detected", {"latency_ms": latency})

        return f"Order {order_id} processed in {latency:.1f}ms"


for i in range(20):
    result = handle_order(f"ORD-{i:04d}")
    print(result)
    time.sleep(0.2)

meter_provider.shutdown()
trace_provider.shutdown()

八、Semantic Conventions(语义约定)

OpenTelemetry 定义了一组标准的指标名和 attribute 名,确保不同服务、不同语言产生的指标可以统一查询。

8.1 常用 HTTP 指标语义约定

指标名类型单位说明
http.server.request.durationHistogramsHTTP 请求延迟
http.server.active_requestsUpDownCounter1当前活跃请求数
http.server.request.body.sizeHistogramBy请求体大小
http.server.response.body.sizeHistogramBy响应体大小

8.2 常用 HTTP Attributes

Attribute示例说明
http.request.methodGETHTTP 方法
url.schemehttpsURL scheme
http.route/api/users/{id}路由模板
http.response.status_code200响应状态码
server.addressapi.example.com服务器地址
network.protocol.version1.1HTTP 协议版本

8.3 常用系统指标语义约定

指标名类型单位说明
system.cpu.utilizationGauge1CPU 使用率 (0~1)
system.memory.usageUpDownCounterBy内存使用量
system.disk.ioCounterBy磁盘 IO 字节数
process.cpu.utilizationGauge1进程 CPU 使用率

参考:完整的语义约定见 OpenTelemetry Semantic Conventions


九、常见问题 QA

Q1:Counter 和 Gauge 有什么区别?什么时候用哪个?

A:核心区别在于是否累计

维度CounterGauge
值的变化只增不减(单调递增)可以任意增减
语义累计总量当前瞬时值
重启后从 0 开始重新累计与历史无关,报告当前值
后端处理通常用 rate() 计算速率直接使用
决策指南:

  "总共处理了多少请求?"     → Counter    (累计值,只增)
  "当前 CPU 使用率是多少?"  → Gauge      (瞬时值,直接读取)
  "当前有多少活跃连接?"     → UpDownCounter(连接来了+1,走了-1,是累加逻辑)
  "当前温度是多少?"         → Gauge      (直接报告当前值,不是累加的)

Q2:为什么 Histogram 不直接记录精确值?桶分布有什么好处?

A:性能和存储的权衡。

精确值方案(不用 Histogram):
  - 假设 QPS = 1000,每秒记录 1000 个精确值
  - 1 分钟 = 60,000 个值
  - 1 小时 = 3,600,000 个值
  - 存储和查询成本巨大

桶分布方案(Histogram):
  - 不管 QPS 多少,每个桶只存一个计数值
  - 15 个桶 + count + sum + min + max = ~20 个数值
  - 1 小时还是 ~20 个数值(按导出间隔聚合)
  - 可以估算 P50/P90/P99

代价:
  - 分位数是近似值(精度取决于桶边界设置)
  - 桶边界需要预先配置

如果你需要精确的分位数计算,可以考虑 ExponentialBucketHistogramAggregation(指数桶直方图),它能以更少的桶实现更高的精度。

Q3:Metrics 的 Attributes 和 Traces 的 Span Attributes 是一回事吗?

A:概念类似但有关键区别:

维度Metrics AttributesSpan Attributes
影响每组唯一组合 = 一个时间序列只是 Span 的附加信息
基数敏感度极度敏感,必须控制相对宽松
能放 user_id 吗?不推荐(基数爆炸)可以
存储方式聚合后的维度标签单条记录的属性
python
# Traces:高基数 OK
span.set_attribute("user.id", "user-12345")          # ✅
span.set_attribute("order.id", "ORD-67890")           # ✅

# Metrics:高基数 NOT OK
counter.add(1, {"user.id": "user-12345"})             # ❌ 基数爆炸!
counter.add(1, {"http.method": "GET", "status": 200}) # ✅ 低基数

Q4:PeriodicExportingMetricReader 的 export_interval_millis 设多大合适?

A:取决于场景:

场景推荐间隔理由
开发调试1-5 秒快速看到结果
生产环境(Prometheus)15-60 秒与 Prometheus scrape 间隔匹配
生产环境(OTLP push)10-60 秒平衡实时性和性能
高 QPS 生产环境30-60 秒减少导出开销
python
# 开发环境
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)

# 生产环境
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=30000)

Q5:Observable(异步)Instrument 的回调函数有什么限制?

A

  1. 回调必须快速返回:SDK 在导出线程中调用回调,长时间阻塞会影响导出。
  2. 不要在回调中做 IO 密集操作:如数据库查询、HTTP 请求。
  3. 回调异常不会传播:SDK 会捕获异常并跳过该数据点。
  4. 每次回调应返回完整数据:不要依赖上一次回调的状态。
python
# ❌ 不好的做法
def bad_callback(options):
    result = requests.get("http://monitoring-api/cpu")  # HTTP 请求,太慢
    yield metrics.Observation(result.json()["cpu"])

# ✅ 好的做法
def good_callback(options):
    yield metrics.Observation(psutil.cpu_percent())  # 快速的本地调用

Q6:如何给一个已有的 FastAPI/Flask 应用快速添加 Metrics?

A:推荐使用自动检测(auto-instrumentation),零代码修改:

bash
# 1. 安装
pip install opentelemetry-distro
pip install opentelemetry-instrumentation-fastapi  # 或 flask
pip install opentelemetry-exporter-otlp

# 2. 自动安装所有可用的 instrumentation
opentelemetry-bootstrap -a install

# 3. 启动应用
OTEL_SERVICE_NAME=my-fastapi-app \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
opentelemetry-instrument uvicorn app:app --host 0.0.0.0 --port 8000

自动检测会为你添加标准的 HTTP 指标(http.server.request.duration 等),无需修改代码。

如果需要添加自定义业务指标,则需要在代码中手动创建 Instrument:

python
from opentelemetry import metrics

meter = metrics.get_meter("myapp.business")
order_counter = meter.create_counter("business.orders.created")

@app.post("/orders")
async def create_order(order: OrderRequest):
    result = await process_order(order)
    order_counter.add(1, {"order.type": order.type, "region": order.region})
    return result

Q7:如何在 Prometheus 中查询 OpenTelemetry 导出的 Histogram 指标?

A:OpenTelemetry Histogram 导出到 Prometheus 后会自动转换为 Prometheus 的 histogram 类型:

promql
# 计算 P50 延迟
histogram_quantile(0.5, rate(http_server_request_duration_bucket[5m]))

# 计算 P99 延迟
histogram_quantile(0.99, rate(http_server_request_duration_bucket[5m]))

# 计算平均延迟
rate(http_server_request_duration_sum[5m]) / rate(http_server_request_duration_count[5m])

# 计算 QPS
rate(http_server_request_duration_count[5m])

# 计算错误率
sum(rate(http_server_request_duration_count{http_response_status_code=~"5.."}[5m]))
/
sum(rate(http_server_request_duration_count[5m]))

Q8:为什么我的 Metrics 在程序重启后从 0 开始了?这不会导致告警误报吗?

A:这是 Cumulative Temporality 的已知特征。

重启前                       重启后
  ┌─────────────────────┐    ┌───────────────────
  │  value: 1000 ────── │ →  │ value: 0 ──────  
  └─────────────────────┘    │ value: 1
                             │ value: 2 ...
                             
Counter 从 0 重新开始,曲线出现"断崖式下跌"。

Prometheus 的处理

  • rate()increase() 函数内置了重置检测。当检测到值变小(counter reset),Prometheus 会自动跳过这个断点,而不是把它算成负增长。
  • 所以生产中用 rate(counter[5m]) 而不是直接用 counter 的绝对值。

OTLP 的处理

  • 使用 Delta Temporality 可以完全避免这个问题,因为每次导出的都是增量值。

Q9:同一个指标名可以创建多次吗?会冲突吗?

A:可以创建多次,只要 nameunitdescription 一致,SDK 会返回同一个 Instrument 实例:

python
# 在文件 A 中
counter_a = meter.create_counter(name="request.count", unit="1", description="Total requests")

# 在文件 B 中
counter_b = meter.create_counter(name="request.count", unit="1", description="Total requests")

# counter_a 和 counter_b 指向同一个底层 Instrument
# 在两个文件中 add() 的值会合并到同一个指标

但如果 name 相同而 unit 或 description 不同,SDK 的行为取决于实现版本——有的会警告,有的会覆盖。最佳实践是统一在一处创建 Instrument,然后传递引用

Q10:Metrics 的数据模型中,什么是 "数据点(Data Point)"?

A

一个 Metric 的数据结构:

Metric
├── name: "http.server.request.count"
├── description: "Total HTTP requests"
├── unit: "1"
└── data: Sum
    ├── temporality: CUMULATIVE
    ├── is_monotonic: true
    └── data_points:
        ├── DataPoint
        │   ├── attributes: {method="GET", status=200}
        │   ├── start_time: 2025-03-15T10:00:00Z
        │   ├── time: 2025-03-15T10:01:00Z
        │   └── value: 1523
        ├── DataPoint
        │   ├── attributes: {method="POST", status=201}
        │   ├── start_time: 2025-03-15T10:00:00Z
        │   ├── time: 2025-03-15T10:01:00Z
        │   └── value: 487
        └── ...

每一组唯一的 attributes 组合对应一个 DataPoint。
DataPoint 是最终导出给后端的最小数据单元。

十、小结与知识图谱

                        Metrics 子系统

          ┌─────────────────┼─────────────────┐
          │                 │                 │
     MeterProvider        Meter           Exporter
     (全局配置)         (创建 Instrument)   (数据导出)
          │                 │                 │
    ┌─────┼─────┐     ┌────┼────────┐    ┌───┼────┐
    │     │     │     │    │    │   │    │   │    │
 Resource Views Reader Cnt UpDn His Gau  OTLP Prom Console
              │                │
         ┌────┼────┐     Observable*
         │         │     (异步版本)
    Temporality  Interval
    (累计/增量)  (导出间隔)

本阶段核心收获

  1. 理解 Metrics 在可观测性中的定位:全局统计视角,支撑告警和趋势分析
  2. 掌握 MeterProvider → Meter → Instrument 的层级关系
  3. 熟练使用 4 种同步 Instrument:Counter、UpDownCounter、Histogram、Gauge
  4. 理解 3 种异步 Instrument 的使用场景和回调机制
  5. 掌握 Views 的自定义聚合能力(桶边界、属性过滤、重命名、丢弃)
  6. 理解 Attributes 与基数控制的重要性,避免基数爆炸
  7. 了解 Temporality(累计 vs 增量)的区别和选择
  8. 能够将 Metrics 与 Prometheus 集成并使用 PromQL 查询
  9. 理解 Semantic Conventions 对指标命名的规范化价值

下一阶段预告:阶段 4 将深入 Logs,学习结构化日志、日志与 Traces 的关联、LogRecord 和 LoggerProvider 的使用。