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:
@@ -7,6 +7,7 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
Gauge,
|
||||
Inbox,
|
||||
Radio,
|
||||
RefreshCw,
|
||||
@@ -17,9 +18,12 @@ import {
|
||||
import {
|
||||
useAlertmanagerAlerts,
|
||||
useAlertmanagerStatus,
|
||||
useGrafanaStatus,
|
||||
usePrometheusStatus,
|
||||
usePrometheusTargets,
|
||||
useMonitoringMachines,
|
||||
} from "../hooks/useObservability";
|
||||
import { useServiceInstances } from "../hooks/useServices";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
@@ -43,9 +47,6 @@ import type {
|
||||
PrometheusTarget,
|
||||
} from "../types";
|
||||
|
||||
const GRAFANA_BASE_URL =
|
||||
import.meta.env.VITE_GRAFANA_URL || "http://localhost:3000";
|
||||
|
||||
function severityVariant(
|
||||
severity: string,
|
||||
): "default" | "secondary" | "destructive" | "outline" {
|
||||
@@ -283,6 +284,18 @@ export function ObservabilityPage() {
|
||||
error: statusError,
|
||||
refetch: refetchStatus,
|
||||
} = useAlertmanagerStatus();
|
||||
const {
|
||||
data: grafanaStatus,
|
||||
isLoading: grafanaLoading,
|
||||
error: grafanaError,
|
||||
refetch: refetchGrafana,
|
||||
} = useGrafanaStatus();
|
||||
const {
|
||||
data: prometheusStatus,
|
||||
isLoading: prometheusLoading,
|
||||
error: prometheusError,
|
||||
refetch: refetchPrometheus,
|
||||
} = usePrometheusStatus();
|
||||
const {
|
||||
data: prometheusTargets,
|
||||
isLoading: targetsLoading,
|
||||
@@ -295,8 +308,14 @@ export function ObservabilityPage() {
|
||||
error: machinesError,
|
||||
refetch: refetchMachines,
|
||||
} = useMonitoringMachines();
|
||||
const { data: grafanaServices = [] } = useServiceInstances("grafana");
|
||||
const [selectedMachineId, setSelectedMachineId] = useState<string>("");
|
||||
|
||||
const grafanaService =
|
||||
grafanaServices.find((s) => s.enabled) ?? grafanaServices[0];
|
||||
const GRAFANA_BASE_URL =
|
||||
(grafanaService?.config?.base_url as string | undefined) ?? "";
|
||||
|
||||
const selectedMachine = useMemo<MonitoringMachine | null>(
|
||||
() =>
|
||||
machines.find((m) => m.id === selectedMachineId) ?? machines[0] ?? null,
|
||||
@@ -304,13 +323,13 @@ export function ObservabilityPage() {
|
||||
);
|
||||
|
||||
const nodeExporterDashboardUrl = useMemo(() => {
|
||||
if (!selectedMachine) return "";
|
||||
if (!selectedMachine || !GRAFANA_BASE_URL) return "";
|
||||
const instance = `${selectedMachine.host || "localhost"}:9100`;
|
||||
return `${GRAFANA_BASE_URL}/d/node-exporter-overview/node-exporter-overview?kiosk&var-instance=${encodeURIComponent(instance)}`;
|
||||
}, [selectedMachine]);
|
||||
}, [selectedMachine, GRAFANA_BASE_URL]);
|
||||
|
||||
const logsUrl = useMemo(() => {
|
||||
if (!selectedMachine) return "";
|
||||
if (!selectedMachine || !GRAFANA_BASE_URL) return "";
|
||||
const container =
|
||||
selectedMachine.mode === "local" ? "backend" : selectedMachine.name;
|
||||
return `${GRAFANA_BASE_URL}/explore?orgId=1&left=${encodeURIComponent(
|
||||
@@ -320,7 +339,7 @@ export function ObservabilityPage() {
|
||||
range: { from: "now-1h", to: "now" },
|
||||
}),
|
||||
)}`;
|
||||
}, [selectedMachine]);
|
||||
}, [selectedMachine, GRAFANA_BASE_URL]);
|
||||
|
||||
const alertmanagerStatusDetail = alertmanagerStatus?.up
|
||||
? alertmanagerStatus.version
|
||||
@@ -402,6 +421,52 @@ export function ObservabilityPage() {
|
||||
icon={Server}
|
||||
isLoading={machinesLoading}
|
||||
/>
|
||||
<HealthCard
|
||||
title="Grafana"
|
||||
status={
|
||||
grafanaError
|
||||
? "error"
|
||||
: grafanaStatus?.up
|
||||
? "ok"
|
||||
: grafanaLoading
|
||||
? "unknown"
|
||||
: "error"
|
||||
}
|
||||
detail={
|
||||
grafanaStatus?.up
|
||||
? grafanaStatus.version
|
||||
? `version ${grafanaStatus.version}`
|
||||
: "reachable"
|
||||
: grafanaStatus?.error === "no_service_configured"
|
||||
? "not configured"
|
||||
: "unreachable"
|
||||
}
|
||||
icon={Gauge}
|
||||
isLoading={grafanaLoading}
|
||||
/>
|
||||
<HealthCard
|
||||
title="Prometheus"
|
||||
status={
|
||||
prometheusError
|
||||
? "error"
|
||||
: prometheusStatus?.up
|
||||
? "ok"
|
||||
: prometheusLoading
|
||||
? "unknown"
|
||||
: "error"
|
||||
}
|
||||
detail={
|
||||
prometheusStatus?.up
|
||||
? prometheusStatus.version
|
||||
? `version ${prometheusStatus.version}`
|
||||
: "reachable"
|
||||
: prometheusStatus?.error === "no_service_configured"
|
||||
? "not configured"
|
||||
: "unreachable"
|
||||
}
|
||||
icon={Radio}
|
||||
isLoading={prometheusLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -433,6 +498,20 @@ export function ObservabilityPage() {
|
||||
refetch={refetchMachines}
|
||||
/>
|
||||
)}
|
||||
{grafanaError && (
|
||||
<QueryError
|
||||
label="Grafana status"
|
||||
error={grafanaError}
|
||||
refetch={refetchGrafana}
|
||||
/>
|
||||
)}
|
||||
{prometheusError && (
|
||||
<QueryError
|
||||
label="Prometheus status"
|
||||
error={prometheusError}
|
||||
refetch={refetchPrometheus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{alertsSummary?.error && (
|
||||
@@ -542,7 +621,8 @@ export function ObservabilityPage() {
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{selectedMachine ? (
|
||||
{selectedMachine ? (
|
||||
GRAFANA_BASE_URL ? (
|
||||
<>
|
||||
<GrafanaLinkCard
|
||||
title={`${selectedMachine.name} metrics`}
|
||||
@@ -556,6 +636,18 @@ export function ObservabilityPage() {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={Gauge}
|
||||
title="No Grafana service configured"
|
||||
description="Add a Grafana service instance to enable deep-links to dashboards and logs."
|
||||
action={
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to="/services">Open Services</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={ServerOff}
|
||||
title="No machine selected"
|
||||
|
||||
Reference in New Issue
Block a user