feat(status): add /health aggregate endpoint and 10s TTL cache on /stats

This commit is contained in:
2026-05-21 23:53:15 +08:00
parent bf6d47e1fd
commit 6bf5600a26

View File

@@ -1,32 +1,53 @@
"""Define API routes for status.""" """Define API routes for status."""
import time
from typing import Any
from fastapi import APIRouter from fastapi import APIRouter
from app.config.settings import settings from app.config.settings import settings
from app.shared.bootstrap import get_document_query_service, get_vector_index from app.shared.bootstrap import (
# Keep route handlers close to their transport-layer wiring for easier auditing. get_bm25_retriever,
get_binary_store,
get_conversation_store,
get_document_query_service,
get_vector_index,
)
router = APIRouter(prefix="/status", tags=["系统状态"]) router = APIRouter(prefix="/status", tags=["系统状态"])
# ---------------------------------------------------------------------------
# Simple TTL cache for /stats (avoids O(N) doc scan on every request)
# ---------------------------------------------------------------------------
_stats_cache: dict[str, Any] = {}
_stats_cache_time: float = 0.0
_STATS_TTL_SECONDS: float = 10.0
@router.get("/stats") @router.get("/stats")
async def get_stats(): async def get_stats():
"""Return stats.""" """Return document statistics (cached for 10 s)."""
global _stats_cache, _stats_cache_time
now = time.time()
if _stats_cache and (now - _stats_cache_time) < _STATS_TTL_SECONDS:
return _stats_cache
documents = get_document_query_service().list_documents() documents = get_document_query_service().list_documents()
indexed = sum(1 for item in documents if item.status.value == "indexed") indexed = sum(1 for d in documents if d.status.value == "indexed")
failed = sum(1 for item in documents if item.status.value == "failed") failed = sum(1 for d in documents if d.status.value == "failed")
return { _stats_cache = {
"documents_total": len(documents), "documents_total": len(documents),
"documents_indexed": indexed, "documents_indexed": indexed,
"documents_failed": failed, "documents_failed": failed,
"chunks_total": sum(item.chunk_count for item in documents), "chunks_total": sum(d.chunk_count for d in documents),
} }
_stats_cache_time = now
return _stats_cache
@router.get("/config") @router.get("/config")
async def get_config(): async def get_config():
"""Return config.""" """Return system configuration."""
return { return {
"embedding_model": settings.embedding_model, "embedding_model": settings.embedding_model,
"embedding_dim": settings.embedding_dim, "embedding_dim": settings.embedding_dim,
@@ -44,5 +65,49 @@ async def get_config():
@router.get("/milvus/health") @router.get("/milvus/health")
async def milvus_health(): async def milvus_health():
"""Handle milvus health.""" """Return Milvus health (kept for backwards compat)."""
return get_vector_index().health() return get_vector_index().health()
@router.get("/health")
async def get_health():
"""Return aggregate health of all backend services."""
# --- Milvus ---
try:
milvus_info = get_vector_index().health()
milvus_status = "ok" if milvus_info.get("connected") else "error"
except Exception as exc: # noqa: BLE001
milvus_info = {}
milvus_status = "error"
milvus_info["error"] = str(exc)
# --- MinIO ---
try:
minio_connected = get_binary_store().client.connected
minio_status = "ok" if minio_connected else "error"
except Exception: # noqa: BLE001
minio_status = "error"
minio_connected = False
# --- BM25 ---
bm25 = get_bm25_retriever()
# --- Sessions ---
try:
session_count = len(get_conversation_store().list_sessions())
except Exception: # noqa: BLE001
session_count = 0
return {
"milvus": {"status": milvus_status, **milvus_info},
"minio": {"status": minio_status, "connected": minio_connected},
"bm25": {"available": bm25 is not None},
"reranker": {
"enabled": settings.reranker_enabled,
"model": settings.reranker_model if settings.reranker_enabled else None,
},
"sessions": {
"active": session_count,
"max": settings.session_max_sessions,
},
}