Files

121 lines
4.4 KiB
TypeScript

// Grafana's API client — grafana's own code, living in the module (novox/hq ADR 0039). 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.
import { readFileSync } from "node:fs";
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<string, string>;
activeAt?: string;
}
/** The settings-merged config the mesh delivers (novox/hq ADR 0046): { url, apiKey, token, password, user, ... }. */
function meshConfig(file?: string): Record<string, string> {
if (!file) return {};
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
catch { return {}; }
}
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 cfg = meshConfig(env.MESH_GRAFANA_CONFIG_FILE);
const url = cfg.url ?? env.MESH_GRAFANA_URL ?? `http://127.0.0.1:${env.GRAFANA_PORT ?? "3000"}`;
const token = cfg.token ?? env.MESH_GRAFANA_TOKEN;
if (token) return new GrafanaClient(url, `Bearer ${token}`);
const password = cfg.password ?? env.MESH_GRAFANA_PASSWORD;
if (password) {
const user = cfg.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<any> {
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<GrafanaHealth> {
const h = await this.get("/api/health");
return { database: h.database ?? "unknown", version: h.version ?? "unknown", commit: h.commit ?? "unknown" };
}
async listDatasources(): Promise<GrafanaDatasource[]> {
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<GrafanaDashboard[]> {
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<GrafanaAlert[]> {
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,
}));
}
}