// Grafana's API client — grafana's own code, living in the module (novox/hq ADR 0044). Ported from // the shared hal sdk, where a change here rebuilt everything; here it rebuilds only grafana. Both // this module's tools and its events entrypoint import it, and nothing outside grafana does. export interface GrafanaHealth { database: string; version: string; commit: string; } export interface GrafanaDatasource { id: number; uid: string; name: string; type: string; url: string; isDefault: boolean; database?: string; } export interface GrafanaDashboard { uid: string; title: string; url: string; tags: string[]; folderTitle?: string; } export interface GrafanaAlert { /** The rule name (labels.alertname), the stable identity a firing alert is diffed on. */ name: string; /** Grafana unified-alerting state: "Normal" | "Pending" | "Alerting". */ state: string; labels: Record; activeAt?: string; } export class GrafanaClient { readonly baseUrl: string; private readonly authHeader: string; constructor(url: string, authHeader: string) { this.baseUrl = url.replace(/\/$/, ""); this.authHeader = authHeader; } /** * Build from the module's resolved environment. Auth is a service-account/API token * (MESH_GRAFANA_TOKEN, sent as Bearer) when present, else HTTP basic with the admin password the * module keeps as its own secret (MESH_GRAFANA_PASSWORD, user MESH_GRAFANA_USER, default admin). * Throws when neither is configured — the module then contributes nothing rather than failing. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): GrafanaClient { const url = env.MESH_GRAFANA_URL ?? `http://127.0.0.1:${env.GRAFANA_PORT ?? "3000"}`; const token = env.MESH_GRAFANA_TOKEN; if (token) return new GrafanaClient(url, `Bearer ${token}`); const password = env.MESH_GRAFANA_PASSWORD; if (password) { const user = env.MESH_GRAFANA_USER ?? "admin"; return new GrafanaClient(url, `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`); } throw new Error("no Grafana auth — set MESH_GRAFANA_TOKEN or MESH_GRAFANA_PASSWORD"); } private async get(path: string): Promise { const res = await fetch(`${this.baseUrl}${path}`, { headers: { Authorization: this.authHeader, Accept: "application/json" }, }); if (!res.ok) throw new Error(`Grafana API ${path}: ${res.status} ${await res.text()}`); return res.json(); } async health(): Promise { const h = await this.get("/api/health"); return { database: h.database ?? "unknown", version: h.version ?? "unknown", commit: h.commit ?? "unknown" }; } async listDatasources(): Promise { const arr = (await this.get("/api/datasources")) as any[]; return arr.map((d) => ({ id: d.id, uid: d.uid, name: d.name, type: d.type, url: d.url, isDefault: !!d.isDefault, database: d.database || undefined, })); } async listDashboards(query?: string): Promise { const params = new URLSearchParams({ type: "dash-db" }); if (query) params.set("query", query); const arr = (await this.get(`/api/search?${params.toString()}`)) as any[]; return arr.map((d) => ({ uid: d.uid, title: d.title, url: d.url, tags: d.tags ?? [], folderTitle: d.folderTitle || undefined, })); } /** * Active alert instances from unified alerting's Prometheus-compatible surface. Grafana without * alerting configured answers this with an empty set (or a 404, surfaced by get) — callers treat * "no alerts" and "no alerting" alike. */ async listAlerts(): Promise { const data = (await this.get("/api/prometheus/grafana/api/v1/alerts")).data ?? {}; const alerts = (data.alerts ?? []) as any[]; return alerts.map((a) => ({ name: a.labels?.alertname ?? "unknown", state: a.state ?? "unknown", labels: a.labels ?? {}, activeAt: a.activeAt || undefined, })); } }