{T}

监控与告警实战

没有监控的系统就是黑箱——出问题时你只能猜。 监控与告警是生产环境的"眼睛"和"耳朵",让你在用户投诉之前就知道哪里出了问题。

阅读提示

监控体系全景

现代可观测性(Observability)由三大支柱构成:指标(Metrics)、日志(Logs)、链路追踪(Traces)。三者协同才能还原系统的完整运行状态。

图表渲染中…

三大支柱对比

维度指标 Metrics日志 Logs链路追踪 Traces
数据特征数值型,可聚合文本型,离散事件有向无环图(DAG)
数据量小(KB 级/秒)中(MB 级/秒)大(MB~GB 级/秒)
典型用途告警、容量规划故障排查、审计性能瓶颈定位
存储成本
采样策略全量采集可按级别过滤通常需采样(1%~100%)
典型工具PrometheusELK / LokiJaeger / Tempo
关键问题"系统现在健康吗?""刚才发生了什么?""请求为什么慢?"

监控层次模型

图表渲染中…

Google SRE 提出的四大黄金指标:延迟(Latency)、流量(Traffic)、错误(Errors)、饱和度(Saturation),是监控体系的北极星。

Prometheus + Python 应用指标采集

Prometheus 是云原生监控的事实标准。Python 通过 prometheus_client 库暴露指标,Prometheus Server 主动拉取(Pull 模式)。

核心概念

图表渲染中…

安装与基础配置

bash
pip install prometheus-client

四种指标类型

指标类型用途典型场景示例
Counter只增不减的计数器请求总数、错误总数http_requests_total
Gauge可增可减的仪表盘当前连接数、温度active_connections
Histogram观测值分布直方图请求延迟、响应大小http_request_duration_seconds
Summary分位数统计请求延迟(客户端计算)rpc_duration_seconds

Histogram 与 Summary 的关键区别:Histogram 在服务端通过 PromQL 计算分位数,Summary 在客户端计算。生产环境推荐 Histogram,因为可以聚合。

Counter 计数器

python
from prometheus_client import Counter, start_http_server

# 定义计数器
REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']  # 标签(维度)
)

# 在业务代码中递增
def handle_request(method: str, endpoint: str, status: int):
    REQUEST_COUNT.labels(
        method=method,
        endpoint=endpoint,
        status=str(status)
    ).inc()

# 批量递增
def handle_batch_errors(count: int):
    REQUEST_COUNT.labels(
        method='POST',
        endpoint='/api/data',
        status='500'
    ).inc(count)

Gauge 仪表盘

python
from prometheus_client import Gauge
import threading

# 当前活跃连接数
ACTIVE_CONNECTIONS = Gauge(
    'active_connections',
    'Number of active connections'
)

# 可以作为上下文管理器使用
def process_connection():
    with ACTIVE_CONNECTIONS.track_inprogress():
        # 连接处理逻辑
        handle_connection()
    # 退出 with 块后自动减 1

# 也可以直接设置值
def update_temperature(temp: float):
    TEMPERATURE = Gauge('room_temperature_celsius', 'Room temperature')
    TEMPERATURE.set(temp)

# 设置为当前函数返回值
ACTIVE_THREADS = Gauge('active_threads', 'Number of active threads')
ACTIVE_THREADS.set_function(lambda: threading.active_count())

Histogram 直方图

python
from prometheus_client import Histogram
import time

# 自定义桶边界(秒),根据业务 SLA 调整
REQUEST_DURATION = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration in seconds',
    ['method', 'endpoint'],
    buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)

# 计时装饰器
def monitor_duration(method: str, endpoint: str):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            try:
                return func(*args, **kwargs)
            finally:
                duration = time.perf_counter() - start
                REQUEST_DURATION.labels(
                    method=method,
                    endpoint=endpoint
                ).observe(duration)
        return wrapper
    return decorator

# 使用装饰器
@monitor_duration('GET', '/api/users')
def get_users():
    time.sleep(0.05)
    return [{'id': 1, 'name': 'Alice'}]

# 也可以用上下文管理器
def process_task():
    with REQUEST_DURATION.labels('POST', '/api/tasks').time():
        do_heavy_work()

完整示例:独立指标服务

python
"""metrics_server.py — 独立指标服务"""
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
import random
import threading

# ===== 定义指标 =====

REQUEST_COUNT = Counter(
    'app_http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

REQUEST_DURATION = Histogram(
    'app_http_request_duration_seconds',
    'HTTP request duration',
    ['method', 'endpoint'],
    buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5]
)

