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

114 lines (113 loc) 5.46 kB
/** * Pure utility functions shared between the alloy_pipeline tool and its tests. * * Extracted here so tests import the real implementations instead of * duplicating them (maintenance risk if the tool's version diverges). */ // ── Component ID Extraction ─────────────────────────────────────── /** * Extract Alloy component IDs from raw River config. * Matches patterns like: `type.subtype "label" {` or `type.subtype.sub2 "label" {` * Returns component IDs in the format Alloy uses: `type.subtype.label` */ export function extractComponentIds(config) { const ids = []; const re = /^([a-zA-Z_]\w+(?:\.[a-zA-Z_]\w+)+)\s+"([^"]+)"\s*\{/gm; let match; while ((match = re.exec(config)) !== null) { ids.push(`${match[1]}.${match[2]}`); } return ids; } // ── Signal Inference ────────────────────────────────────────────── /** * Infer signal type from raw Alloy config component types. * loki/faro → logs, otelcol+traces/tempo → traces, pyroscope → profiles, else metrics. */ export function inferSignalFromConfig(config) { const hasLoki = /\bloki\.\w+/.test(config); const hasFaro = /\bfaro\.receiver\b/.test(config); const hasTraces = /\botelcol\.(?:receiver|processor|exporter|connector)\.\w+[\s\S]*?traces\s*=/.test(config) || /\btempo\b/.test(config); const hasProfiles = /\bpyroscope\.\w+/.test(config); if (hasLoki || hasFaro) return "logs"; if (hasTraces) return "traces"; if (hasProfiles) return "profiles"; return "metrics"; } // ── Signal → Query Tool Mapping ─────────────────────────────────── /** Map signal type to the appropriate query tool name. */ export function queryToolForSignal(signal) { if (signal === "logs") return "grafana_query_logs"; if (signal === "traces") return "grafana_query_traces"; if (signal === "profiles") return "grafana_explore_datasources"; return "grafana_query"; } /** Map signal type to the query type key used in response shapes. */ export function queryTypeForSignal(signal) { if (signal === "logs") return "logs"; if (signal === "traces") return "traces"; return "metrics"; } // ── Suggested Workflow ──────────────────────────────────────────── /** * Build a basic suggestedWorkflow for raw-config pipelines based on signal type. */ export function rawConfigSuggestedWorkflow(pipelineName, signal, sampleQueries) { const firstQuery = sampleQueries ? Object.values(sampleQueries)[0] : undefined; const tool = queryToolForSignal(signal); const isProfiles = signal === "profiles"; return [ { tool: "alloy_pipeline", action: "Verify data is flowing", example: { action: "status", name: pipelineName }, }, isProfiles ? { tool, action: "Verify Pyroscope datasource available", example: {} } : tool === "grafana_query_traces" ? { tool, action: "Search collected traces", example: firstQuery ? { query: firstQuery } : { query: '{ }' } } : tool === "grafana_query_logs" ? { tool, action: "Search collected logs", example: firstQuery ? { expr: firstQuery } : { expr: '{source="..."}' } } : { tool, action: "Query collected metrics", example: firstQuery ? { expr: firstQuery } : { expr: 'up{job="..."}' } }, ]; } // ── Credential Redaction ────────────────────────────────────────── /** Redact sensitive params for storage (does not mutate original). */ export function redactSecrets(params, recipe) { const redacted = { ...params }; for (const name of recipe.credentialParams) { if (redacted[name]) { redacted[name] = "***REDACTED***"; } } return redacted; } // ── Raw Config Credential Scanning ────────────────────────────── /** URI schemes with embedded credentials (user:pass@host). */ const RE_URI_CREDS = /(postgres|mysql|mongodb|redis|amqp):\/\/[^:]+:[^@]+@/g; /** Credential keywords as bare double-quoted string values, NOT inside sys.env(). */ const RE_CRED_KEYWORDS = /\b(password|secret|token|api_key|apikey)\s*=\s*"(?!sys\.env)[^"]+"/gi; /** * Scan raw Alloy config for likely plaintext credentials. * Returns advisory warnings (not errors) — the config is still written. * Only flags credentials that are NOT already wrapped in sys.env(). */ export function scanForCredentials(config) { const warnings = []; for (const m of config.matchAll(RE_URI_CREDS)) { warnings.push(`Config contains a ${m[1]}:// URI with embedded credentials. Use sys.env("MY_VAR") instead, or use the matching recipe which handles this automatically.`); } for (const m of config.matchAll(RE_CRED_KEYWORDS)) { warnings.push(`Config contains '${m[1]}' as a plaintext string value. Wrap sensitive values in sys.env("ENV_VAR_NAME") so they aren't written to disk.`); } return warnings; }