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
118 lines (114 loc) • 4.98 kB
JavaScript
import { AlloyConfigBuilder, componentLabel, escapeString, renderTargets } from "../../config-builder.js";
/**
* Valid pprof profile types for Go applications.
* Maps to pyroscope.scrape profiling_config blocks.
*/
const VALID_PROFILE_TYPES = ["cpu", "memory", "goroutine", "mutex", "block"];
/**
* Map our short names to Alloy's profiling_config block names.
* "cpu" → profile.process_cpu, rest → profile.{name}
*/
function profileBlockName(type) {
return type === "cpu" ? "profile.process_cpu" : `profile.${type}`;
}
function validateTargets(raw) {
if (!Array.isArray(raw) || raw.length === 0) {
throw new Error("continuous-profiling requires 'targets' — array of { address, serviceName }. " +
'Example: [{ "address": "myapp:6060", "serviceName": "my-app" }]');
}
return raw.map((t, i) => {
const obj = t;
if (!obj.address || typeof obj.address !== "string") {
throw new Error(`targets[${i}]: 'address' is required (e.g., "myapp:6060")`);
}
if (!obj.serviceName || typeof obj.serviceName !== "string") {
throw new Error(`targets[${i}]: 'serviceName' is required (e.g., "my-app")`);
}
return { address: obj.address, serviceName: obj.serviceName };
});
}
const recipe = {
name: "continuous-profiling",
category: "profiling",
signal: "profiles",
summary: "Continuous profiling via Pyroscope — scrape pprof profiles (CPU, memory, goroutine, mutex, block) from Go applications and send to Pyroscope",
dashboardTemplate: null,
credentialParams: [],
requiredParams: [
{
name: "targets",
type: "object",
description: 'Array of targets: [{ address: "host:port", serviceName: "my-app" }]. ' +
"Go apps expose pprof at :6060/debug/pprof by default",
example: '[{"address": "myapp:6060", "serviceName": "my-app"}]',
},
],
optionalParams: [
{ name: "scrapeInterval", type: "string", description: "Scrape interval", default: "15s" },
{
name: "profileTypes",
type: "object",
description: "Profile types to enable. Valid: cpu, memory, goroutine, mutex, block. " +
"Default: all five enabled",
default: ["cpu", "memory", "goroutine", "mutex", "block"],
},
{
name: "pyroscopeUrl",
type: "string",
description: "Override Pyroscope write URL (default from config or http://localhost:4040)",
},
],
generateConfig(pipelineId, params, targets, _pipelineName) {
const validatedTargets = validateTargets(params.targets);
const scrapeInterval = params.scrapeInterval || "15s";
const profileTypes = params.profileTypes ?? VALID_PROFILE_TYPES.slice();
const pyroscopeUrl = params.pyroscopeUrl || targets.pyroscopeWriteUrl || "http://localhost:4040";
// Validate profile types
for (const pt of profileTypes) {
if (!VALID_PROFILE_TYPES.includes(pt)) {
throw new Error(`Invalid profile type '${pt}'. Valid types: ${VALID_PROFILE_TYPES.join(", ")}`);
}
}
const scrapeLabel = componentLabel(pipelineId, "profiles");
const writeLabel = componentLabel(pipelineId, "write");
// Build targets using shared utility
const targetsStr = renderTargets(validatedTargets.map((t) => ({ address: t.address, labels: { service_name: t.serviceName } })));
// Build profiling_config — iterate ALL valid types, mark selected ones as enabled
const profileBlocks = VALID_PROFILE_TYPES
.map((pt) => {
const enabled = profileTypes.includes(pt);
return ` ${profileBlockName(pt)} {\n enabled = ${enabled}\n }`;
})
.join("\n\n");
const builder = new AlloyConfigBuilder();
builder.addBlock(`pyroscope.scrape "${scrapeLabel}" {
targets = ${targetsStr}
scrape_interval = "${escapeString(scrapeInterval)}"
profiling_config {
${profileBlocks}
}
forward_to = [pyroscope.write.${writeLabel}.receiver]
}`);
builder.addBlock(`pyroscope.write "${writeLabel}" {
endpoint {
url = "${escapeString(pyroscopeUrl)}"
}
}`);
return builder.build(pipelineId, "continuous-profiling", "continuous-profiling");
},
sampleQueries(params) {
const targets = params.targets;
const svc = targets?.[0]?.serviceName ?? "my-app";
return {
cpuProfile: `process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name="${svc}"}`,
memoryProfile: `memory:alloc_objects:count:space:bytes{service_name="${svc}"}`,
};
},
componentIds(pipelineId) {
return [
`pyroscope.scrape.${componentLabel(pipelineId, "profiles")}`,
`pyroscope.write.${componentLabel(pipelineId, "write")}`,
];
},
};
export default recipe;