ACTIVE_REQUESTS = Gauge(
    'app_http_requests_in_progress',
    'Requests currently in progress',
    ['endpoint']
)

DB_POOL_SIZE = Gauge(
    'app_db_pool_size',
    'Database connection pool size'
)

DB_POOL_AVAILABLE = Gauge(
    'app_db_pool_available',
    'Available database connections'
)

# ===== 模拟业务流量 =====

def simulate_requests():
    """模拟业务请求"""
    endpoints = ['/api/users', '/api/orders', '/api/products']
    methods = ['GET', 'POST', 'PUT', 'DELETE']
    statuses = [200, 200, 200, 200, 201, 400, 404, 500]

    while True:
        endpoint = random.choice(endpoints)
        method = random.choice(methods)
        status = random.choice(statuses)

        with ACTIVE_REQUESTS.labels(endpoint=endpoint).track_inprogress():
            duration = random.expovariate(10)  # 模拟延迟
            time.sleep(min(duration, 2.0))

        REQUEST_COUNT.labels(
            method=method, endpoint=endpoint, status=str(status)
        ).inc()
        REQUEST_DURATION.labels(
            method=method, endpoint=endpoint
        ).observe(duration)

def simulate_db_pool():
    """模拟数据库连接池"""
    total = 20
    while True:
        available = random.randint(0, total)
        DB_POOL_SIZE.set(total)
        DB_POOL_AVAILABLE.set(available)
        time.sleep(1)

if __name__ == '__main__':
    # 在 8000 端口启动指标服务
    start_http_server(8000)
    print("Metrics server started at http://localhost:8000/metrics")

    # 启动模拟线程
    threading.Thread(target=simulate_requests, daemon=True).start()
    threading.Thread(target=simulate_db_pool, daemon=True).start()

    # 主线程保持运行
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("Shutting down...")

与 FastAPI 集成

python
"""app/metrics.py — FastAPI 指标中间件"""
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, generate_latest
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import time

# 定义指标
REQUEST_COUNT = Counter(
    'fastapi_requests_total',
    'Total request count',
    ['method', 'endpoint', 'status_code']
)

REQUEST_DURATION = Histogram(
    'fastapi_request_duration_seconds',
    'Request duration',
    ['method', 'endpoint'],
    buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

REQUESTS_IN_PROGRESS = Gauge(
    'fastapi_requests_in_progress',
    'Requests in progress',
    ['method', 'endpoint']
)


class PrometheusMiddleware(BaseHTTPMiddleware):
    """自动采集每个请求的指标"""

    async def dispatch(self, request: Request, call_next):
        method = request.method
        # 规范化路径,避免高基数问题
        endpoint = self._normalize_path(request.url.path)

        REQUESTS_IN_PROGRESS.labels(method=method, endpoint=endpoint).inc()

        start_time = time.perf_counter()
        try:
            response = await call_next(request)
            status_code = str(response.status_code)
        except Exception:
            status_code = '500'
            raise
        finally:
            duration = time.perf_counter() - start_time
            REQUESTS_IN_PROGRESS.labels(method=method, endpoint=endpoint).dec()
            REQUEST_COUNT.labels(
                method=method, endpoint=endpoint, status_code=status_code
            ).inc()
            REQUEST_DURATION.labels(
                method=method, endpoint=endpoint
            ).observe(duration)

        return response

    @staticmethod
    def _normalize_path(path: str) -> str:
        """将路径中的动态参数替换为占位符,避免高基数"""
        parts = path.strip('/').split('/')
        normalized = []
        for part in parts:
            # 如果段看起来像 ID(纯数字或 UUID),替换为占位符
            if part.isdigit() or '-' in part and len(part) > 20:
                normalized.append('{id}')
            else:
                normalized.append(part)
        return '/' + '/'.join(normalized)


def metrics_endpoint():
    """暴露 /metrics 端点"""
    return Response(
        content=generate_latest(REGISTRY),
        media_type='text/plain; version=0.0.4; charset=utf-8'
    )
python
"""main.py — FastAPI 应用注册"""
from fastapi import FastAPI
from app.metrics import PrometheusMiddleware, metrics_endpoint

app = FastAPI(title="监控示例应用")

# 注册中间件
app.add_middleware(PrometheusMiddleware)

# 注册指标端点
app.add_route('/metrics', metrics_endpoint, methods=['GET'])


@app.get('/api/users/{user_id}')
async def get_user(user_id: int):
    return {'id': user_id, 'name': 'Alice'}


@app.get('/api/orders')
async def list_orders():
    return [{'id': 1, 'amount': 99.9}]

高基数标签陷阱

python
# 错误:用户 ID 作为标签 → 标签值爆炸
REQUEST_COUNT.labels(user_id=str(user_id)).inc()  # 千万不要这样做!

# 正确:只使用低基数的标签
REQUEST_COUNT.labels(method='GET', endpoint='/api/users', status='200').inc()

# 标签基数参考
# ✅ 低基数(< 100):HTTP 方法、状态码、端点路径、环境
# ⚠️ 中基数(100~10k):数据中心、版本号
# ❌ 高基数(> 10k):用户 ID、请求 ID、邮箱地址

Grafana 仪表盘配置

Grafana 将 Prometheus 采集的指标可视化,是监控的"仪表盘"。

关键 PromQL 查询

指标PromQL说明
请求速率rate(http_requests_total[5m])每秒请求数
错误率rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])5xx 比例
P50 延迟histogram_quantile(0.5, rate(http_request_duration_seconds_bucket[5m]))中位数延迟
P99 延迟histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))尾部延迟
可用连接app_db_pool_available / app_db_pool_size连接池利用率

