UNPKG

openclaw-grafana-lens

Version:

OpenClaw plugin that gives AI agents full Grafana access — 18 composable tools for PromQL/LogQL/TraceQL queries, dashboard creation, alerting, SRE investigation, security monitoring, data collection pipeline management via Grafana Alloy (29 recipes), and

155 lines (154 loc) 6.38 kB
/** * Alert Webhook Handler Service * * Receives Grafana alert notifications via HTTP webhook, stores them in a * bounded in-memory queue, and makes them available to the agent via the * `grafana_check_alerts` tool. * * The flow: * Grafana alert fires → POST to /grafana-lens/alerts → stored here * → agent sees alerts in prompt context (before_prompt_build hook) * → agent investigates with grafana_query + grafana_annotate * * Design: bounded queue (max 50, 24h TTL) keeps memory usage predictable. * Grafana expects fast webhook responses, so we return 200 immediately * and process asynchronously. */ // ── Alert Store ──────────────────────────────────────────────────── const MAX_ALERTS = 50; const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours export function createAlertStore() { const alerts = []; let counter = 0; let firingCount = 0; let resolvedCount = 0; function evict() { const now = Date.now(); // Remove expired alerts for (let i = alerts.length - 1; i >= 0; i--) { if (now - alerts[i].receivedAt > TTL_MS) { alerts.splice(i, 1); } } // Remove oldest if over capacity while (alerts.length > MAX_ALERTS) { alerts.shift(); } } return { getPendingAlerts() { evict(); return alerts.filter((a) => !a.acknowledged); }, getAllAlerts() { evict(); return [...alerts]; }, getAlert(id) { return alerts.find((a) => a.id === id); }, acknowledgeAlert(id) { const alert = alerts.find((a) => a.id === id); if (!alert) return false; alert.acknowledged = true; return true; }, acknowledgeAll() { let count = 0; for (const alert of alerts) { if (!alert.acknowledged) { alert.acknowledged = true; count++; } } return count; }, addAlert(notification) { evict(); if (notification.status === "firing") firingCount++; else resolvedCount++; const stored = { id: `alert-${++counter}`, receivedAt: Date.now(), acknowledged: false, title: notification.title, status: notification.status, message: notification.message, alerts: notification.alerts, commonLabels: notification.commonLabels, groupLabels: notification.groupLabels, externalURL: notification.externalURL, }; alerts.push(stored); return stored; }, size() { evict(); return alerts.length; }, totalReceived() { return { firing: firingCount, resolved: resolvedCount }; }, }; } // ── Service factory ──────────────────────────────────────────────── export function createAlertWebhookService(config, registerHttpRoute) { const store = createAlertStore(); const service = { id: "grafana-lens-alert-webhook", async start(ctx) { if (!config.proactive?.enabled) { ctx.logger.info("grafana-lens: alert webhook disabled (proactive.enabled = false)"); return; } const webhookPath = config.proactive?.webhookPath ?? "/grafana-lens/alerts"; registerHttpRoute({ path: webhookPath, auth: "gateway", handler: async (req, res) => { const httpReq = req; // Only accept POST if (httpReq.method !== "POST") { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Method not allowed" })); return; } // Collect request body const chunks = []; await new Promise((resolve, reject) => { httpReq.on("data", (chunk) => { chunks.push(Buffer.from(chunk)); }); httpReq.on("end", () => resolve()); httpReq.on("error", (err) => reject(err)); }); try { const body = Buffer.concat(chunks).toString("utf-8"); const notification = JSON.parse(body); // Validate minimal fields if (!notification.status || !Array.isArray(notification.alerts)) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Invalid alert notification payload" })); return; } const stored = store.addAlert(notification); ctx.logger.info(`grafana-lens: received alert webhook — ${notification.status}: ${notification.title} (${notification.alerts.length} instances, id: ${stored.id})`); // Return 200 immediately (Grafana expects fast responses) res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "received", id: stored.id })); } catch (err) { ctx.logger.error(`grafana-lens: failed to parse alert webhook: ${err}`); res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Failed to parse notification payload" })); } }, }); ctx.logger.info(`grafana-lens: alert webhook handler started (${webhookPath})`); }, }; return { service, store }; }