"""The alert queue and per-alert diagnosis.""" from __future__ import annotations import threading import time import traceback import uuid from concurrent.futures import ThreadPoolExecutor from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy.orm import Session from triagelib import runbooks from ..auth import current_user from ..db import SessionLocal, get_db from ..models import User from ..services import Engine, RuleAdapter, get_or_create_case router = APIRouter(prefix="/api", tags=["alerts"]) engine = Engine() _jobs: dict[str, dict[str, Any]] = {} _jobs_lock = threading.Lock() _pool = ThreadPoolExecutor(max_workers=3, thread_name_prefix="triage") JOB_TTL = 30 * 60 class TriageBody(BaseModel): fingerprint: str force: bool = False @router.get("/alerts") def alert_queue(force: bool = False, db: Session = Depends(get_db), user: User = Depends(current_user)): return engine.queue(db, force=force) @router.post("/triage") def start_triage(body: TriageBody, db: Session = Depends(get_db), user: User = Depends(current_user)): alert = engine.find_alert(db, body.fingerprint) if alert is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "That alert is no longer firing. Refresh the queue.") case = get_or_create_case(db, alert, user) job_id = uuid.uuid4().hex[:12] with _jobs_lock: _reap() _jobs[job_id] = {"id": job_id, "state": "running", "created": time.time(), "result": None, "error": ""} _pool.submit(_run, job_id, alert, body.force) return {"job_id": job_id, "alert": alert.to_json(), "case": case.to_json(with_events=True)} @router.get("/jobs/{job_id}") def job(job_id: str, user: User = Depends(current_user)): with _jobs_lock: found = _jobs.get(job_id) if not found: raise HTTPException(status.HTTP_404_NOT_FOUND, "Unknown job") return found def _run(job_id: str, alert: Any, force: bool) -> None: started = time.monotonic() db = SessionLocal() try: diagnosis = runbooks.diagnose(alert, engine.prom, engine.snapshot.get(), force, RuleAdapter(db)) payload = diagnosis.to_json() payload["elapsed_seconds"] = round(time.monotonic() - started, 1) with _jobs_lock: _jobs[job_id].update({"state": "done", "result": payload}) except Exception: with _jobs_lock: _jobs[job_id].update({"state": "error", "error": traceback.format_exc(limit=4)}) finally: db.close() def _reap() -> None: cutoff = time.time() - JOB_TTL for key in [k for k, v in _jobs.items() if v["created"] < cutoff]: _jobs.pop(key, None)