Python 应用关键指标仪表盘

yaml
# grafana/dashboards/python-app.json — 核心面板配置(简化版)
# 在 Grafana UI 中导入或通过 provisioning 配置

dashboard:
  title: Python 应用监控
  panels:
    - title: 请求速率 (QPS)
      type: timeseries
      targets:
        - expr: sum(rate(fastapi_requests_total[5m])) by (endpoint)
          legendFormat: "{{endpoint}}"

    - title: 错误率
      type: timeseries
      targets:
        - expr: |
            sum(rate(fastapi_requests_total{status_code=~"5.."}[5m])) by (endpoint)
            /
            sum(rate(fastapi_requests_total[5m])) by (endpoint)
          legendFormat: "{{endpoint}}"

    - title: 请求延迟 (P50/P95/P99)
      type: timeseries
      targets:
        - expr: |
            histogram_quantile(0.5,
              sum(rate(fastapi_request_duration_seconds_bucket[5m])) by (le, endpoint)
            )
          legendFormat: "P50 - {{endpoint}}"
        - expr: |
            histogram_quantile(0.95,
              sum(rate(fastapi_request_duration_seconds_bucket[5m])) by (le, endpoint)
            )
          legendFormat: "P95 - {{endpoint}}"
        - expr: |
            histogram_quantile(0.99,
              sum(rate(fastapi_request_duration_seconds_bucket[5m])) by (le, endpoint)
            )
          legendFormat: "P99 - {{endpoint}}"

    - title: 在线请求数
      type: gauge
      targets:
        - expr: sum(fastapi_requests_in_progress)
          legendFormat: "In Progress"

    - title: 连接池利用率
      type: gauge
      targets:
        - expr: app_db_pool_available / app_db_pool_size
          legendFormat: "可用率"
      thresholds:
        - value: 0.2
          color: red      # 低于 20% 可用,危险
        - value: 0.5
          color: yellow   # 低于 50% 可用,警告
        - value: 1.0
          color: green    # 正常

Grafana Provisioning 自动化

yaml
# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false
yaml
# grafana/provisioning/dashboards/dashboards.yml
apiVersion: 1
providers:
  - name: Python App
    orgId: 1
    folder: ''
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: false

Sentry 异常追踪

Sentry 是实时异常追踪平台,捕获未处理异常、记录上下文信息,帮助快速定位问题。

工作流程

图表渲染中…

基础集成

bash
pip install sentry-sdk
python
"""sentry_config.py — Sentry 初始化"""
import sentry_sdk

def init_sentry(dsn: str, environment: str = "production", release: str = ""):
    """初始化 Sentry SDK"""
    sentry_sdk.init(
        dsn=dsn,
        environment=environment,
        release=release,              # 关联代码版本
        traces_sample_rate=0.1,       # 性能追踪采样率 10%
        profiles_sample_rate=0.1,     # 性能分析采样率 10%
        send_default_pii=False,       # 不发送个人身份信息
        max_breadcrumbs=50,           # Breadcrumb 数量
        attach_stacktrace=True,       # 附加堆栈到非异常日志
    )

FastAPI 集成

python
"""FastAPI + Sentry"""
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
    integrations=[FastApiIntegration()],
    traces_sample_rate=0.1,
    environment="production",
    release="my-app@1.0.0",
)

