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

130 lines (129 loc) 5.46 kB
/** * Panel query resolution utility. * * Extracts the query expression and datasource from an existing dashboard * panel, so grafana_query / grafana_query_logs can re-run it with different * time ranges without the agent navigating panel JSON manually. */ import { getQueryCapability } from "./explore-datasources.js"; // ── Helpers ──────────────────────────────────────────────────────────── /** * Resolve a panel's effective datasource UID. * * Panel datasource references can be: * - Concrete: `{ type: "prometheus", uid: "abc123" }` * - Template variable: `{ type: "prometheus", uid: "$prometheus" }` * - Missing/default: `null` or `{}` (uses Grafana's default datasource) */ function resolveDatasourceUid(panelDs, datasources) { if (!panelDs || typeof panelDs !== "object") { // No datasource specified — use the first available Prometheus datasource as default const defaultDs = datasources.find((d) => d.isDefault) ?? datasources.find((d) => d.type === "prometheus"); return defaultDs ? { uid: defaultDs.uid, type: defaultDs.type } : null; } const ds = panelDs; const uid = ds.uid; const dsType = ds.type; if (!uid) return null; // Concrete UID — look up its type if (!uid.startsWith("$")) { const found = datasources.find((d) => d.uid === uid); return found ? { uid: found.uid, type: found.type } : (dsType ? { uid, type: dsType } : null); } // Template variable (e.g. $prometheus, $loki) — resolve by type if (dsType) { const found = datasources.find((d) => d.type === dsType); if (found) return { uid: found.uid, type: found.type }; } return null; } /** * Replace Grafana template variables in expressions with wildcards. * Returns whether any replacements were made. * * `$__range`, `$__rate_interval`, `$__interval` → `5m` (safe default) * `$variable` and `${variable}` → `.*` (match any label value) */ function replaceTemplateVars(expr) { let replaced = false; const result = expr .replace(/\$__(?:range|rate_interval|interval)/g, () => { replaced = true; return "5m"; }) .replace(/\$\{[a-zA-Z_]\w*\}/g, () => { replaced = true; return ".*"; }) .replace(/\$[a-zA-Z_]\w*/g, () => { replaced = true; return ".*"; }); return { result, replaced }; } // ── Main resolution function ─────────────────────────────────────────── /** * Resolve a dashboard panel's query expression and datasource. * * Fetches the dashboard, finds the panel by ID, extracts `targets[0].expr`, * resolves the datasource UID (handling template variables), and determines * which query tool to use based on datasource type. */ export async function resolvePanelQuery(client, dashboardUid, panelId) { // Fetch dashboard and datasources in parallel let dashboard; let datasources; try { const [dashData, dsList] = await Promise.all([ client.getDashboard(dashboardUid), client.listDatasources(), ]); dashboard = dashData.dashboard; datasources = dsList; } catch (err) { const reason = err instanceof Error ? err.message : String(err); return { error: `Dashboard '${dashboardUid}' not found: ${reason}` }; } // Find panel const panels = dashboard.panels ?? []; const panel = panels.find((p) => p.id === panelId); if (!panel) { const availableIds = panels.map((p) => `${p.id} (${p.title})`).join(", "); return { error: `Panel ${panelId} not found in dashboard '${dashboardUid}'. Available panels: ${availableIds}`, }; } // Extract query expression const targets = panel.targets ?? []; const firstTarget = targets[0]; if (!firstTarget) { return { error: `Panel ${panelId} ('${panel.title}') has no query targets. It may be a text/row panel.`, }; } const rawExpr = firstTarget.expr ?? firstTarget.query; if (!rawExpr) { return { error: `Panel ${panelId} ('${panel.title}') has no 'expr' or 'query' in its first target.`, }; } // Resolve datasource from panel config against known datasources const resolved = resolveDatasourceUid(panel.datasource, datasources); if (!resolved) { return { error: `Could not resolve datasource for panel ${panelId} ('${panel.title}'). Use grafana_explore_datasources to find the correct datasourceUid and pass it explicitly.`, }; } // Determine query tool const cap = getQueryCapability(resolved.type); if (!cap.supported) { return { error: `Panel ${panelId} uses datasource type '${resolved.type}' which is not supported by grafana_query, grafana_query_logs, or grafana_query_traces. Use Grafana UI to view this panel.`, }; } // Replace template variables const { result: expr, replaced } = replaceTemplateVars(rawExpr); return { expr, datasourceUid: resolved.uid, datasourceType: resolved.type, queryTool: cap.queryTool, panelTitle: panel.title ?? "Untitled", panelType: panel.type ?? "unknown", templateVarsReplaced: replaced, }; }