"""健康检查。 /healthz → 存活探针(liveness):不触碰依赖,快速返回。 /api/health → 就绪探针(readiness):探测 SQLite + 文件存储。 """ import time from fastapi import APIRouter, Response, status from pydantic import BaseModel from sqlalchemy import text from app.core.config import get_settings from app.core.db import get_session_factory router = APIRouter() class HealthComponent(BaseModel): status: str latency_ms: int | None = None error: str | None = None class HealthReport(BaseModel): status: str environment: str components: dict[str, HealthComponent] @router.get("/healthz") async def liveness() -> dict[str, str]: return {"status": "ok"} @router.get("/health", response_model=HealthReport) async def readiness(response: Response) -> HealthReport: report = HealthReport( status="ok", environment=get_settings().environment, components={ "database": _check_db(), "storage": _check_storage(), }, ) if any(c.status != "ok" for c in report.components.values()): report.status = "degraded" response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return report def _check_db() -> HealthComponent: start = time.perf_counter() try: factory = get_session_factory() with factory() as session: session.execute(text("SELECT 1")) except Exception as exc: # noqa: BLE001 return HealthComponent(status="down", error=_brief(exc)) return HealthComponent(status="ok", latency_ms=_ms(start)) def _check_storage() -> HealthComponent: start = time.perf_counter() try: from app.storage.local_storage import get_storage storage = get_storage() root = storage._root if not root.exists(): root.mkdir(parents=True, exist_ok=True) except Exception as exc: # noqa: BLE001 return HealthComponent(status="down", error=_brief(exc)) return HealthComponent(status="ok", latency_ms=_ms(start)) def _ms(start: float) -> int: return int((time.perf_counter() - start) * 1000) def _brief(exc: Exception) -> str: text = f"{type(exc).__name__}: {exc}" return text.split("\n")[0][:200]