app = FastAPI()

# 全局异常处理,确保未捕获异常上报 Sentry
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    # Sentry 自动捕获未处理异常,这里也可以手动上报
    sentry_sdk.capture_exception(exc)
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal Server Error"}
    )

@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    if user_id > 1000:
        raise ValueError(f"Invalid user_id: {user_id}")
    return {"id": user_id, "name": "Alice"}

Flask 集成

python
"""Flask + Sentry"""
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from flask import Flask, jsonify

sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
    integrations=[FlaskIntegration()],
    traces_sample_rate=0.1,
)

app = Flask(__name__)

@app.errorhandler(Exception)
def handle_exception(exc):
    sentry_sdk.capture_exception(exc)
    return jsonify({"error": "Internal Server Error"}), 500

@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
    order = find_order(order_id)
    if not order:
        raise LookupError(f"Order {order_id} not found")
    return jsonify(order)

自定义 Context

python
"""自定义 Sentry 上下文信息"""
import sentry_sdk

def set_user_context(user_id: int, username: str, email: str):
    """设置用户上下文"""
    sentry_sdk.set_user({
        "id": str(user_id),
        "username": username,
        "email": email,
        # 不要放密码等敏感信息!
    })

def set_request_context(request_id: str, tenant_id: str):
    """设置标签和额外信息"""
    # 标签:可搜索、可过滤
    sentry_sdk.set_tag("request_id", request_id)
    sentry_sdk.set_tag("tenant_id", tenant_id)
    sentry_sdk.set_tag("service", "user-api")

    # 额外信息:不可搜索,但有助于排查
    sentry_sdk.set_context("request_info", {
        "request_id": request_id,
        "tenant_id": tenant_id,
        "region": "cn-east-1",
    })

def add_breadcrumb(action: str, category: str, data: dict | None = None):
    """添加 Breadcrumb(面包屑),记录异常发生前的操作路径"""
    sentry_sdk.add_breadcrumb(
        category=category,
        message=action,
        data=data or {},
        level="info",
    )

# 使用示例
@app.post("/api/checkout")
async def checkout(request: Request):
    user = get_current_user()
    set_user_context(user.id, user.name, user.email)

    add_breadcrumb("开始结账流程", "checkout", {"cart_items": 3})

    order = create_order(user)
    add_breadcrumb("订单创建成功", "checkout", {"order_id": order.id})

    payment = process_payment(order)
    if payment.failed:
        # 手动上报业务异常
        sentry_sdk.capture_message(
            f"支付失败: order={order.id}, reason={payment.error}",
            level="warning"
        )

    return {"order_id": order.id, "status": "paid"}

采样策略

python
"""Sentry 采样策略 — 控制数据量和成本"""
import sentry_sdk

def traces_sampler(sampling_context):
    """自定义性能追踪采样"""
    op = sampling_context.get("parent_sampled")
    transaction_name = sampling_context.get("transaction_context", {}).get("name", "")

    # 健康检查不采样
    if transaction_name in ("/health", "/ready", "/metrics"):
        return 0.0

    # 关键业务接口全量采样
    if transaction_name.startswith("/api/payment"):
        return 1.0

    # 其他接口 10% 采样
    return 0.1

sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
    traces_sampler=traces_sampler,
)

结构化日志与日志聚合

传统 print 和裸 logging 无法被机器解析。结构化日志以 JSON 格式输出,方便日志系统索引和检索。

structlog 结构化日志

bash
pip install structlog
python
"""logging_config.py — 结构化日志配置"""
import logging
import structlog
from typing import Any


def add_app_context(
    logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
) -> dict[str, Any]:
    """自定义处理器:添加应用上下文"""
    event_dict["app"] = "user-api"
    event_dict["version"] = "1.0.0"
    return event_dict


