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

156 lines (155 loc) 6.59 kB
/** * grafana_search tool * * Search existing dashboards in Grafana by title or tag. The agent uses * this to check for existing dashboards before creating duplicates, or * to find a dashboard the user mentions by name. * * Optional enrichment (enrich: true) fetches dashboard details in parallel * to add updatedAt and panelCount — useful for reporting workflows. */ import { jsonResult, readNumberParam, readStringParam } from "../sdk-compat.js"; import { instanceProperties } from "./instance-param.js"; /** * Extract updatedAt and panelCount from a full dashboard response. */ function extractDashboardMeta(full) { const meta = full.meta; const dashboard = full.dashboard; const updatedAt = typeof meta?.updated === "string" ? meta.updated : undefined; const panels = Array.isArray(dashboard?.panels) ? dashboard.panels : undefined; return { updatedAt, panelCount: panels?.length, }; } /** * Build a search result entry — always includes folder info when available. * When enrichDetails is provided (from getDashboard), adds updatedAt + panelCount. */ function buildResultEntry(d, dashboardUrl, enrichDetails) { const entry = { uid: d.uid, title: d.title, url: dashboardUrl, tags: d.tags, }; // Always include folder info when available (free from search API) if (d.folderTitle) entry.folderTitle = d.folderTitle; if (d.folderUid) entry.folderUid = d.folderUid; // Enriched fields from getDashboard if (enrichDetails) { if (enrichDetails.updatedAt) entry.updatedAt = enrichDetails.updatedAt; if (enrichDetails.panelCount !== undefined) entry.panelCount = enrichDetails.panelCount; } return entry; } /** Max concurrent getDashboard requests during enrichment. */ const ENRICH_CONCURRENCY = 10; /** * Fetch dashboard details in batches to avoid overwhelming Grafana. */ async function fetchDetailsInBatches(uids, fetcher) { const results = []; for (let i = 0; i < uids.length; i += ENRICH_CONCURRENCY) { const batch = uids.slice(i, i + ENRICH_CONCURRENCY); const batchResults = await Promise.allSettled(batch.map(fetcher)); results.push(...batchResults); } return results; } export function createSearchToolFactory(registry) { return (_ctx) => ({ name: "grafana_search", label: "Search Dashboards", description: [ "Search for existing dashboards in Grafana by title or tags.", "WORKFLOW: Use before creating a new dashboard to avoid duplicates.", "Also use when user refers to a dashboard by name ('show me my cost dashboard').", "Returns dashboard UIDs, URLs, and folder info for use with other tools.", "Set enrich=true for reporting workflows — adds updatedAt and panelCount per dashboard (extra API calls).", "After finding a dashboard, use grafana_get_dashboard to inspect panels, grafana_share_dashboard to render a chart, or grafana_update_dashboard to modify it.", ].join(" "), parameters: { type: "object", properties: { ...instanceProperties(registry), query: { type: "string", description: "Search query (matches dashboard titles, e.g., 'cost', 'agent overview')", }, tags: { type: "array", items: { type: "string" }, description: "Filter by dashboard tags (e.g., ['production', 'api'])", }, starred: { type: "boolean", description: "Only return starred dashboards", }, sort: { type: "string", enum: ["alpha-asc", "alpha-desc"], description: "Sort order for results", }, limit: { type: "number", description: "Max results to return (default: 100)", }, enrich: { type: "boolean", description: "Fetch dashboard details to add updatedAt and panelCount per result. " + "Use for reporting/audit workflows. Adds one API call per result (default: false)", }, }, required: ["query"], }, async execute(_toolCallId, params) { const client = registry.get(readStringParam(params, "instance")); const query = readStringParam(params, "query", { required: true, label: "Search query" }); const tags = params.tags; const starred = params.starred === true ? true : undefined; const sort = readStringParam(params, "sort"); const limit = readNumberParam(params, "limit"); const enrich = params.enrich === true; try { const results = await client.searchDashboards(query, { tags, starred, sort: sort ?? undefined, limit, }); let dashboards; if (enrich && results.length > 0) { // Batched parallel fetch — max ENRICH_CONCURRENCY at a time const details = await fetchDetailsInBatches(results.map((d) => d.uid), (uid) => client.getDashboard(uid)); dashboards = results.map((d, i) => { const detail = details[i]; const enrichDetails = detail.status === "fulfilled" ? extractDashboardMeta(detail.value) : undefined; return buildResultEntry(d, client.dashboardUrl(d.uid), enrichDetails); }); } else { dashboards = results.map((d) => buildResultEntry(d, client.dashboardUrl(d.uid))); } const response = { status: "success", count: results.length, enriched: enrich, dashboards, }; return jsonResult(response); } catch (err) { const reason = err instanceof Error ? err.message : String(err); return jsonResult({ error: `Search failed: ${reason}` }); } }, }); }