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