def configure_logging(log_level: str = "INFO", json_logs: bool = True):
    """配置结构化日志"""
    shared_processors: list[Any] = [
        structlog.contextvars.merge_contextvars,     # 合并上下文变量
        structlog.stdlib.add_logger_name,             # 添加 logger 名称
        structlog.stdlib.add_log_level,               # 添加日志级别
        structlog.stdlib.PositionalArgumentsFormatter(), # 位置参数格式化
        structlog.processors.TimeStamper(fmt="iso"),  # ISO 8601 时间戳
        structlog.processors.StackInfoRenderer(),     # 堆栈信息
        structlog.processors.UnicodeDecoder(),        # Unicode 解码
        add_app_context,                              # 自定义上下文
    ]

    if json_logs:
        # JSON 格式输出(生产环境)
        renderer = structlog.processors.JSONRenderer()
    else:
        # 控制台彩色输出(开发环境)
        renderer = structlog.dev.ConsoleRenderer()

    structlog.configure(
        processors=[
            *shared_processors,
            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
        ],
        context_class=dict,
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )

    # 配置标准库 logging 的格式化器
    formatter = structlog.stdlib.ProcessorFormatter(
        processors=[
            structlog.stdlib.ProcessorFormatter.remove_processors_meta,
            renderer,
        ],
        foreign_pre_chain=shared_processors,
    )

    handler = logging.StreamHandler()
    handler.setFormatter(formatter)

    root_logger = logging.getLogger()
    root_logger.handlers = [handler]
    root_logger.setLevel(log_level.upper())

    # 降低第三方库日志级别
    logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)

使用示例

python
"""业务代码中的结构化日志"""
import structlog

logger = structlog.get_logger()


def process_order(order_id: int, user_id: int):
    # 绑定上下文,后续日志自动携带
    log = logger.bind(order_id=order_id, user_id=user_id)

    log.info("order_processing_started")

    try:
        result = validate_order(order_id)
        log.info("order_validated", item_count=result.item_count)
    except ValueError as e:
        log.error("order_validation_failed", error=str(e), error_type=type(e).__name__)
        raise

    try:
        payment = charge_payment(order_id)
        log.info("payment_completed", amount=payment.amount, method=payment.method)
    except PaymentError as e:
        log.error("payment_failed", error=str(e))
        raise

输出示例(JSON 格式):

json
{
  "event": "order_validated",
  "order_id": 12345,
  "user_id": 678,
  "item_count": 3,
  "logger": "app.orders",
  "level": "info",
  "timestamp": "2026-06-06T10:30:00.123456Z",
  "app": "user-api",
  "version": "1.0.0"
}

上下文变量(Context Variables)

python
"""请求级上下文传递 — 不用层层传参"""
import structlog
from contextvars import ContextVar

# 定义上下文变量
request_id_var: ContextVar[str] = ContextVar("request_id", default="")
tenant_id_var: ContextVar[str] = ContextVar("tenant_id", default="")


def context_injector(
    logger, method_name: str, event_dict: dict
) -> dict:
    """自动注入上下文变量到每条日志"""
    request_id = request_id_var.get("")
    tenant_id = tenant_id_var.get("")
    if request_id:
        event_dict["request_id"] = request_id
    if tenant_id:
        event_dict["tenant_id"] = tenant_id
    return event_dict


# FastAPI 中间件设置上下文
from fastapi import Request
import uuid

@app.middleware("http")
async def context_middleware(request: Request, call_next):
    request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
    tenant_id = request.headers.get("X-Tenant-ID", "default")

    request_id_var.set(request_id)
    tenant_id_var.set(tenant_id)

    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

ELK vs Loki 对比

维度ELK (Elasticsearch + Logstash + Kibana)Loki + Grafana
架构重,三组件独立部署轻,与 Prometheus/Grafana 统一
索引全文索引,资源消耗大仅索引标签,日志正文不索引
查询Lucene 语法,功能强大LogQL,与 PromQL 类似
存储需要大量磁盘压缩存储,资源消耗低
适用规模大规模、需要全文搜索中小规模、与指标统一查看
学习成本
推荐场景日志分析复杂、需要聚合已有 Prometheus/Grafana 体系

Loki 日志配置

yaml
# promtail/config.yml — 日志采集代理配置
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: python-app
    static_configs:
      - targets:
          - localhost
        labels:
          job: python-app
          environment: production
          __path__: /var/log/python-app/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
            logger: logger
            request_id: request_id
            tenant_id: tenant_id
      - labels:
          level:
          logger:
          tenant_id:
      - timestamp:
          source: timestamp
          format: RFC3339Nano

实战场景

场景一:FastAPI 应用的完整监控

将 Prometheus 指标、Sentry 异常追踪、结构化日志整合到一起。

图表渲染中…
python
"""app/monitoring.py — 统一监控配置"""
import time
import uuid
import structlog
import sentry_sdk
from contextvars import ContextVar
from sentry_sdk.integrations.fastapi import FastApiIntegration
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, generate_latest
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

# ===== 上下文变量 =====
request_id_var: ContextVar[str] = ContextVar("request_id", default="")

