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