feat(observability): resolve services from registry, add health endpoints

Slice 2 of observability-service-registry. The monitoring router resolves
observability components from the service registry instead of env vars.

- routers/monitoring.py: removed _alertmanager_client/_webhook_client env
  readers + the get_settings import. Added _resolve_service_record(store,
  service_type, service_id?) -> ServiceRecord|None (requested instance with
  type+enabled checks, else first enabled instance), plus _base_url/_timeout/
  _auth_headers (Bearer from api_key)/_status_response helpers.
- /alerts + /alertmanager-status now take service_id? + Depends(store),
  resolve an alertmanager service, return graceful not-configured/
  unreachable payloads including service_id/name; status down-branches now
  include peers:[] + error (fixes prior type drift).
- NEW /grafana-status (probes /api/health) and /prometheus-status (probes
  /-/healthy then /api/v1/status/buildinfo) returning
  {up,version,service_id,name,error}.
- Webhook receiver is now log-only (dropped the outbound
  ALERTMANAGER_WEBHOOK_URL forward).
- tests: rewrote TestAlertmanager + TestAlertmanagerWebhook to mock
  _resolve_service_record/requests.get (not-configured via empty registry);
  added TestGrafanaStatus/TestPrometheusStatus and a TestResolveServiceRecord
  unit class covering service_id match/type-mismatch/disabled and first-
  enabled/none-enabled paths.

Orphaned config fields alertmanager_url/alertmanager_webhook_url and the
env-var removal land in Slice 5. ruff clean; 240 backend tests pass.

