feat(api): vertical slice POST /v1/agent/chat and the Celery ping path
One request exercises every layer: JWT auth, quota reservation, a versioned prompt through the LLM gateway, a cost row, quota commit or refund, and the unified envelope. The trivial ping task proves the queue routing and the tk_task transitions before any real long-running task is written. Routes are mounted at the /api/v1 base path of 架构文档 §9.1; /v1/* is rewritten to it at the ASGI layer so both spellings work from one route table.
This commit is contained in:
Executable
+59
@@ -0,0 +1,59 @@
|
||||
"""Liveness, readiness and metrics. Mounted at the root, outside ``/api/v1``.
|
||||
|
||||
``/healthz`` must never touch a dependency: if it did, a slow database would
|
||||
make the orchestrator kill healthy processes. ``/readyz`` is the one that checks
|
||||
dependencies, because a process that cannot reach PostgreSQL should be taken out
|
||||
of the load-balancer rotation but not restarted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Response
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
|
||||
from sqlalchemy import text
|
||||
|
||||
from packages.shared.config import get_settings
|
||||
from packages.shared.db import get_session_factory
|
||||
from packages.shared.errors import AppError, ErrorCode
|
||||
from packages.shared.logging import get_logger
|
||||
from packages.shared.observability import REGISTRY
|
||||
from packages.shared.redis_client import get_redis
|
||||
from packages.shared.response import ok
|
||||
|
||||
router = APIRouter(tags=["ops"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/healthz", summary="Liveness probe")
|
||||
async def healthz() -> dict[str, object]:
|
||||
settings = get_settings()
|
||||
return ok({"status": "ok", "app": settings.app_name, "env": settings.app_env})
|
||||
|
||||
|
||||
@router.get("/readyz", summary="Readiness probe (checks PostgreSQL and Redis)")
|
||||
async def readyz() -> dict[str, object]:
|
||||
checks: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
factory = get_session_factory()
|
||||
async with factory() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
checks["postgres"] = "ok"
|
||||
except Exception as exc:
|
||||
checks["postgres"] = f"error: {type(exc).__name__}"
|
||||
|
||||
try:
|
||||
await get_redis().ping()
|
||||
checks["redis"] = "ok"
|
||||
except Exception as exc:
|
||||
checks["redis"] = f"error: {type(exc).__name__}"
|
||||
|
||||
if any(v != "ok" for v in checks.values()):
|
||||
logger.warning("readiness_failed", checks=checks)
|
||||
raise AppError(ErrorCode.UPSTREAM_ERROR, message="依赖未就绪", data={"checks": checks})
|
||||
return ok({"status": "ready", "checks": checks})
|
||||
|
||||
|
||||
@router.get("/metrics", summary="Prometheus scrape endpoint", include_in_schema=False)
|
||||
async def metrics() -> Response:
|
||||
return Response(content=generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)
|
||||
Reference in New Issue
Block a user