第 18 章 · Prometheus、OpenTelemetry 与 APM
本章目标:使用 prometheus-fastapi-instrumentator 在 svc-demo 暴露 /metrics(请求计数、延迟直方图);理解 RED 指标与 Grafana 告警概念;入门 OpenTelemetry FastAPI instrumentation,让 trace_id 贯穿 ORM、Redis 与后台任务;将结构化日志与 metrics 通过 request_id 关联(衔接 ch08);了解 APM(Datadog / New Relic / SkyWalking 类工具)概念;建立生产可观测性 checklist。
学时建议:5~6 小时(含 2 小时 Prometheus + Grafana 跟练)
前置:本模块 ch08 请求 ID 与结构化日志;ch10 Celery 异步;ch11 Redis 限流;ch17 压测基线。监控概念可对照 ops-deploy 与 xiaozi-cloud 告警章节。
18.1 可观测性三大支柱
Metrics Logs Traces
Prometheus/Grafana JSON → ELK/Loki OpenTelemetry → Jaeger
「系统是否异常?」 「发生了什么?」 「慢在哪里?」
| 支柱 | svc-demo 示例 |
|---|---|
| Metrics | /metrics 暴露 http_requests_total、http_request_duration_seconds |
| Logs | request_id=abc123 关联访问与审计日志 |
| Traces | trace_id 串联 ORM 查询、Redis 缓存与 Celery 任务 |
统一使用虚构 svc-demo、api.example.com;严禁写入真实监控地址、API Key 或租户 ID。
RED 方法(请求型 API 服务):
| 字母 | 含义 | Prometheus 示例 |
|---|---|---|
| R | Rate 每秒请求 | rate(http_requests_total[5m]) |
| E | Errors 错误率 | rate(http_requests_total{status=~"5.."}[5m]) |
| D | Duration 延迟分布 | histogram_quantile(0.95, ...) |
18.2 prometheus-fastapi-instrumentator
安装:
pip install prometheus-fastapi-instrumentator
svc_demo/main.py:
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI(title="svc-demo", version="1.0.0")
@app.on_event("startup")
async def startup():
Instrumentator(
should_group_status_codes=True,
should_ignore_untemplated=True,
excluded_handlers=["/metrics", "/health", "/docs", "/openapi.json"],
).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
默认采集:
| 指标 | 说明 |
|---|---|
http_requests_total | 按 method、handler、status 计数 |
http_request_duration_seconds | 延迟直方图 |
http_requests_in_progress | 进行中请求(若启用) |
自定义业务指标
from prometheus_client import Counter, Histogram
ORDER_CREATED = Counter(
"svc_demo_orders_created_total",
"成功创建订单数",
["tenant"],
)
DB_QUERY_LATENCY = Histogram(
"svc_demo_db_query_seconds",
"数据库查询耗时",
["operation"],
)
# 业务代码中
ORDER_CREATED.labels(tenant="demo").inc()
with DB_QUERY_LATENCY.labels(operation="list_products").time():
await db.execute(stmt)
/metrics 安全
| 实践 | 原因 |
|---|---|
| 不对外网暴露 | 可能泄露内部路由结构 |
| Nginx IP 白名单 | 仅 Prometheus 可抓 |
include_in_schema=False | 不出现在 /docs |
| 独立端口(选修) | :9090/metrics sidecar |
docker-compose.prod.yml 中 Prometheus 抓取:
scrape_configs:
- job_name: svc-demo
metrics_path: /metrics
static_configs:
- targets: ["api:8000"]
18.3 Grafana 与告警(概念)
Grafana 连接 Prometheus 数据源,常用面板:
| 面板 | PromQL 思路 |
|---|---|
| QPS | sum(rate(http_requests_total{job="svc-demo"}[5m])) |
| 错误率 | 5xx / total |
| p95 延迟 | histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) |
| 饱和度 | 进程 CPU、连接池(自定义 exporter) |
告警规则示例(概念 YAML):
groups:
- name: svc-demo
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.05
for: 5m
labels: {severity: warning}
annotations:
summary: "svc-demo 5xx 错误率 > 5%"
- alert: HighLatencyP95
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) > 1.0
for: 10m
labels: {severity: warning}
告警通知:邮件、钉钉、PagerDuty 等;staging 先验证,避免告警风暴。
18.4 OpenTelemetry 链路追踪
OpenTelemetry(OTel)提供 vendor-neutral 的 Traces / Metrics / Logs API。
安装:
pip install opentelemetry-api opentelemetry-sdk \
opentelemetry-instrumentation-fastapi \
opentelemetry-instrumentation-sqlalchemy \
opentelemetry-instrumentation-httpx \
opentelemetry-exporter-otlp
svc_demo/core/telemetry.py:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
def setup_telemetry(app, engine):
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="otel-collector.example.com:4317", insecure=True)
)
)
trace.set_tracer_provider(provider)
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine.sync_engine)
main.py 启动时调用 setup_telemetry(app, engine)。
trace_id 与 request_id 关联
ch08 中间件已注入 request_id;在 span 中写入:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@app.middleware("http")
async def bind_trace_context(request: Request, call_next):
span = trace.get_current_span()
request_id = request.state.request_id # ch08 中间件设置
span.set_attribute("request.id", request_id)
response = await call_next(request)
response.headers["X-Trace-Id"] = format(span.get_span_context().trace_id, "032x")
return response
结构化日志(structlog / logging JSON):
logger.info(
"product_created",
request_id=request_id,
trace_id=format(trace_id, "032x"),
product_id=product.id,
)
在 Jaeger UI 搜索 trace_id,可看到:HTTP → SQLAlchemy → Redis 全链路。