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

189 lines (188 loc) 8.22 kB
/** * SDK compatibility layer — vendored utilities + resilient import resolution. * * Pure utility functions (jsonResult, readStringParam, readNumberParam) were * removed from the root `openclaw/plugin-sdk` in OpenClaw 2026.3.16 * (commit f2bd76cd1a "finalize plugin sdk legacy boundary cleanup"). * These are vendored locally (ponyfill pattern) — zero SDK dependency. * * SDK hooks (onDiagnosticEvent, registerLogTransport) can't be vendored since * they connect to openclaw's internal event bus. These are resolved via dynamic * import fallback chains — tries new subpaths first, falls back to root. * * All SDK coupling lives in this one file. Future breakage = one-file fix. */ // ── camelCase → snake_case key resolution ────────────────────────── function toSnakeCaseKey(key) { return key .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .toLowerCase(); } function readParamRaw(params, key) { if (Object.hasOwn(params, key)) return params[key]; const snakeKey = toSnakeCaseKey(key); if (snakeKey !== key && Object.hasOwn(params, snakeKey)) return params[snakeKey]; return undefined; } // ── ToolInputError ──────────────────────────────────────────────── class ToolInputError extends Error { status = 400; constructor(message) { super(message); this.name = "ToolInputError"; } } export function readStringParam(params, key, options = {}) { const { required = false, trim = true, label = key, allowEmpty = false } = options; const raw = readParamRaw(params, key); if (typeof raw !== "string") { if (required) throw new ToolInputError(`${label} required`); return undefined; } const value = trim ? raw.trim() : raw; if (!value && !allowEmpty) { if (required) throw new ToolInputError(`${label} required`); return undefined; } return value; } export function readNumberParam(params, key, options = {}) { const { required = false, label = key, integer = false, strict = false } = options; const raw = readParamRaw(params, key); let value; if (typeof raw === "number" && Number.isFinite(raw)) { value = raw; } else if (typeof raw === "string") { const trimmed = raw.trim(); if (trimmed) { const parsed = strict ? Number(trimmed) : Number.parseFloat(trimmed); if (Number.isFinite(parsed)) value = parsed; } } if (value === undefined) { if (required) throw new ToolInputError(`${label} required`); return undefined; } return integer ? Math.trunc(value) : value; } // ── Result formatter ────────────────────────────────────────────── export function jsonResult(payload) { return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], details: payload, }; } // ── Error formatting ────────────────────────────────────────────── export function getErrorMessage(err) { return err instanceof Error ? err.message : String(err); } // ── Process-global singleton helper ─────────────────────────────── /** * Resolve or create a process-wide singleton keyed by a symbol on globalThis. * * Mirrors `resolveGlobalSingleton` from `openclaw/plugin-sdk/global-singleton` * (the canonical SDK helper). We vendor it because openclaw 2026.4.25 declares * the subpath in `package.json` exports but doesn't ship `dist/.../global-singleton.js`, * so a direct import would crash at runtime. When upstream fixes the packaging, * this helper can be swapped for `import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton"`. */ export function resolveGlobalSingleton(key, create) { const store = globalThis; if (Object.prototype.hasOwnProperty.call(store, key)) { return store[key]; } const created = create(); store[key] = created; return created; } /** * Test-only: drop a singleton entry created by `resolveGlobalSingleton`. * Production code must not call this — it bypasses the re-register protection * the singleton exists to provide. */ export function clearGlobalSingletonForTests(key) { const store = globalThis; delete store[key]; } /** * Resolve SDK diagnostic hooks from whichever subpath is available. * * Order matters: * 1. `plugin-sdk/diagnostic-runtime` — canonical scoped subpath (openclaw >= 2026.5.0). * Explicit migration target named in `plugin-sdk/compat`'s deprecation warning. * Present in every 2026.5.x release. * 2. `plugin-sdk` root — re-exports `onDiagnosticEvent` for older openclaw versions. * May or may not work on 2026.5.5+ depending on dist bundling. * * Failures emit `logger.warn` with the import path + Node error code (e.g. * `ERR_MODULE_NOT_FOUND`) so operators see the actionable reason in the * gateway log, not a generic "not available" line. * * The `GRAFANA_LENS_DEBUG_SDK=1` env var continues to mirror failures to * `console.warn` for environments where no logger is wired (tests, scripts). */ export async function resolveDiagnosticHooks(logger) { const hooks = { onDiagnosticEvent: null, onInternalDiagnosticEvent: null, registerLogTransport: null, }; const paths = [ "openclaw/plugin-sdk/diagnostic-runtime", "openclaw/plugin-sdk", ]; for (const p of paths) { try { const m = await import(p); if (typeof m.onDiagnosticEvent === "function") { if (!hooks.onDiagnosticEvent) { logger?.debug?.(`grafana-lens: sdk-compat: resolved onDiagnosticEvent from ${p}`); } hooks.onDiagnosticEvent ??= m.onDiagnosticEvent; } if (typeof m.onInternalDiagnosticEvent === "function") { if (!hooks.onInternalDiagnosticEvent) { logger?.debug?.(`grafana-lens: sdk-compat: resolved onInternalDiagnosticEvent from ${p}`); } hooks.onInternalDiagnosticEvent ??= m.onInternalDiagnosticEvent; } if (typeof m.registerLogTransport === "function") { hooks.registerLogTransport ??= m.registerLogTransport; } } catch (err) { const code = err?.code ?? "import-error"; const rawMsg = getErrorMessage(err); const msg = rawMsg.length > 200 ? `${rawMsg.slice(0, 197)}...` : rawMsg; const line = `grafana-lens: sdk-compat: import("${p}") failed: ${code}: ${msg}`; logger?.warn?.(line); if (process.env.GRAFANA_LENS_DEBUG_SDK) { // eslint-disable-next-line no-console console.warn(line); } } // registerLogTransport is permanently null at openclaw 2026.5.5+; only // onDiagnosticEvent gates the early break. if (hooks.onDiagnosticEvent) break; } // Warn when the unfiltered hook is unavailable but the public hook resolved // — on openclaw >= 2026.5.7 this means all model.usage-derived metrics will // silently stay empty. if (hooks.onDiagnosticEvent && !hooks.onInternalDiagnosticEvent) { logger?.warn?.("grafana-lens: sdk-compat: onInternalDiagnosticEvent not exported by installed openclaw — " + "model.usage-derived metrics (openclaw_lens_tokens, _cost_by_*, _context_tokens, _cache_*_ratio, " + "_daily_cost_usd, _cache_savings_usd, _session_latency_avg_ms) will be missing on openclaw >= 2026.5.7. " + "Report at https://github.com/awsome-o/grafana-lens/issues with your openclaw version."); } return hooks; }