Reviewed fresh-context (read-only): no blockers.
This commit is contained in:
Developer
2026-06-24 07:53:25 +00:00
parent 7d49df3e7d
commit 14771ae990
3 changed files with 408 additions and 123 deletions
@@ -1,34 +1,71 @@
"""Monitoring router — observability stack status (Alertmanager + Prometheus).""" """Monitoring router — observability service status.
Observability components (Alertmanager, Grafana, Prometheus) are resolved from
the service registry, not environment variables. The endpoints pick the first
enabled instance of a type when no ``service_id`` is given, and return graceful
"not configured" / "unreachable" payloads so the UI always renders a health card.
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
import requests
from fastapi import APIRouter, Body, Depends 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.dependencies import get_settings_store
from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.services.targets import build_node_exporter_targets from media_library_viewer_api.services.targets import build_node_exporter_targets
from media_library_viewer_api.widgets.sources import ServiceRecord, build_service_record
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _alertmanager_client() -> Any: def _resolve_service_record(
"""Return a simple HTTP client for the configured Alertmanager URL.""" store: SettingsStore, service_type: str, service_id: str | None = None
import requests ) -> ServiceRecord | None:
"""Return the requested service instance, else the first enabled one.
settings = get_settings() Returns ``None`` when the instance does not exist / is the wrong type, or
return requests.Session(), settings.alertmanager_url when no enabled instance of ``service_type`` is configured.
"""
if service_id:
row = store.get_service(service_id)
if not row or row.get("service_type") != service_type:
return None
if not row.get("enabled", True):
return None
return build_service_record(store, row)
for row in store.list_services(service_type):
if row.get("enabled", True):
return build_service_record(store, row)
return None
def _webhook_client() -> Any: def _base_url(service: ServiceRecord) -> str:
"""Return a simple HTTP client for the optional webhook receiver URL.""" return str(service.config.get("base_url") or "").rstrip("/")
import requests
settings = get_settings()
return requests.Session(), settings.alertmanager_webhook_url def _timeout(service: ServiceRecord, default: int) -> int:
return int(service.config.get("timeout_seconds") or default)
def _auth_headers(service: ServiceRecord) -> dict[str, str]:
api_key = str(service.secrets.get("api_key") or "")
return {"Authorization": f"Bearer {api_key}"} if api_key else {}
def _status_response(
service: ServiceRecord | None, *, version: str = "", error: str | None = None
) -> dict[str, Any]:
return {
"up": error is None,
"version": version or "",
"service_id": service.id if service else "",
"name": service.name if service else "",
"error": error,
}
def _summary_from_alerts(alerts: list[dict[str, Any]]) -> dict[str, Any]: def _summary_from_alerts(alerts: list[dict[str, Any]]) -> dict[str, Any]:
@@ -49,11 +86,9 @@ def get_machines(store: SettingsStore = Depends(get_settings_store)) -> list[dic
@router.get("/prometheus-targets") @router.get("/prometheus-targets")
def get_prometheus_targets(store: SettingsStore = Depends(get_settings_store)) -> list[dict[str, Any]]: def get_prometheus_targets(store: SettingsStore = Depends(get_settings_store)) -> list[dict[str, Any]]:
"""Return Prometheus file-SD targets for remote Node Exporters. """Return Prometheus scrape targets for remote Node Exporters.
The backend writes these targets to a JSON file that Prometheus reads via External Prometheus instances consume this list via ``http_sd_configs``.
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) targets = build_node_exporter_targets(store)
logger.info("Prometheus targets requested count=%s", len(targets)) logger.info("Prometheus targets requested count=%s", len(targets))
@@ -61,52 +96,90 @@ def get_prometheus_targets(store: SettingsStore = Depends(get_settings_store)) -
@router.get("/alerts") @router.get("/alerts")
def get_alertmanager_alerts() -> dict[str, Any]: def get_alertmanager_alerts(
service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
) -> dict[str, Any]:
"""Return a summary of active Alertmanager alerts for the UI. """Return a summary of active Alertmanager alerts for the UI.
Proxies the Alertmanager `/api/v1/alerts` endpoint and reshapes the payload Resolves an ``alertmanager`` service instance from the registry. When none
into a stable, UI-friendly format. If Alertmanager is unreachable (or not is configured the endpoint returns an empty summary with an
configured via ``ALERTMANAGER_URL``), the endpoint returns an empty summary ``alertmanager_not_configured`` error so the UI can render a health card.
so the UI can still render a health card instead of an error page.
""" """
session, base_url = _alertmanager_client() service = _resolve_service_record(store, "alertmanager", service_id)
if not base_url: if service is None:
return {"total": 0, "by_severity": {}, "alerts": [], "error": "alertmanager_not_configured"} return {"total": 0, "by_severity": {}, "alerts": [], "error": "alertmanager_not_configured"}
try: try:
response = session.get(f"{base_url}/api/v1/alerts", timeout=5) response = requests.get(
f"{_base_url(service)}/api/v1/alerts",
headers=_auth_headers(service),
timeout=_timeout(service, 5),
)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
except Exception: except Exception:
logger.exception("Failed to fetch Alertmanager alerts from %s", base_url) logger.exception("Failed to fetch Alertmanager alerts")
return {"total": 0, "by_severity": {}, "alerts": [], "error": "alertmanager_unreachable"} return {
"total": 0,
"by_severity": {},
"alerts": [],
"error": "alertmanager_unreachable",
"service_id": service.id,
"name": service.name,
}
if data.get("status") != "success": if data.get("status") != "success":
return {"total": 0, "by_severity": {}, "alerts": [], "error": data.get("error", "unknown")} return {
"total": 0,
"by_severity": {},
"alerts": [],
"error": data.get("error", "unknown"),
"service_id": service.id,
"name": service.name,
}
summary = _summary_from_alerts(data.get("data", [])) summary = _summary_from_alerts(data.get("data", []))
summary["service_id"] = service.id
summary["name"] = service.name
logger.info("Alertmanager alerts requested total=%s", summary["total"]) logger.info("Alertmanager alerts requested total=%s", summary["total"])
return summary return summary
@router.get("/alertmanager-status") @router.get("/alertmanager-status")
def get_alertmanager_status() -> dict[str, Any]: def get_alertmanager_status(
"""Return Alertmanager cluster/status for the UI health card. service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
Uses the Alertmanager `/api/v2/status` endpoint and exposes only the high- ) -> dict[str, Any]:
level fields the UI needs: uptime, version, and whether the cluster is """Return Alertmanager cluster/status for the UI health card."""
healthy. Returns ``up=False`` when Alertmanager is unreachable or not service = _resolve_service_record(store, "alertmanager", service_id)
configured via ``ALERTMANAGER_URL``. if service is None:
""" return {
session, base_url = _alertmanager_client() "up": False,
if not base_url: "version": "",
return {"up": False, "version": "", "uptime": ""} "uptime": "",
"name": "",
"peers": [],
"error": "alertmanager_not_configured",
}
try: try:
response = session.get(f"{base_url}/api/v2/status", timeout=5) response = requests.get(
f"{_base_url(service)}/api/v2/status",
headers=_auth_headers(service),
timeout=_timeout(service, 5),
)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
except Exception: except Exception:
logger.exception("Failed to fetch Alertmanager status from %s", base_url) logger.exception("Failed to fetch Alertmanager status")
return {"up": False, "version": "", "uptime": ""} return {
"up": False,
"version": "",
"uptime": "",
"name": service.name,
"peers": [],
"service_id": service.id,
"error": "alertmanager_unreachable",
}
cluster = data.get("cluster") or {} cluster = data.get("cluster") or {}
status = data.get("clusterStatus") or {} status = data.get("clusterStatus") or {}
@@ -114,20 +187,70 @@ def get_alertmanager_status() -> dict[str, Any]:
"up": True, "up": True,
"version": data.get("versionInfo", {}).get("version", ""), "version": data.get("versionInfo", {}).get("version", ""),
"uptime": status.get("createdAt", ""), "uptime": status.get("createdAt", ""),
"name": "", "name": service.name,
"peers": [p.get("name", "") for p in cluster.get("peers", [])], "peers": [p.get("name", "") for p in cluster.get("peers", [])],
"service_id": service.id,
"error": None,
} }
@router.get("/grafana-status")
def get_grafana_status(
service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
) -> dict[str, Any]:
"""Probe a Grafana service instance's ``/api/health`` endpoint."""
service = _resolve_service_record(store, "grafana", service_id)
if service is None:
return _status_response(None, error="no_service_configured")
try:
response = requests.get(
f"{_base_url(service)}/api/health",
headers=_auth_headers(service),
timeout=_timeout(service, 5),
)
response.raise_for_status()
data = response.json()
except Exception:
logger.exception("Failed to fetch Grafana status")
return _status_response(service, error="grafana_unreachable")
return _status_response(service, version=data.get("version", ""))
@router.get("/prometheus-status")
def get_prometheus_status(
service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
) -> dict[str, Any]:
"""Probe a Prometheus service instance's health and build info."""
service = _resolve_service_record(store, "prometheus", service_id)
if service is None:
return _status_response(None, error="no_service_configured")
base = _base_url(service)
timeout = _timeout(service, 10)
headers = _auth_headers(service)
try:
health = requests.get(f"{base}/-/healthy", headers=headers, timeout=timeout)
health.raise_for_status()
build_info = requests.get(
f"{base}/api/v1/status/buildinfo", headers=headers, timeout=timeout
)
build_info.raise_for_status()
version = build_info.json().get("data", {}).get("version", "")
except Exception:
logger.exception("Failed to fetch Prometheus status")
return _status_response(service, error="prometheus_unreachable")
return _status_response(service, version=version)
@router.post("/alertmanager-webhook") @router.post("/alertmanager-webhook")
def receive_alertmanager_webhook(payload: dict[str, Any] = Body(...)) -> dict[str, str]: def receive_alertmanager_webhook(payload: dict[str, Any] = Body(...)) -> dict[str, str]:
"""Receive alerts from Alertmanager and optionally forward to a webhook URL. """Receive alerts from Alertmanager and log them for audit/debug.
This endpoint is the receiver referenced by the optional `webhook_configs` This endpoint is the receiver referenced by the optional ``webhook_configs``
block in Alertmanager. It logs the payload for audit/debug purposes and, if block in Alertmanager. It is log-only: received payloads are recorded but not
`ALERTMANAGER_WEBHOOK_URL` is configured, forwards the alert JSON verbatim. forwarded anywhere. (The previous outbound relay to ``ALERTMANAGER_WEBHOOK_URL``
Forwarding is best-effort: a failure to reach the downstream webhook does was removed when observability became service-registry configured.)
not fail this endpoint, so Alertmanager sees a successful delivery.
""" """
alerts = payload.get("alerts", []) alerts = payload.get("alerts", [])
logger.info( logger.info(
@@ -135,16 +258,4 @@ def receive_alertmanager_webhook(payload: dict[str, Any] = Body(...)) -> dict[st
len(alerts), len(alerts),
payload.get("status", "unknown"), 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"} return {"status": "received"}
+236 -60
View File
@@ -24,6 +24,10 @@ from media_library_viewer_api.main import app
from media_library_viewer_api.routers.media import get_media_index from media_library_viewer_api.routers.media import get_media_index
from media_library_viewer_api.services.media_index import MediaIndex from media_library_viewer_api.services.media_index import MediaIndex
from media_library_viewer_api.services.settings_store import SettingsStore from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.widgets.sources import ServiceRecord
# Short alias for the monitoring router module under test.
_MON = "media_library_viewer_api.routers.monitoring"
# --- Fixtures --- # --- Fixtures ---
@@ -626,6 +630,76 @@ class TestMonitoring:
assert data[0]["labels"]["job"] == "node-exporter-remote" assert data[0]["labels"]["job"] == "node-exporter-remote"
class TestResolveServiceRecord:
"""Unit tests for _resolve_service_record (service_id + first-enabled paths)."""
def _store(self, rows):
store = MagicMock()
store.get_service = lambda sid: next((r for r in rows if r["id"] == sid), None)
def list_filtered(service_type=None):
return [r for r in rows if r["service_type"] == (service_type or r["service_type"])]
store.list_services = list_filtered
return store
def test_service_id_match_returns_record(self):
from media_library_viewer_api.routers.monitoring import _resolve_service_record
row = {"id": "am1", "service_type": "alertmanager", "name": "AM", "enabled": True, "config": {}, "secrets": {}}
store = self._store([row])
with patch(f"{_MON}.build_service_record", return_value="RECORD") as mock_build:
result = _resolve_service_record(store, "alertmanager", "am1")
assert result == "RECORD"
mock_build.assert_called_once_with(store, row)
def test_service_id_type_mismatch_returns_none(self):
from media_library_viewer_api.routers.monitoring import _resolve_service_record
row = {"id": "x1", "service_type": "grafana", "name": "G", "enabled": True, "config": {}, "secrets": {}}
store = self._store([row])
assert _resolve_service_record(store, "alertmanager", "x1") is None
def test_service_id_disabled_returns_none(self):
from media_library_viewer_api.routers.monitoring import _resolve_service_record
row = {"id": "am1", "service_type": "alertmanager", "name": "AM", "enabled": False, "config": {}, "secrets": {}}
store = self._store([row])
assert _resolve_service_record(store, "alertmanager", "am1") is None
def test_no_service_id_returns_first_enabled(self):
from media_library_viewer_api.routers.monitoring import _resolve_service_record
rows = [
{
"id": "am1",
"service_type": "alertmanager",
"name": "Disabled",
"enabled": False,
"config": {},
"secrets": {},
},
{
"id": "am2",
"service_type": "alertmanager",
"name": "Active",
"enabled": True,
"config": {},
"secrets": {},
},
]
store = self._store(rows)
with patch(f"{_MON}.build_service_record", return_value="RECORD") as mock_build:
result = _resolve_service_record(store, "alertmanager", None)
assert result == "RECORD"
mock_build.assert_called_once_with(store, rows[1])
def test_no_service_id_and_none_enabled_returns_none(self):
from media_library_viewer_api.routers.monitoring import _resolve_service_record
store = self._store([])
assert _resolve_service_record(store, "alertmanager", None) is None
class TestSettingsMachines: class TestSettingsMachines:
def test_post_machine_rewrites_prometheus_targets(self, test_client): def test_post_machine_rewrites_prometheus_targets(self, test_client):
with patch("media_library_viewer_api.routers.settings.write_prometheus_targets") as write_targets: with patch("media_library_viewer_api.routers.settings.write_prometheus_targets") as write_targets:
@@ -665,75 +739,120 @@ class TestSettingsMachines:
write_targets.assert_called_once() write_targets.assert_called_once()
def _am_service(name="Alertmanager", **config):
cfg = {"base_url": "http://alertmanager:9093", "timeout_seconds": 5}
cfg.update(config)
return ServiceRecord(id="am1", service_type="alertmanager", name=name, config=cfg)
class TestAlertmanager: class TestAlertmanager:
def test_alerts_endpoint_when_alertmanager_unreachable(self, test_client): def test_alerts_endpoint_when_not_configured(self, test_client):
with patch("media_library_viewer_api.routers.monitoring._alertmanager_client") as mock_client: # No alertmanager service seeded -> registry resolves None.
session = MagicMock() response = test_client.get("/api/monitoring/alerts")
session.get.side_effect = Exception("connection refused") assert response.status_code == 200
mock_client.return_value = (session, "http://alertmanager:9093") data = response.json()
assert data["total"] == 0
assert data["error"] == "alertmanager_not_configured"
def test_alerts_endpoint_when_unreachable(self, test_client):
service = _am_service()
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", side_effect=Exception("connection refused")),
):
response = test_client.get("/api/monitoring/alerts") response = test_client.get("/api/monitoring/alerts")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] == 0 assert data["total"] == 0
assert data["error"] == "alertmanager_unreachable" assert data["error"] == "alertmanager_unreachable"
assert data["service_id"] == "am1"
def test_alerts_endpoint_when_not_configured(self, test_client): assert data["name"] == "Alertmanager"
with patch("media_library_viewer_api.routers.monitoring._alertmanager_client") as mock_client:
session = MagicMock()
mock_client.return_value = (session, "")
response = test_client.get("/api/monitoring/alerts")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert data["error"] == "alertmanager_not_configured"
session.get.assert_not_called()
def test_alerts_endpoint_returns_summary(self, test_client): def test_alerts_endpoint_returns_summary(self, test_client):
with patch("media_library_viewer_api.routers.monitoring._alertmanager_client") as mock_client: service = _am_service()
session = MagicMock() resp = MagicMock()
session.get.return_value.json.return_value = { resp.json.return_value = {
"status": "success", "status": "success",
"data": [ "data": [
{ {
"labels": {"alertname": "BackupJobFailed", "severity": "critical", "job_name": "test"}, "labels": {"alertname": "BackupJobFailed", "severity": "critical", "job_name": "test"},
"annotations": {"summary": "Backup failed", "description": "details"}, "annotations": {"summary": "Backup failed", "description": "details"},
"startsAt": "2026-05-11T02:00:00Z", "startsAt": "2026-05-11T02:00:00Z",
"status": "firing", "status": "firing",
} }
], ],
} }
session.get.return_value.raise_for_status = MagicMock() resp.raise_for_status = MagicMock()
mock_client.return_value = (session, "http://alertmanager:9093") with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", return_value=resp),
):
response = test_client.get("/api/monitoring/alerts") response = test_client.get("/api/monitoring/alerts")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] == 1 assert data["total"] == 1
assert data["by_severity"]["critical"] == 1 assert data["by_severity"]["critical"] == 1
assert data["alerts"][0]["name"] == "BackupJobFailed" assert data["alerts"][0]["name"] == "BackupJobFailed"
assert data["service_id"] == "am1"
def test_alertmanager_status_endpoint_when_not_configured(self, test_client): def test_alerts_endpoint_sends_bearer_token(self, test_client):
with patch("media_library_viewer_api.routers.monitoring._alertmanager_client") as mock_client: service = _am_service()
session = MagicMock() service.secrets = {"api_key": "tok"}
mock_client.return_value = (session, "") resp = MagicMock()
resp.json.return_value = {"status": "success", "data": []}
resp.raise_for_status = MagicMock()
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", return_value=resp) as mock_get,
):
test_client.get("/api/monitoring/alerts")
_, kwargs = mock_get.call_args
assert kwargs["headers"]["Authorization"] == "Bearer tok"
def test_alertmanager_status_when_not_configured(self, test_client):
response = test_client.get("/api/monitoring/alertmanager-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is False
assert data["error"] == "alertmanager_not_configured"
assert data["peers"] == []
def test_alertmanager_status_when_unreachable(self, test_client):
service = _am_service()
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", side_effect=Exception("refused")),
):
response = test_client.get("/api/monitoring/alertmanager-status") response = test_client.get("/api/monitoring/alertmanager-status")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["up"] is False assert data["up"] is False
session.get.assert_not_called() assert data["error"] == "alertmanager_unreachable"
assert data["name"] == "Alertmanager"
def test_alertmanager_status_endpoint_when_unreachable(self, test_client): def test_alertmanager_status_returns_cluster_info(self, test_client):
with patch("media_library_viewer_api.routers.monitoring._alertmanager_client") as mock_client: service = _am_service()
session = MagicMock() resp = MagicMock()
session.get.side_effect = Exception("connection refused") resp.json.return_value = {
mock_client.return_value = (session, "http://alertmanager:9093") "versionInfo": {"version": "0.27.0"},
"clusterStatus": {"createdAt": "2026-06-01T00:00:00Z"},
"cluster": {"peers": [{"name": "am-1"}, {"name": "am-2"}]},
}
resp.raise_for_status = MagicMock()
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", return_value=resp),
):
response = test_client.get("/api/monitoring/alertmanager-status") response = test_client.get("/api/monitoring/alertmanager-status")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["up"] is False assert data["up"] is True
assert data["version"] == "0.27.0"
assert data["peers"] == ["am-1", "am-2"]
class TestAlertmanagerWebhook: class TestAlertmanagerWebhook:
"""Tests for POST /api/monitoring/alertmanager-webhook.""" """Tests for POST /api/monitoring/alertmanager-webhook (log-only receiver)."""
def test_webhook_receives_payload_and_logs(self, test_client, caplog): def test_webhook_receives_payload_and_logs(self, test_client, caplog):
payload = { payload = {
@@ -753,22 +872,79 @@ class TestAlertmanagerWebhook:
assert response.json()["status"] == "received" assert response.json()["status"] == "received"
assert "Received Alertmanager webhook with 1 alert(s)" in caplog.text assert "Received Alertmanager webhook with 1 alert(s)" in caplog.text
def test_webhook_forwards_when_configured(self, test_client, caplog, monkeypatch):
payload = {"status": "resolved", "alerts": []}
forwarded = {"captured": False}
class FakeResponse: class TestGrafanaStatus:
def raise_for_status(self): def test_grafana_status_when_not_configured(self, test_client):
pass response = test_client.get("/api/monitoring/grafana-status")
def fake_post(url, json, timeout):
forwarded["captured"] = True
forwarded["url"] = url
forwarded["payload"] = json
return FakeResponse()
monkeypatch.setattr("requests.Session.post", lambda _self, url, json, timeout: fake_post(url, json, timeout))
with caplog.at_level("INFO", logger="media_library_viewer_api.routers.monitoring"):
response = test_client.post("/api/monitoring/alertmanager-webhook", json=payload)
assert response.status_code == 200 assert response.status_code == 200
assert forwarded["captured"] is False data = response.json()
assert data["up"] is False
assert data["error"] == "no_service_configured"
def test_grafana_status_when_unreachable(self, test_client):
service = ServiceRecord(id="g1", service_type="grafana", name="Grafana", config={"base_url": "http://grafana:3000"})
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", side_effect=Exception("refused")),
):
response = test_client.get("/api/monitoring/grafana-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is False
assert data["error"] == "grafana_unreachable"
assert data["name"] == "Grafana"
def test_grafana_status_returns_version(self, test_client):
service = ServiceRecord(id="g1", service_type="grafana", name="Grafana", config={"base_url": "http://grafana:3000"})
resp = MagicMock()
resp.json.return_value = {"version": "11.3.1", "database": "ok"}
resp.raise_for_status = MagicMock()
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", return_value=resp),
):
response = test_client.get("/api/monitoring/grafana-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is True
assert data["version"] == "11.3.1"
assert data["service_id"] == "g1"
class TestPrometheusStatus:
def test_prometheus_status_when_not_configured(self, test_client):
response = test_client.get("/api/monitoring/prometheus-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is False
assert data["error"] == "no_service_configured"
def test_prometheus_status_when_unreachable(self, test_client):
service = ServiceRecord(id="p1", service_type="prometheus", name="Prometheus", config={"base_url": "http://prometheus:9090"})
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", side_effect=Exception("refused")),
):
response = test_client.get("/api/monitoring/prometheus-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is False
assert data["error"] == "prometheus_unreachable"
def test_prometheus_status_returns_version(self, test_client):
service = ServiceRecord(id="p1", service_type="prometheus", name="Prometheus", config={"base_url": "http://prometheus:9090"})
health = MagicMock()
health.raise_for_status = MagicMock()
build_info = MagicMock()
build_info.raise_for_status = MagicMock()
build_info.json.return_value = {"status": "success", "data": {"version": "2.55.1"}}
with (
patch(f"{_MON}._resolve_service_record", return_value=service),
patch(f"{_MON}.requests.get", side_effect=[health, build_info]),
):
response = test_client.get("/api/monitoring/prometheus-status")
assert response.status_code == 200
data = response.json()
assert data["up"] is True
assert data["version"] == "2.55.1"
assert data["service_id"] == "p1"
+1 -3
View File
@@ -380,9 +380,7 @@ async def test_alertmanager_adapter_sends_bearer_token():
config={"base_url": "http://am:9093"}, config={"base_url": "http://am:9093"},
secrets={"api_key": "tok"}, secrets={"api_key": "tok"},
) )
payload = SimpleNamespace( payload = SimpleNamespace(raise_for_status=lambda: None, json=lambda: {"status": "success", "data": []})
raise_for_status=lambda: None, json=lambda: {"status": "success", "data": []}
)
with patch("media_library_viewer_api.widgets.sources.requests.get", return_value=payload) as mock_get: with patch("media_library_viewer_api.widgets.sources.requests.get", return_value=payload) as mock_get:
result = await adapter.fetch(service, "active_alerts", {}) result = await adapter.fetch(service, "active_alerts", {})
assert result["total"] == 0 assert result["total"] == 0