feat(observability): service discovery, health cards, alertmanager widget
Slice 3 of observability-service-registry (frontend). The Observability
page discovers Grafana from the service registry instead of env vars,
adds Grafana + Prometheus health cards, and ships an alertmanager
active_alerts dashboard widget.
- types: added GrafanaStatus + PrometheusStatus; added optional
service_id/error to AlertmanagerStatus.
- api/client.ts + hooks/useObservability.ts: fetchGrafanaStatus,
fetchPrometheusStatus, useGrafanaStatus, usePrometheusStatus.
- widgets/AlertmanagerAlertsWidget.tsx (new): presentational widget
consuming the active_alerts summary shape (total/by_severity/alerts);
exported from widgets/index.ts.
- integrations/registry.ts: alertmanager binding (active_alerts kind,
30s refresh, optional severity_filter); registry.test.ts updated to
6 service types incl alertmanager + a resolve test.
- components/ObservabilityPage.tsx: removed
import.meta.env.VITE_GRAFANA_URL; derive GRAFANA_BASE_URL from the
first enabled grafana service via useServiceInstances("grafana");
added Grafana + Prometheus HealthCards (up/not-configured/unreachable)
with QueryError retry blocks; machine dashboard shows a "No Grafana
service configured" empty-state linking to /services when none is set.
npm run build (tsc -b + vite) clean; 0 lint errors; 72 frontend tests
pass. Reviewed fresh-context (read-only): no blockers.
This commit is contained in:
@@ -636,6 +636,7 @@ class TestResolveServiceRecord:
|
||||
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"])]
|
||||
|
||||
@@ -882,7 +883,9 @@ class TestGrafanaStatus:
|
||||
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"})
|
||||
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")),
|
||||
@@ -895,7 +898,9 @@ class TestGrafanaStatus:
|
||||
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"})
|
||||
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()
|
||||
@@ -920,7 +925,9 @@ class TestPrometheusStatus:
|
||||
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"})
|
||||
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")),
|
||||
@@ -932,7 +939,9 @@ class TestPrometheusStatus:
|
||||
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"})
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user