# ===== Prometheus 指标 =====
REQUEST_COUNT = Counter(
    'app_requests_total',
    'Total requests',
    ['method', 'endpoint', 'status']
)

REQUEST_DURATION = Histogram(
    'app_request_duration_seconds',
    'Request duration',
    ['method', 'endpoint'],
    buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

ACTIVE_REQUESTS = Gauge(
    'app_active_requests',
    'Currently processing requests'
)

DB_QUERY_DURATION = Histogram(
    'app_db_query_duration_seconds',
    'Database query duration',
    ['operation'],
    buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5]
)

# ===== 结构化日志 =====
logger = structlog.get_logger()


# ===== 监控中间件 =====
class MonitoringMiddleware(BaseHTTPMiddleware):
    """统一监控中间件:采集指标 + 设置上下文"""

    async def dispatch(self, request: Request, call_next):
        # 设置请求上下文
        request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
        request_id_var.set(request_id)

        method = request.method
        endpoint = self._normalize_path(request.url.path)

        log = logger.bind(
            request_id=request_id,
            method=method,
            endpoint=endpoint,
        )

        log.info("request_started")
        ACTIVE_REQUESTS.inc()

        start = time.perf_counter()
        try:
            response = await call_next(request)
            status = str(response.status_code)
            log.info("request_completed", status_code=status)
        except Exception as e:
            status = "500"
            log.error("request_failed", error=str(e), error_type=type(e).__name__)
            # Sentry 自动捕获,这里显式上报确保不遗漏
            sentry_sdk.capture_exception(e)
            raise
        finally:
            duration = time.perf_counter() - start
            ACTIVE_REQUESTS.dec()
            REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc()
            REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration)
            log.info("request_metrics", duration_ms=round(duration * 1000, 2))

        return response

    @staticmethod
    def _normalize_path(path: str) -> str:
        parts = path.strip('/').split('/')
        normalized = []
        for part in parts:
            if part.isdigit() or (len(part) > 20 and '-' in part):
                normalized.append('{id}')
            else:
                normalized.append(part)
        return '/' + '/'.join(normalized)


# ===== 应用初始化 =====
def setup_monitoring(app: FastAPI, sentry_dsn: str = "", env: str = "production"):
    """统一初始化监控组件"""
    # Sentry
    if sentry_dsn:
        sentry_sdk.init(
            dsn=sentry_dsn,
            integrations=[FastApiIntegration()],
            traces_sample_rate=0.1,
            environment=env,
        )

    # 中间件
    app.add_middleware(MonitoringMiddleware)

    # 指标端点
    def metrics():
        return Response(
            content=generate_latest(REGISTRY),
            media_type="text/plain; version=0.0.4; charset=utf-8",
        )

    app.add_route("/metrics", metrics, methods=["GET"])

    # 全局异常处理
    @app.exception_handler(Exception)
    async def handle_exception(request: Request, exc: Exception):
        logger.error(
            "unhandled_exception",
            error=str(exc),
            error_type=type(e).__name__,
            path=request.url.path,
        )
        sentry_sdk.capture_exception(exc)
        return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})

    logger.info("monitoring_initialized", environment=env)
python
"""main.py — 完整监控的 FastAPI 应用"""
from fastapi import FastAPI
from app.monitoring import setup_monitoring, DB_QUERY_DURATION, logger
import time
import os

app = FastAPI(title="完整监控示例")

# 初始化监控
setup_monitoring(
    app,
    sentry_dsn=os.getenv("SENTRY_DSN", ""),
    env=os.getenv("ENVIRONMENT", "development"),
)


@app.get("/health")
async def health():
    return {"status": "ok"}


@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    with DB_QUERY_DURATION.labels(operation="select_user").time():
        user = await fetch_user_from_db(user_id)

    if not user:
        logger.warning("user_not_found", user_id=user_id)
        return {"error": "User not found"}, 404

    return user


@app.post("/api/orders")
async def create_order():
    with DB_QUERY_DURATION.labels(operation="insert_order").time():
        order = await insert_order_to_db()

    logger.info("order_created", order_id=order.id)
    return order

场景二:自定义告警规则

Prometheus Alertmanager 配置告警规则和通知路由。

