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