用 prometheus_client 给 Python 应用暴露指标(4 种 metric 用法)

prometheus_client 是 Prometheus 官方的 Python 库,让任何 Python 应用
能在 /metrics 端点导出 Prometheus 格式的指标。

安装 + 最小可用

uv add prometheus-client
# 暴露在独立端口
from prometheus_client import start_http_server, Counter, Gauge, Histogram

req_count = Counter('http_requests_total', 'Total HTTP requests',
                    ['method', 'path', 'status'])

start_http_server(8001)   # 独立 :8001/metrics
# 然后做你的事 ...
req_count.labels(method='GET', path='/users', status='200').inc()

或挂在 FastAPI 路由:

from prometheus_client import make_asgi_app
app.mount('/metrics', make_asgi_app())

Django:

# urls.py
from prometheus_client import make_wsgi_app
from django.urls import path
from django.views.generic import View

class MetricsView(View):
    def get(self, request):
        ...   # 用 django-prometheus 包更省事

实际项目直接用 django-prometheus

uv add django-prometheus

加 middleware 后内置一堆 Django 指标(视图延迟、SQL 时间、缓存命中等)。

4 种 metric 用法

Counter:单调递增

requests = Counter('requests_total', '...', ['method'])
requests.labels(method='GET').inc()
requests.labels(method='POST').inc(5)

PromQL:

rate(requests_total[5m])    # 每秒请求数(按 5 分钟窗口)

Gauge:可上可下

queue_size = Gauge('queue_size', '...')
queue_size.set(42)
queue_size.inc(); queue_size.dec()

# 用 callback 让 Prometheus 拉取时实时计算
queue_size.set_function(lambda: r.llen('jobs'))

适合:当前队列长度、连接数、温度、内存使用。

Histogram:分布

latency = Histogram('http_latency_seconds', 'HTTP latency',
                    ['endpoint'],
                    buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25,
                             0.5, 1, 2.5, 5, 10])

# 上下文管理器自动测延迟
with latency.labels(endpoint='/users').time():
    do_work()

PromQL 出 p95:

histogram_quantile(0.95,
  sum(rate(http_latency_seconds_bucket[5m])) by (le, endpoint))

bucket 选 5-12 个,覆盖典型延迟范围。bucket 多了占资源,bucket 少了
分位数不准。

Summary:分位数(不推荐)

from prometheus_client import Summary
size = Summary('request_size_bytes', '...')
size.observe(2048)

Summary 在客户端算分位数,无法在多实例上做正确聚合。生产基本只用
Histogram
,需要分位数时用 histogram_quantile() 在服务端算。

label 设计原则

label 的不同值组合决定了 series 的数量。label cardinality 高了
Prometheus 内存爆。

# 错: user_id 是无限基数
requests.labels(user_id=user.id).inc()

# 错: 完整 URL 含动态 ID
requests.labels(path='/users/123/posts').inc()

# 对: 路径模板化
requests.labels(path='/users/{id}/posts').inc()

label 数应该是个有限小集合:endpoint 模板、HTTP method、status code、
某几个固定 region 等。

一个完整中间件(FastAPI)

import time
from prometheus_client import Counter, Histogram
from starlette.middleware.base import BaseHTTPMiddleware

REQ = Counter('http_requests_total', '...',
              ['method', 'path', 'status'])
LATENCY = Histogram('http_latency_seconds', '...',
                    ['method', 'path'],
                    buckets=[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5])

class MetricsMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        t0 = time.perf_counter()
        response = await call_next(request)
        elapsed = time.perf_counter() - t0
        # 用路由模板而不是实际路径
        route = request.scope.get('route')
        path = route.path if route else request.url.path
        REQ.labels(method=request.method, path=path,
                   status=str(response.status_code)).inc()
        LATENCY.labels(method=request.method, path=path).observe(elapsed)
        return response

app.add_middleware(MetricsMiddleware)

request.scope.get('route').path 给出的是 /users/{user_id}/posts
这种模板,不是 /users/42/posts 这种实参。

内置 collector

prometheus_client 自带几个:

from prometheus_client import REGISTRY, GCCollector, PlatformCollector, ProcessCollector

# 默认这些已经注册(CPython 平台 + 进程信息)
# 可以手动反注册节省指标
REGISTRY.unregister(GCCollector(REGISTRY))

process_cpu_seconds_totalprocess_resident_memory_bytes 等都是
免费白送的。

多 worker 的坑

gunicorn / uvicorn 多 worker 时,每个 worker 是独立进程,独立的 metrics。
直接 scrape /metrics 只看到一个 worker 的数据。两种解法:

  1. mmap 共享:设 PROMETHEUS_MULTIPROC_DIR=/tmp/prom,prometheus_client
    会把指标写到共享目录,import 时聚合所有 worker 的数据。
  2. 每个 worker 独立 scrape:让 Prometheus 单独抓每个 worker 端口
    (复杂度高,不推荐)。

mmap 版本写法:

# 进程启动时
import os
os.environ['PROMETHEUS_MULTIPROC_DIR'] = '/tmp/prom'

# 暴露指标时用 MultiProcessCollector
from prometheus_client import multiprocess, CollectorRegistry, generate_latest

registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
output = generate_latest(registry)

worker 退出时调 multiprocess.mark_process_dead(pid)

测试

def test_counter_increments():
    REQ.labels(method='GET', path='/x', status='200').inc()
    # 直接读 metric 内部值
    val = REQ.labels(method='GET', path='/x', status='200')._value.get()
    assert val == 1

踩过的坑

  • 把 user-id / session-id 当 label:1M 用户 = 1M series,Prometheus 崩盘。
    这种"高基数"信息应该用 log,不用 metric。
  • Histogram bucket 改了:旧数据和新数据不可比较,需要双写或重设。建议
    bucket 一开始想清楚。
  • /metrics 端点不要走鉴权:Prometheus scrape 没有简单的 auth;
    挂内网或加 IP 白名单。
  • with 管 Histogram timer:抛异常时也会记录,所以错误请求的延迟
    也算进 p95。如果想只看成功请求的延迟,加 try/except 区分。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。