图表渲染中…
yaml
# prometheus/alert_rules.yml — 告警规则
groups:
  - name: python_app_alerts
    rules:
      # ===== 错误率告警 =====
      - alert: HighErrorRate
        expr: |
          sum(rate(app_requests_total{status=~"5.."}[5m]))
          /
          sum(rate(app_requests_total[5m]))
          > 0.05
        for: 2m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "HTTP 5xx 错误率超过 5%"
          description: |
            当前 5xx 错误率: {{ $value | printf "%.2f" }}%
            持续时间: 2 分钟
            请立即排查应用日志

      # ===== 高延迟告警 =====
      - alert: HighLatencyP99
        expr: |
          histogram_quantile(0.99,
            sum(rate(app_request_duration_seconds_bucket[5m])) by (le, endpoint)
          ) > 2.0
        for: 5m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "P99 延迟超过 2 秒"
          description: |
            端点: {{ $labels.endpoint }}
            P99 延迟: {{ $value | printf "%.2f" }}s

      # ===== 连接池耗尽告警 =====
      - alert: DBPoolExhaustion
        expr: app_db_pool_available / app_db_pool_size < 0.1
        for: 1m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "数据库连接池即将耗尽"
          description: |
            可用连接: {{ $value | printf "%.1f" }}%
            请检查是否有连接泄漏

      # ===== 服务不可用 =====
      - alert: ServiceDown
        expr: up{job="python-app"} == 0
        for: 1m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "Python 应用实例下线"
          description: "实例 {{ $labels.instance }} 已下线超过 1 分钟"

      # ===== 内存使用率 =====
      - alert: HighMemoryUsage
        expr: process_resident_memory_bytes / (1024 * 1024) > 1024
        for: 10m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "Python 进程内存超过 1GB"
          description: |
            当前内存: {{ $value | printf "%.0f" }}MB
            可能存在内存泄漏

      # ===== 请求激增 =====
      - alert: RequestSpike
        expr: |
          sum(rate(app_requests_total[1m]))
          /
          sum(rate(app_requests_total[5m] offset 1h))
          > 3
        for: 2m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "请求量突增 3 倍"
          description: "当前 QPS 是 1 小时前的 3 倍以上,请关注容量"
yaml
# alertmanager/alertmanager.yml — 告警路由与通知
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'severity']
  group_wait: 30s       # 同组告警等待 30s 合并
  group_interval: 5m    # 同组新告警间隔 5m
  repeat_interval: 4h   # 重复告警间隔 4h
  receiver: 'default'

  routes:
    # 严重告警 → 钉钉 + 电话
    - match:
        severity: critical
      receiver: 'critical'
      repeat_interval: 30m

    # 警告 → 钉钉
    - match:
        severity: warning
      receiver: 'warning'
      repeat_interval: 2h

# 抑制规则:服务下线时抑制其他告警
inhibit_rules:
  - source_match:
      alertname: ServiceDown
    target_match:
      severity: warning
    equal: ['instance']

receivers:
  - name: 'default'
    webhook_configs:
      - url: 'http://alertmanager-webhook:8080/notify'

  - name: 'critical'
    webhook_configs:
      - url: 'http://alertmanager-webhook:8080/notify/critical'

  - name: 'warning'
    webhook_configs:
      - url: 'http://alertmanager-webhook:8080/notify/warning'

Docker Compose 完整监控栈

yaml
# docker-compose.monitoring.yml
version: "3.8"

services:
  # ===== 应用 =====
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SENTRY_DSN=${SENTRY_DSN}
      - ENVIRONMENT=production
    networks:
      - monitoring

  # ===== Prometheus =====
  prometheus:
    image: prom/prometheus:v2.51.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
    networks:
      - monitoring

  # ===== Alertmanager =====
  alertmanager:
    image: prom/alertmanager:v0.27.0
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
    networks:
      - monitoring

  # ===== Grafana =====
  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - grafana_data:/var/lib/grafana
    networks:
      - monitoring

  # ===== Loki =====
  loki:
    image: grafana/loki:2.9.0
    ports:
      - "3100:3100"
    volumes:
      - loki_data:/loki
    networks:
      - monitoring

  # ===== Promtail =====
  promtail:
    image: grafana/promtail:2.9.0
    volumes:
      - ./promtail/config.yml:/etc/promtail/config.yml
      - /var/log:/var/log:ro
    networks:
      - monitoring

volumes:
  prometheus_data:
  grafana_data:
  loki_data:

networks:
  monitoring:
    driver: bridge
yaml
# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

rule_files:
  - alert_rules.yml

scrape_configs:
  - job_name: 'python-app'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['app:8000']
        labels:
          service: 'user-api'

常见陷阱

