Files
xiaoxia 53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) (#1928)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 12:59:17 +08:00

146 lines
4.1 KiB
Python

"""
Prometheus metrics middleware and metrics endpoint for the API.
Exposes:
- HTTP request duration histogram (by method, path, status)
- HTTP requests total counter (by method, path, status)
- Requests in progress gauge
- Custom application metrics
"""
import re
import time
from collections.abc import Callable
from fastapi import Request, Response
from prometheus_client import (
CONTENT_TYPE_LATEST,
REGISTRY,
Counter,
Gauge,
Histogram,
generate_latest,
)
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import PlainTextResponse
# Buckets for HTTP request duration (seconds)
HTTP_DURATION_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
# --- Metrics Definitions ---
HTTP_REQUESTS_TOTAL = Counter(
"http_requests_total",
"Total HTTP requests",
["method", "endpoint", "status"],
registry=REGISTRY,
)
HTTP_REQUEST_DURATION_SECONDS = Histogram(
"http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint", "status"],
buckets=HTTP_DURATION_BUCKETS,
registry=REGISTRY,
)
HTTP_REQUESTS_IN_PROGRESS = Gauge(
"http_requests_in_progress",
"Number of HTTP requests currently being processed",
["method"],
registry=REGISTRY,
)
# --- Application Metrics ---
APP_INFO = Gauge(
"app_info",
"Application information",
["version", "environment"],
registry=REGISTRY,
)
def _normalize_path(path: str) -> str:
"""
Normalize request path to reduce cardinality.
Replace UUIDs and numeric IDs with placeholders.
"""
# Replace UUIDs
path = re.sub(
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
":uuid",
path,
)
# Replace numeric IDs in path segments
path = re.sub(r"/\d+(/|$)", "/:id\\1", path)
return path
class PrometheusMetricsMiddleware(BaseHTTPMiddleware):
"""Middleware that records Prometheus metrics for every HTTP request."""
def __init__(self, app, exclude_paths: list[str] | None = None):
super().__init__(app)
self.exclude_paths = set(exclude_paths or ["/metrics", "/health", "/ready", "/startup"])
async def dispatch(self, request: Request, call_next: Callable) -> Response:
path = request.url.path
if path in self.exclude_paths:
return await call_next(request)
method = request.method
normalized_path = _normalize_path(path)
HTTP_REQUESTS_IN_PROGRESS.labels(method=method).inc()
start_time = time.perf_counter()
try:
response = await call_next(request)
status = str(response.status_code)
return response
except Exception:
status = "500"
raise
finally:
duration = time.perf_counter() - start_time
HTTP_REQUESTS_TOTAL.labels(
method=method,
endpoint=normalized_path,
status=status,
).inc()
HTTP_REQUEST_DURATION_SECONDS.labels(
method=method,
endpoint=normalized_path,
status=status,
).observe(duration)
HTTP_REQUESTS_IN_PROGRESS.labels(method=method).dec()
async def metrics_endpoint(request: Request) -> PlainTextResponse:
"""FastAPI endpoint that returns Prometheus metrics in text format.
需要 Bearer Token 认证,Token 通过 METRICS_AUTH_TOKEN 环境变量配置。
"""
import os
# Bearer Token 认证
auth_token = os.getenv("METRICS_AUTH_TOKEN", "")
if auth_token:
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer ") or auth_header[7:] != auth_token:
return PlainTextResponse(content="Unauthorized", status_code=401)
version = os.getenv("APP_VERSION", "unknown")
environment = os.getenv("APP_ENV", "unknown")
APP_INFO.labels(version=version, environment=environment).set(1)
metrics_output = generate_latest(REGISTRY)
return PlainTextResponse(
content=metrics_output,
media_type=CONTENT_TYPE_LATEST,
)