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
95 lines (94 loc) • 3.46 kB
JavaScript
/**
* OTLP JSON Writer
*
* Constructs OTLP ExportMetricsServiceRequest payloads as JSON with custom
* timestamps and POSTs them to the OTLP HTTP endpoint. This bypasses the
* OTel SDK (which doesn't expose timestamp control) while using the same
* wire protocol and collector endpoint.
*
* Data flow:
* TimestampedSample[] → OTLP JSON (gauge dataPoints with timeUnixNano)
* → POST to :4318/v1/metrics → OTel Collector → Mimir → Grafana
*/
// ── Writer ───────────────────────────────────────────────────────────
export class OtlpJsonWriter {
endpoint;
headers;
constructor(config) {
this.endpoint = config.endpoint;
this.headers = config.headers ?? {};
}
async write(samples) {
if (samples.length === 0)
return;
const payload = this.buildPayload(samples);
const res = await fetch(this.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
...this.headers,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`OTLP push failed (HTTP ${res.status}): ${body || res.statusText}`);
}
}
/** Visible for testing. */
buildPayload(samples) {
// Group samples by metric name
const grouped = new Map();
for (const s of samples) {
let group = grouped.get(s.metricName);
if (!group) {
group = [];
grouped.set(s.metricName, group);
}
group.push(s);
}
const metrics = [];
for (const [name, group] of grouped) {
metrics.push({
name,
description: group[0].description ?? "Custom metric",
gauge: {
dataPoints: group.map((s) => ({
timeUnixNano: msToNanoString(s.timestampMs),
asDouble: s.value,
attributes: labelsToAttributes(s.labels),
})),
},
});
}
return {
resourceMetrics: [
{
resource: {
attributes: [
{ key: "service.name", value: { stringValue: "openclaw" } },
{ key: "service.namespace", value: { stringValue: "grafana-lens" } },
],
},
scopeMetrics: [
{
scope: { name: "grafana-lens-custom" },
metrics,
},
],
},
],
};
}
}
// ── Helpers ──────────────────────────────────────────────────────────
/** Convert millisecond timestamp to nanosecond string (avoids Number overflow). */
export function msToNanoString(ms) {
return `${ms}000000`;
}
export function labelsToAttributes(labels) {
return Object.entries(labels).map(([key, val]) => ({
key,
value: { stringValue: val },
}));
}