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

180 lines (179 loc) 8.17 kB
/** * grafana_annotate tool * * Create or query annotations on Grafana dashboards. Annotations mark * events (deployments, incidents, config changes) that correlate with * metric changes visible on dashboards. */ import { jsonResult, readStringParam, readNumberParam } from "../sdk-compat.js"; import { parseDateMathToMs } from "../grafana-client.js"; import { instanceProperties } from "./instance-param.js"; /** * Resolve a time parameter that may be epoch ms (number) or a Grafana * relative time string like "now-7d" (string). Returns epoch ms. */ export function resolveTimeParam(value) { if (value === undefined || value === null) return undefined; if (typeof value === "number") return value; if (typeof value === "string") return parseDateMathToMs(value); return undefined; } /** Default comparison window: 30 minutes on each side of the annotation. */ const DEFAULT_COMPARISON_WINDOW_MS = 30 * 60 * 1000; /** * Build a comparisonHint for an annotation creation response. * * Provides ready-to-use time ranges for before/after comparison with * `grafana_query`, eliminating manual time math for the agent. * * For region annotations (time → timeEnd), the "before" window ends at * `time` and the "after" window starts at `timeEnd`. For point annotations, * both windows are symmetric around the annotation time. * * The `afterWindow.to` is capped at "now" if the annotation is recent. */ export function buildComparisonHint(time, timeEnd, windowMs = DEFAULT_COMPARISON_WINDOW_MS) { const now = Date.now(); const beforeFrom = time - windowMs; const beforeTo = time; const afterStart = timeEnd ?? time; const afterFrom = afterStart; const afterTo = Math.min(afterStart + windowMs, now); return { beforeWindow: { from: new Date(beforeFrom).toISOString(), to: new Date(beforeTo).toISOString(), }, afterWindow: { from: new Date(afterFrom).toISOString(), to: new Date(afterTo).toISOString(), }, suggestion: "Use grafana_query with these time ranges to compare metrics before vs. after. " + "Example: run the same PromQL expression twice with from/to set to each window.", }; } export function createAnnotateToolFactory(registry) { return (_ctx) => ({ name: "grafana_annotate", label: "Grafana Annotate", description: [ "Create or list annotations on Grafana dashboards.", "WORKFLOW: Use 'create' to mark events (deployments, incidents, config changes).", "Use 'list' to query recent events. Annotations appear as vertical lines on dashboards.", "Time params accept epoch ms OR relative strings: 'now', 'now-1h', 'now-7d', 'now-30m'.", "Tags help categorize — e.g., 'deploy', 'incident', 'config-change'.", "Create response includes comparisonHint with ready-to-use before/after time windows for grafana_query — no manual time math needed.", ].join(" "), parameters: { type: "object", properties: { ...instanceProperties(registry), action: { type: "string", enum: ["create", "list"], description: "Action to perform. Default: 'create'", }, text: { type: "string", description: "Annotation text (required for create). E.g., 'Deployed v2.1.0'", }, tags: { type: "array", items: { type: "string" }, description: "Tags for the annotation (e.g., ['deploy', 'production'])", }, dashboardUid: { type: "string", description: "Scope annotation to a specific dashboard (optional)", }, panelId: { type: "number", description: "Scope annotation to a specific panel (optional)", }, time: { type: ["number", "string"], description: "Annotation timestamp — epoch ms (e.g., 1700000000000) or relative string (e.g., 'now-2h'). Default: now", }, timeEnd: { type: ["number", "string"], description: "End timestamp for region annotations — epoch ms or relative string (e.g., 'now'). Creates a time range highlight", }, from: { type: ["number", "string"], description: "Query start time (for 'list') — epoch ms or relative string (e.g., 'now-7d', 'now-24h')", }, to: { type: ["number", "string"], description: "Query end time (for 'list') — epoch ms or relative string (e.g., 'now'). Default: now", }, limit: { type: "number", description: "Maximum annotations to return (for 'list' action). Default: 20", }, }, }, async execute(_toolCallId, params) { const client = registry.get(readStringParam(params, "instance")); const action = readStringParam(params, "action") ?? "create"; const tags = params.tags ?? []; try { if (action === "list") { const from = resolveTimeParam(params.from); const to = resolveTimeParam(params.to); const dashboardUid = readStringParam(params, "dashboardUid"); const panelId = readNumberParam(params, "panelId"); const limit = readNumberParam(params, "limit") ?? 20; const annotations = await client.getAnnotations({ from, to, dashboardUID: dashboardUid, panelId, tags: tags.length > 0 ? tags : undefined, limit, }); return jsonResult({ status: "success", count: annotations.length, annotations: annotations.map((a) => ({ id: a.id, text: a.text, tags: a.tags, time: new Date(a.time).toISOString(), timeEnd: a.timeEnd ? new Date(a.timeEnd).toISOString() : undefined, dashboardUID: a.dashboardUID || undefined, panelId: a.panelId || undefined, })), }); } // Create annotation const text = readStringParam(params, "text", { required: true, label: "Annotation text" }); const dashboardUid = readStringParam(params, "dashboardUid"); const panelId = readNumberParam(params, "panelId"); const time = resolveTimeParam(params.time) ?? Date.now(); const timeEnd = resolveTimeParam(params.timeEnd); const result = await client.createAnnotation({ text, tags, dashboardUID: dashboardUid, panelId, time, timeEnd, }); return jsonResult({ status: "created", id: result.id, message: `Annotation created: "${text}"`, time: new Date(time).toISOString(), comparisonHint: buildComparisonHint(time, timeEnd), }); } catch (err) { const reason = err instanceof Error ? err.message : String(err); return jsonResult({ error: `Annotation ${action} failed: ${reason}` }); } }, }); }