陷阱说明后果正确做法
高基数标签将用户 ID、请求 ID 等高基数值作为 Prometheus 标签TSDB 膨胀、查询超时、OOM标签基数控制在 100 以内,高基数值放日志
忽略 /health 端点/health 和 /metrics 也被 Prometheus 拉取,但不记录指标指标数据包含大量无意义的健康检查在中间件中过滤 /health/ready/metrics 路径
Sentry 不设采样所有请求全量上报性能数据Sentry 配额迅速耗尽、成本失控按 API 重要性设置采样率,健康检查采样率为 0
日志无结构使用 print() 或裸 logging 输出纯文本无法被日志系统索引和检索使用 structlog 输出 JSON 格式
日志级别滥用大量 INFO 日志淹没重要信息告警疲劳、关键信息丢失INFO 记录关键业务事件,DEBUG 记录调试信息
忘记清理 Sentry Context请求结束后未清理 set_user() 等上下文下一个请求可能携带上一个用户的上下文使用 Sentry SDK 的 scope 机制,自动按请求隔离
Histogram 桶不合理使用默认桶边界,无法区分业务 SLAP99 计算不准确根据业务 SLA 自定义桶边界
只监控应用层只关注 QPS、延迟,忽略基础设施磁盘满、内存泄漏等基础问题遗漏同时监控 CPU、内存、磁盘、网络
告警过多任何小波动都触发告警告警疲劳,团队开始忽略告警设置合理的 for 持续时间,分级告警
Prometheus 无持久化Docker 部署未挂载存储卷容器重启后历史数据丢失挂载 volume 到 /prometheus
不关联 ReleaseSentry 未设置 release 参数无法区分哪个版本引入了问题始终设置 release,与 CI/CD 版本号一致

最佳实践速查表

指标设计

实践说明
使用四大黄金指标延迟、流量、错误、饱和度全覆盖
Histogram 优于 SummaryHistogram 可在服务端聚合,Summary 不行
自定义桶边界根据 SLA 设置,如 P99 < 500ms 则桶边界应包含 0.5
标签基数 < 100避免高基数标签导致 TSDB 膨胀
路径规范化/api/users/123 归一化为 /api/users/{id}

告警设计

实践说明
告警分级Critical(立即响应)、Warning(工作时间处理)、Info(仅记录)
设置 for 持续时间避免瞬时波动触发告警,至少 1~2 分钟
告警收敛同组告警合并,抑制规则防止告警风暴
告警可操作性每条告警必须附带排查指引或 Runbook 链接
定期审查每月审查告警规则,删除无效规则,调整阈值

日志设计

实践说明
结构化输出使用 JSON 格式,方便机器解析
请求 ID 串联每个请求分配唯一 ID,贯穿日志链路
区分日志级别ERROR(需要行动)、WARNING(需关注)、INFO(业务事件)、DEBUG(调试)
不记录敏感信息密码、Token、身份证号脱敏或脱出
上下文绑定使用 structlog.bind() 携带请求级上下文

异常追踪

实践说明
区分环境environment 区分 dev/staging/production
关联版本release 关联 CI/CD 版本号
合理采样关键接口 100%,健康检查 0%,普通接口 10%
自定义上下文添加 user_idtenant_id 等业务标签
Breadcrumbs记录异常前的操作路径,加速定位

术语表

术语英文说明
可观测性Observability通过系统外部输出推断内部状态的能力
指标Metrics可聚合的数值型时序数据,如 QPS、延迟
日志Logs离散的事件记录,通常为文本或 JSON
链路追踪Traces一个请求在分布式系统中的完整调用路径
时序数据库TSDBTime Series Database,存储带时间戳的数值数据
拉取模式Pull ModelPrometheus 主动从目标拉取指标数据
推送模式Push Model应用主动向监控系统推送数据
分位数Quantile数据分布中的分割点,如 P99 表示 99% 的数据低于此值
直方图Histogram将观测值落入预定义桶的分布统计
高基数High Cardinality标签的可能取值数量很大(如用户 ID)
采样率Sample Rate决定多少比例的事件被收集和分析
面包屑BreadcrumbsSentry 中记录异常发生前的操作序列
告警收敛Alert Deduplication合并重复或相关告警,避免告警风暴
PromQLPromQLPrometheus Query Language,查询时序数据的 DSL
LogQLLogQLLoki Query Language,查询日志的 DSL
SLAService Level Agreement服务等级协议,定义可用性和性能承诺
SLIService Level Indicator服务等级指标,衡量 SLA 的具体指标
SLOService Level Objective服务等级目标,SLI 的目标值

延伸阅读