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
JavaScript
/**
* 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 };
}