import time from typing import Any from fastapi import APIRouter, Depends, Request, Response from fastapi.concurrency import run_in_threadpool from pydantic.networks import EmailStr from fluksio.api.deps import get_current_active_superuser from fluksio.flow.state import RedisState from fluksio.models import Message from fluksio.utils import generate_test_email, send_email router = APIRouter(prefix="/utils", tags=["utils"]) @router.post( "/test-email/", dependencies=[Depends(get_current_active_superuser)], status_code=201, ) def test_email(email_to: EmailStr) -> Message: """ Test emails. """ email_data = generate_test_email(email_to=email_to) send_email( email_to=email_to, subject=email_data.subject, html_content=email_data.html_content, ) return Message(message="Test email sent") @router.get("/health-check/") async def health_check() -> bool: return True @router.get("/health/") async def health(request: Request, response: Response) -> dict[str, Any]: """Deep health: 200 while the engine can serve its purpose, 503 otherwise. "Serve its purpose" means the event loop is responsive and the configured state backend answers — the two failure modes a process-alive check never sees. The queue and pool sections are filled by the execution service. """ controller = getattr(request.app.state, "flow_controller", None) watchdog = getattr(request.app.state, "watchdog", None) problems: list[str] = [] loop_lag = watchdog.snapshot() if watchdog else {"ewma": 0.0, "max_60s": 0.0} if watchdog is not None and watchdog.degraded: problems.append("event loop lagging") redis_info: dict[str, Any] = { "configured": False, "connected": None, "rtt_ms": None, } engine: dict[str, Any] = {"flows": 0, "nodes": 0, "quarantined": []} queue: dict[str, Any] = {} if controller is not None: state = controller.state if isinstance(state, RedisState): redis_info["configured"] = True start = time.perf_counter() connected = await run_in_threadpool(state.ping) redis_info["connected"] = connected redis_info["rtt_ms"] = round((time.perf_counter() - start) * 1000, 1) if not connected: problems.append("redis unreachable") # `loaded` is keyed by node id, so its length is a node count. loaded = getattr(controller, "loaded", {}) or {} engine["flows"] = len({entry.flow for entry in loaded.values()}) engine["nodes"] = len( getattr(getattr(controller, "pipeline", None), "nodes", []) or [] ) engine["quarantined"] = sorted(getattr(controller, "quarantined", ()) or ()) stats = getattr(controller, "queue_stats", None) if callable(stats): queue = await run_in_threadpool(stats) if queue.get("oldest_pending_s", 0) > 120: problems.append("queue stalled") status = "degraded" if problems else "ok" if problems: response.status_code = 503 return { "status": status, "problems": problems, "loop_lag_ms": loop_lag, "redis": redis_info, "engine": engine, "queue": queue, "ts": time.time(), }