Files
app/backend/fluksio/api/routes/utils.py
T
stroblmeandClaude Opus 5 640654bd66 Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a
user's venv, so the package that is about to be published takes the name
it is published under. Only the Python package moves; the repo, the
Docker WORKDIR and the compose project keep theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:48:05 +02:00

97 lines
3.3 KiB
Python

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(),
}