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
251 lines (248 loc) • 11.9 kB
JavaScript
import { AlloyConfigBuilder, componentLabel, escapeString, renderValue } from "../../config-builder.js";
/**
* Generate Alloy config for a single sampling policy block.
*/
function renderPolicy(policy) {
const lines = [];
lines.push(` policy {`);
lines.push(` name = "${escapeString(policy.name)}"`);
lines.push(` type = "${escapeString(policy.type)}"`);
switch (policy.type) {
case "status_code":
lines.push(` status_code {`);
lines.push(` status_codes = ${renderValue(policy.statusCodes)}`);
lines.push(` }`);
break;
case "latency":
lines.push(` latency {`);
lines.push(` threshold_ms = ${policy.thresholdMs}`);
lines.push(` }`);
break;
case "probabilistic":
lines.push(` probabilistic {`);
lines.push(` sampling_percentage = ${policy.samplingPercentage}`);
lines.push(` }`);
break;
case "string_attribute":
lines.push(` string_attribute {`);
lines.push(` key = "${escapeString(policy.key)}"`);
lines.push(` values = ${renderValue(policy.values)}`);
if (policy.invertMatch) {
lines.push(` invert_match = true`);
}
lines.push(` }`);
break;
case "numeric_attribute":
lines.push(` numeric_attribute {`);
lines.push(` key = "${escapeString(policy.key)}"`);
lines.push(` min_value = ${policy.minValue}`);
lines.push(` max_value = ${policy.maxValue}`);
lines.push(` }`);
break;
}
lines.push(` }`);
return lines.join("\n");
}
/**
* Validate a raw samplingPolicies array from params.
* Throws descriptive errors for the agent to relay.
*/
function validatePolicies(raw) {
if (!Array.isArray(raw)) {
throw new Error("samplingPolicies must be an array of policy objects");
}
if (raw.length === 0) {
throw new Error("samplingPolicies must contain at least one policy");
}
const validTypes = ["status_code", "latency", "probabilistic", "string_attribute", "numeric_attribute"];
return raw.map((p, i) => {
if (!p || typeof p !== "object") {
throw new Error(`samplingPolicies[${i}]: each policy must be an object`);
}
const policy = p;
if (!policy.name || typeof policy.name !== "string") {
throw new Error(`samplingPolicies[${i}]: 'name' is required (string)`);
}
if (!policy.type || typeof policy.type !== "string" || !validTypes.includes(policy.type)) {
throw new Error(`samplingPolicies[${i}]: 'type' must be one of: ${validTypes.join(", ")}`);
}
const name = policy.name;
switch (policy.type) {
case "status_code":
if (!Array.isArray(policy.statusCodes) || policy.statusCodes.length === 0) {
throw new Error(`samplingPolicies[${i}] (status_code): 'statusCodes' required (e.g., ["ERROR"])`);
}
return { name, type: "status_code", statusCodes: policy.statusCodes };
case "latency":
if (typeof policy.thresholdMs !== "number" || policy.thresholdMs <= 0) {
throw new Error(`samplingPolicies[${i}] (latency): 'thresholdMs' required (positive number, e.g., 5000)`);
}
return { name, type: "latency", thresholdMs: policy.thresholdMs };
case "probabilistic":
if (typeof policy.samplingPercentage !== "number" || policy.samplingPercentage < 0 || policy.samplingPercentage > 100) {
throw new Error(`samplingPolicies[${i}] (probabilistic): 'samplingPercentage' required (0-100)`);
}
return { name, type: "probabilistic", samplingPercentage: policy.samplingPercentage };
case "string_attribute":
if (!policy.key || typeof policy.key !== "string") {
throw new Error(`samplingPolicies[${i}] (string_attribute): 'key' required (string)`);
}
if (!Array.isArray(policy.values) || policy.values.length === 0) {
throw new Error(`samplingPolicies[${i}] (string_attribute): 'values' required (string[])`);
}
return { name, type: "string_attribute", key: policy.key, values: policy.values, invertMatch: !!policy.invertMatch };
case "numeric_attribute":
if (!policy.key || typeof policy.key !== "string") {
throw new Error(`samplingPolicies[${i}] (numeric_attribute): 'key' required (string)`);
}
if (typeof policy.minValue !== "number" || typeof policy.maxValue !== "number") {
throw new Error(`samplingPolicies[${i}] (numeric_attribute): 'minValue' and 'maxValue' required (numbers)`);
}
return { name, type: "numeric_attribute", key: policy.key, minValue: policy.minValue, maxValue: policy.maxValue };
default:
throw new Error(`samplingPolicies[${i}]: unknown type '${policy.type}'`);
}
});
}
// ── Recipe ─────────────────────────────────────────────────────────
const recipe = {
name: "application-traces",
category: "traces",
signal: "traces",
summary: "Receive application traces with service name enrichment, batching, and sampling. " +
"Simple mode: sampleRate (0-1) for probabilistic sampling. " +
"Advanced mode: samplingPolicies array for multi-policy tail sampling (keep errors, keep slow traces, filter health checks, rate-limit the rest)",
dashboardTemplate: null,
credentialParams: [],
requiredParams: [],
optionalParams: [
{ name: "serviceName", type: "string", description: "Service name to set on all spans" },
{ name: "environment", type: "string", description: "Environment label (e.g., production, staging)", default: "production" },
{ name: "sampleRate", type: "number", description: "Simple sampling: rate 0.0-1.0 (1.0 = keep all). Ignored when samplingPolicies is set", default: 1.0 },
{
name: "samplingPolicies",
type: "object",
description: "Advanced multi-policy tail sampling. Array of policy objects. " +
"Types: status_code ({statusCodes}), latency ({thresholdMs}), probabilistic ({samplingPercentage}), " +
"string_attribute ({key, values, invertMatch?}), numeric_attribute ({key, minValue, maxValue}). " +
"Policies are OR-ed: trace is kept if ANY policy matches. Place specific policies before catch-all.",
example: '[{"name":"keep-errors","type":"status_code","statusCodes":["ERROR"]},{"name":"sample-rest","type":"probabilistic","samplingPercentage":10}]',
},
{ name: "decisionWait", type: "string", description: "How long to wait for spans before sampling decision (only with samplingPolicies)", default: "10s" },
{ name: "numTraces", type: "number", description: "Max traces held in memory for sampling decision (only with samplingPolicies)", default: 100 },
],
generateConfig(pipelineId, params, targets, _pipelineName) {
const environment = params.environment || "production";
const sampleRate = params.sampleRate ?? 1.0;
const rawPolicies = params.samplingPolicies;
const decisionWait = params.decisionWait || "10s";
const numTraces = params.numTraces ?? 100;
// Determine sampling mode: multi-policy takes precedence over sampleRate
const policies = rawPolicies ? validatePolicies(rawPolicies) : null;
const useSimpleSampling = !policies && sampleRate > 0 && sampleRate < 1.0;
const useSampling = !!policies || useSimpleSampling;
const recvLabel = componentLabel(pipelineId, "otlp");
const attrLabel = componentLabel(pipelineId, "attrs");
const samplerLabel = componentLabel(pipelineId, "sampler");
const batchLabel = componentLabel(pipelineId, "batch");
const expLabel = componentLabel(pipelineId, "export");
const builder = new AlloyConfigBuilder();
// Receiver → attributes enrichment
builder.addBlock(`otelcol.receiver.otlp "${recvLabel}" {
grpc {
endpoint = "0.0.0.0:4317"
}
http {
endpoint = "0.0.0.0:4318"
}
output {
traces = [otelcol.processor.attributes.${attrLabel}.input]
}
}`);
// Attributes → sampling (if enabled) or batch
const attrNext = useSampling
? `otelcol.processor.tail_sampling.${samplerLabel}.input`
: `otelcol.processor.batch.${batchLabel}.input`;
builder.addBlock(`otelcol.processor.attributes "${attrLabel}" {
action {
key = "deployment.environment"
value = "${escapeString(environment)}"
action = "upsert"
}
output {
traces = [${attrNext}]
}
}`);
// Tail sampling — multi-policy or simple probabilistic
if (policies) {
const policyBlocks = policies.map(renderPolicy).join("\n\n");
builder.addBlock(`otelcol.processor.tail_sampling "${samplerLabel}" {
decision_wait = "${escapeString(decisionWait)}"
num_traces = ${numTraces}
${policyBlocks}
output {
traces = [otelcol.processor.batch.${batchLabel}.input]
}
}`);
}
else if (useSimpleSampling) {
const pct = Math.round(sampleRate * 100);
builder.addBlock(`otelcol.processor.tail_sampling "${samplerLabel}" {
decision_wait = "10s"
policy {
name = "probabilistic"
type = "probabilistic"
probabilistic {
sampling_percentage = ${pct}
}
}
output {
traces = [otelcol.processor.batch.${batchLabel}.input]
}
}`);
}
builder.addBlock(`otelcol.processor.batch "${batchLabel}" {
output {
traces = [otelcol.exporter.otlphttp.${expLabel}.input]
}
}`);
builder.addBlock(`otelcol.exporter.otlphttp "${expLabel}" {
client {
endpoint = "${escapeString(targets.otlpEndpoint)}"
}
}`);
return builder.build(pipelineId, "application-traces", "application-traces");
},
sampleQueries(params) {
const env = params.environment || "production";
const queries = {
traces: `{ resource.deployment.environment = "${env}" }`,
errorTraces: `{ resource.deployment.environment = "${env}" && status = error }`,
};
// Add policy-specific queries when using multi-policy sampling
const rawPolicies = params.samplingPolicies;
if (rawPolicies && Array.isArray(rawPolicies)) {
for (const p of rawPolicies) {
const policy = p;
if (policy.type === "latency" && typeof policy.thresholdMs === "number") {
queries.slowTraces = `{ resource.deployment.environment = "${env}" && duration > ${policy.thresholdMs}ms }`;
}
}
}
return queries;
},
componentIds(pipelineId) {
return [
`otelcol.receiver.otlp.${componentLabel(pipelineId, "otlp")}`,
`otelcol.processor.attributes.${componentLabel(pipelineId, "attrs")}`,
// Sampler is conditionally present — included here always for health checking.
// If not in config, Alloy simply won't report it (missing != unhealthy).
`otelcol.processor.batch.${componentLabel(pipelineId, "batch")}`,
`otelcol.exporter.otlphttp.${componentLabel(pipelineId, "export")}`,
];
},
boundPorts() {
return [4317, 4318];
},
};
export default recipe;