83 lines
2.2 KiB
Python
83 lines
2.2 KiB
Python
"""健康检查。
|
|
|
|
/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] |