From 6bf5600a263b507dace0d2777e0bf974414559a1 Mon Sep 17 00:00:00 2001 From: wangwei Date: Thu, 21 May 2026 23:53:15 +0800 Subject: [PATCH] feat(status): add /health aggregate endpoint and 10s TTL cache on /stats --- backend/app/api/routes/status.py | 85 ++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/status.py b/backend/app/api/routes/status.py index 6ec739f..b53b629 100644 --- a/backend/app/api/routes/status.py +++ b/backend/app/api/routes/status.py @@ -1,32 +1,53 @@ """Define API routes for status.""" +import time +from typing import Any + from fastapi import APIRouter from app.config.settings import settings -from app.shared.bootstrap import get_document_query_service, get_vector_index -# Keep route handlers close to their transport-layer wiring for easier auditing. - +from app.shared.bootstrap import ( + get_bm25_retriever, + get_binary_store, + get_conversation_store, + get_document_query_service, + get_vector_index, +) 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") 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() - indexed = sum(1 for item in documents if item.status.value == "indexed") - failed = sum(1 for item in documents if item.status.value == "failed") - return { + indexed = sum(1 for d in documents if d.status.value == "indexed") + failed = sum(1 for d in documents if d.status.value == "failed") + _stats_cache = { "documents_total": len(documents), "documents_indexed": indexed, "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") async def get_config(): - """Return config.""" + """Return system configuration.""" return { "embedding_model": settings.embedding_model, "embedding_dim": settings.embedding_dim, @@ -44,5 +65,49 @@ async def get_config(): @router.get("/milvus/health") async def milvus_health(): - """Handle milvus health.""" + """Return Milvus health (kept for backwards compat).""" 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, + }, + }