"""Monitoring router — observability stack status (Alertmanager + Prometheus).""" from __future__ import annotations import logging from typing import Any from fastapi import APIRouter, Body, Depends from media_library_viewer_api.config import get_settings from media_library_viewer_api.dependencies import get_settings_store from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.services.targets import build_node_exporter_targets logger = logging.getLogger(__name__) def _alertmanager_client() -> Any: """Return a simple HTTP client for the configured Alertmanager URL.""" import requests settings = get_settings() return requests.Session(), settings.alertmanager_url def _webhook_client() -> Any: """Return a simple HTTP client for the optional webhook receiver URL.""" import requests settings = get_settings() return requests.Session(), settings.alertmanager_webhook_url def _summary_from_alerts(alerts: list[dict[str, Any]]) -> dict[str, Any]: """Build a UI-friendly summary from Alertmanager /api/v1/alerts payload.""" by_severity: dict[str, int] = {} open_alerts: list[dict[str, Any]] = [] for alert in alerts: labels = alert.get("labels") or {} annotations = alert.get("annotations") or {} severity = labels.get("severity", "unknown") by_severity[severity] = by_severity.get(severity, 0) + 1 open_alerts.append( { "name": labels.get("alertname", "unknown"), "severity": severity, "category": labels.get("category", ""), "job_name": labels.get("job_name", labels.get("job", "")), "summary": annotations.get("summary", ""), "description": annotations.get("description", ""), "active_since": alert.get("startsAt"), "state": alert.get("status", "firing"), "labels": labels, } ) open_alerts.sort(key=lambda a: (a["severity"] not in {"critical", "warning"}, a["severity"], a["name"])) return { "total": len(open_alerts), "by_severity": by_severity, "alerts": open_alerts[:50], } router = APIRouter(prefix="/api/monitoring", tags=["monitoring"]) @router.get("/machines") def get_machines(store: SettingsStore = Depends(get_settings_store)) -> list[dict[str, Any]]: """Return enabled monitoring machines for the UI.""" return [m for m in store.list_machines() if m.get("enabled")] @router.get("/prometheus-targets") def get_prometheus_targets(store: SettingsStore = Depends(get_settings_store)) -> list[dict[str, Any]]: """Return Prometheus file-SD targets for remote Node Exporters. The backend writes these targets to a JSON file that Prometheus reads via file_sd_configs. This endpoint returns the same list live from the store so the UI can preview which machines will be scraped. """ targets = build_node_exporter_targets(store) logger.info("Prometheus targets requested count=%s", len(targets)) return targets @router.get("/alerts") def get_alertmanager_alerts() -> dict[str, Any]: """Return a summary of active Alertmanager alerts for the UI. Proxies the Alertmanager `/api/v1/alerts` endpoint and reshapes the payload into a stable, UI-friendly format. If Alertmanager is unreachable, the endpoint returns an empty summary and logs the failure so the UI can still render a health card instead of an error page. """ session, base_url = _alertmanager_client() try: response = session.get(f"{base_url}/api/v1/alerts", timeout=5) response.raise_for_status() data = response.json() except Exception: logger.exception("Failed to fetch Alertmanager alerts from %s", base_url) return {"total": 0, "by_severity": {}, "alerts": [], "error": "alertmanager_unreachable"} if data.get("status") != "success": return {"total": 0, "by_severity": {}, "alerts": [], "error": data.get("error", "unknown")} summary = _summary_from_alerts(data.get("data", [])) logger.info("Alertmanager alerts requested total=%s", summary["total"]) return summary @router.get("/alertmanager-status") def get_alertmanager_status() -> dict[str, Any]: """Return Alertmanager cluster/status for the UI health card. Uses the Alertmanager `/api/v2/status` endpoint and exposes only the high- level fields the UI needs: uptime, version, and whether the cluster is healthy. """ session, base_url = _alertmanager_client() try: response = session.get(f"{base_url}/api/v2/status", timeout=5) response.raise_for_status() data = response.json() except Exception: logger.exception("Failed to fetch Alertmanager status from %s", base_url) return {"up": False, "version": "", "uptime": ""} cluster = data.get("cluster") or {} status = data.get("clusterStatus") or {} return { "up": True, "version": data.get("versionInfo", {}).get("version", ""), "uptime": status.get("createdAt", ""), "name": "", "peers": [p.get("name", "") for p in cluster.get("peers", [])], } @router.post("/alertmanager-webhook") def receive_alertmanager_webhook(payload: dict[str, Any] = Body(...)) -> dict[str, str]: """Receive alerts from Alertmanager and optionally forward to a webhook URL. This endpoint is the receiver referenced by the optional `webhook_configs` block in Alertmanager. It logs the payload for audit/debug purposes and, if `ALERTMANAGER_WEBHOOK_URL` is configured, forwards the alert JSON verbatim. Forwarding is best-effort: a failure to reach the downstream webhook does not fail this endpoint, so Alertmanager sees a successful delivery. """ alerts = payload.get("alerts", []) logger.info( "Received Alertmanager webhook with %s alert(s), status=%s", len(alerts), payload.get("status", "unknown"), ) session, webhook_url = _webhook_client() if webhook_url: try: response = session.post(webhook_url, json=payload, timeout=10) response.raise_for_status() logger.info("Forwarded Alertmanager webhook to %s", webhook_url) except Exception: logger.exception("Failed to forward Alertmanager webhook to %s", webhook_url) else: logger.debug("No ALERTMANAGER_WEBHOOK_URL configured; webhook stored in logs only") return {"status": "received"}