2026-05-20 17:02:45 +08:00
|
|
|
"""Nexus — Health Check Endpoint
|
|
|
|
|
External monitoring endpoint for Shell health_monitor.sh and Supervisor.
|
2026-05-24 10:58:33 +08:00
|
|
|
|
|
|
|
|
Security: Only returns minimal status ("ok") for external probes.
|
|
|
|
|
Detailed health diagnostics require admin JWT auth via /health/detail.
|
2026-05-20 17:02:45 +08:00
|
|
|
"""
|
|
|
|
|
|
2026-05-24 10:58:33 +08:00
|
|
|
import logging
|
|
|
|
|
|
2026-05-30 02:30:01 +08:00
|
|
|
from fastapi import APIRouter, Depends
|
|
|
|
|
from fastapi.responses import PlainTextResponse
|
2026-05-20 17:02:45 +08:00
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
2026-06-07 01:45:10 +08:00
|
|
|
from server.api.auth_jwt import require_current_admin
|
2026-05-24 10:58:33 +08:00
|
|
|
from server.domain.models import Admin
|
2026-05-20 17:02:45 +08:00
|
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
2026-05-24 10:58:33 +08:00
|
|
|
from server.infrastructure.redis.client import get_redis
|
2026-05-21 22:11:38 +08:00
|
|
|
from server.api.websocket import manager as ws_manager
|
2026-05-20 17:02:45 +08:00
|
|
|
|
2026-05-24 10:58:33 +08:00
|
|
|
logger = logging.getLogger("nexus.health")
|
|
|
|
|
|
2026-05-20 17:02:45 +08:00
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
2026-05-30 02:30:01 +08:00
|
|
|
@router.get("/health", response_class=PlainTextResponse)
|
2026-05-20 17:02:45 +08:00
|
|
|
async def health_check():
|
2026-05-30 02:30:01 +08:00
|
|
|
"""Minimal health check — used by Shell health_monitor.sh (Layer 3 guardian).
|
|
|
|
|
Returns plain-text "ok" — no JSON, no backend fingerprint.
|
|
|
|
|
health_monitor.sh discards response body (curl -sf > /dev/null),
|
|
|
|
|
so this is purely for HTTP status code.
|
2026-05-24 10:58:33 +08:00
|
|
|
"""
|
2026-05-30 02:30:01 +08:00
|
|
|
return "ok"
|
2026-05-24 10:58:33 +08:00
|
|
|
|
2026-05-20 17:02:45 +08:00
|
|
|
|
2026-05-24 10:58:33 +08:00
|
|
|
@router.get("/health/detail")
|
2026-06-07 01:45:10 +08:00
|
|
|
async def health_detail(admin: Admin = Depends(require_current_admin)):
|
2026-05-24 10:58:33 +08:00
|
|
|
"""Detailed health diagnostics — requires admin JWT authentication.
|
|
|
|
|
|
|
|
|
|
Returns Redis/MySQL/WebSocket status for admin dashboard use.
|
2026-05-20 17:02:45 +08:00
|
|
|
"""
|
|
|
|
|
checks = {"python": "ok"}
|
|
|
|
|
|
|
|
|
|
# Redis check
|
|
|
|
|
try:
|
2026-05-21 22:11:38 +08:00
|
|
|
redis = get_redis()
|
|
|
|
|
await redis.ping()
|
|
|
|
|
checks["redis"] = "ok"
|
2026-05-24 10:58:33 +08:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Redis health check failed: {e}")
|
2026-05-21 22:11:38 +08:00
|
|
|
checks["redis"] = "unavailable"
|
2026-05-20 17:02:45 +08:00
|
|
|
|
|
|
|
|
# MySQL check
|
|
|
|
|
try:
|
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
|
|
|
await session.execute(text("SELECT 1"))
|
2026-05-24 10:58:33 +08:00
|
|
|
checks["mysql"] = "ok"
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"MySQL health check failed: {e}")
|
2026-05-20 17:02:45 +08:00
|
|
|
checks["mysql"] = "error"
|
|
|
|
|
|
|
|
|
|
# WebSocket connections count
|
2026-05-24 10:58:33 +08:00
|
|
|
try:
|
|
|
|
|
checks["websocket_clients"] = await ws_manager.global_client_count()
|
|
|
|
|
except Exception:
|
|
|
|
|
checks["websocket_clients"] = ws_manager.client_count
|
2026-05-20 17:02:45 +08:00
|
|
|
|
|
|
|
|
return {"status": "ok", "checks": checks}
|