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

222 lines (220 loc) 8.16 kB
/** * Shared loki.process stage builder for all log recipes. * Stage order: json → regex → timestamp → labels → structured_metadata → static_labels → tenant → match → output. */ import { componentLabel, escapeString } from "../../config-builder.js"; /** * Build a loki.process config block from declarative parameters. * Returns null if no processing params are set (backwards compatible). * * @param forwardTo — The Alloy receiver to forward processed logs to, * e.g., "loki.write.lens_abc_write.receiver" */ export function buildProcessBlock(pipelineId, params, forwardTo) { const stages = []; // Stage ordering: json → regex → timestamp → labels → structured_metadata → static_labels → tenant → match → output if (params.jsonExpressions && Object.keys(params.jsonExpressions).length > 0) { stages.push(buildJsonStage(params.jsonExpressions)); } if (params.regexExpression) { stages.push(buildRegexStage(params.regexExpression)); } if (params.timestampSource) { stages.push(buildTimestampStage(params.timestampSource, params.timestampFormat || "RFC3339")); } if (params.labelFields && Object.keys(params.labelFields).length > 0) { stages.push(buildValuesStage("labels", params.labelFields)); } if (params.structuredMetadata && Object.keys(params.structuredMetadata).length > 0) { stages.push(buildValuesStage("structured_metadata", params.structuredMetadata)); } if (params.staticLabels && Object.keys(params.staticLabels).length > 0) { stages.push(buildValuesStage("static_labels", params.staticLabels)); } if (params.tenantValue) { stages.push(buildTenantStage({ value: params.tenantValue })); } else if (params.tenantSource) { stages.push(buildTenantStage({ source: params.tenantSource })); } if (params.matchRoutes && params.matchRoutes.length > 0) { for (const route of params.matchRoutes) { stages.push(buildMatchStage(route)); } } if (params.outputSource) { stages.push(buildOutputStage(params.outputSource)); } // No stages → no processing needed if (stages.length === 0) return null; const label = componentLabel(pipelineId, "process"); const block = `loki.process "${label}" { ${stages.join("\n\n")} forward_to = [${forwardTo}] }`; return { block, componentId: `loki.process.${label}`, receiverRef: `loki.process.${label}.receiver`, }; } /** * Check if any processing params are set. * Useful for recipes that need to conditionally add processing params to optionalParams. */ export function hasProcessingParams(params) { return !!(params.jsonExpressions || params.regexExpression || params.timestampSource || params.labelFields || params.structuredMetadata || params.staticLabels || params.tenantValue || params.tenantSource || params.matchRoutes || params.outputSource); } /** * Extract LogProcessingParams from a raw params object. * Picks only the processing-related fields. */ export function extractProcessingParams(params) { return { jsonExpressions: params.jsonExpressions, regexExpression: params.regexExpression, timestampSource: params.timestampSource, timestampFormat: params.timestampFormat, labelFields: params.labelFields, structuredMetadata: params.structuredMetadata, staticLabels: params.staticLabels, tenantValue: params.tenantValue, tenantSource: params.tenantSource, matchRoutes: params.matchRoutes, outputSource: params.outputSource, }; } /** * Common optional param definitions for log processing. * Import and spread into any log recipe's optionalParams array. */ export const PROCESSING_OPTIONAL_PARAMS = [ { name: "jsonExpressions", type: "object", description: "JSON field extraction. Keys=output names, values=JSON paths (empty string=same name). E.g., { level: '', request_id: 'ctx.rid' }", }, { name: "regexExpression", type: "string", description: "Regex with named capture groups for field extraction. E.g., '^(?P<ts>\\\\S+) (?P<level>\\\\w+)'", }, { name: "timestampSource", type: "string", description: "Extract timestamps from this field name (use with timestampFormat)", }, { name: "timestampFormat", type: "string", description: "Timestamp format: RFC3339, RFC3339Nano, Unix, UnixMs, or Go layout", default: "RFC3339", }, { name: "labelFields", type: "object", description: "Promote fields to Loki labels. Keys=label names, values=source fields (empty=same). Use sparingly for low-cardinality fields", }, { name: "structuredMetadata", type: "object", description: "Store high-cardinality fields as structured metadata (queryable, not indexed). Preferred for request_id, user_id, etc.", }, { name: "staticLabels", type: "object", description: "Add fixed labels to all entries. E.g., { environment: 'production' }", }, { name: "tenantValue", type: "string", description: "Static Loki tenant ID (X-Scope-OrgID). Routes all logs to this tenant", }, { name: "tenantSource", type: "string", description: "Dynamic Loki tenant ID — reads tenant from this extracted field name", }, { name: "matchRoutes", type: "object", description: "Multi-tenant routing: array of {selector, tenantValue?, tenantSource?, pipelineName?}. " + "Each route matches logs by LogQL selector and applies nested tenant/stages. " + 'E.g., [{selector: \'{env="prod"}\', tenantValue: "prod-tenant"}]', }, { name: "outputSource", type: "string", description: "Replace log line with this extracted field's value (e.g., 'message')", }, ]; // ── Stage Builders ───────────────────────────────────────────────── function buildJsonStage(expressions) { const entries = Object.entries(expressions) .map(([key, val]) => ` "${escapeString(key)}" = "${escapeString(val)}"`) .join(",\n"); return ` stage.json { expressions = { ${entries}, } }`; } function buildRegexStage(expression) { return ` stage.regex { expression = "${escapeString(expression)}" }`; } function buildTimestampStage(source, format) { return ` stage.timestamp { source = "${escapeString(source)}" format = "${escapeString(format)}" }`; } /** Shared builder for stage.labels, stage.structured_metadata, and stage.static_labels. */ function buildValuesStage(stageName, values) { const entries = Object.entries(values) .map(([key, val]) => ` "${escapeString(key)}" = "${escapeString(val)}"`) .join(",\n"); return ` stage.${stageName} { values = { ${entries}, } }`; } function buildOutputStage(source) { return ` stage.output { source = "${escapeString(source)}" }`; } function buildTenantStage(opts, indent = " ") { if (opts.value) { return `${indent}stage.tenant {\n${indent} value = "${escapeString(opts.value)}"\n${indent}}`; } return `${indent}stage.tenant {\n${indent} source = "${escapeString(opts.source)}"\n${indent}}`; } function buildMatchStage(route) { const lines = []; lines.push(` stage.match {`); lines.push(` selector = "${escapeString(route.selector)}"`); if (route.pipelineName) { lines.push(` pipeline_name = "${escapeString(route.pipelineName)}"`); } if (route.tenantValue) { lines.push(buildTenantStage({ value: route.tenantValue }, " ")); } else if (route.tenantSource) { lines.push(buildTenantStage({ source: route.tenantSource }, " ")); } lines.push(` }`); return lines.join("\n"); }