@openclaw/diagnostics-prometheus 2026.9.2 → 2026.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +103 -72
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { isInternalDiagnosticEventMetadata, redactSensitiveText } from "./api.js";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
- import { normalizeDiagnosticLane, normalizeDiagnosticValue } from "openclaw/plugin-sdk/diagnostic-runtime";
3
+ import { isDiagnosticsEnabled, normalizeDiagnosticLane, normalizeDiagnosticValue } from "openclaw/plugin-sdk/diagnostic-runtime";
4
4
  import { asNonNegativeFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
5
+ import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
5
6
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
6
7
  //#region extensions/diagnostics-prometheus/src/service.ts
7
8
  const DURATION_BUCKETS_SECONDS = [
@@ -70,7 +71,9 @@ function seconds(ms) {
70
71
  return value === void 0 ? void 0 : value / 1e3;
71
72
  }
72
73
  function sortedLabels(labels) {
73
- return Object.entries(labels).toSorted(([left], [right]) => left.localeCompare(right));
74
+ const entries = Object.entries(labels);
75
+ entries.sort(([left], [right]) => left.localeCompare(right));
76
+ return entries;
74
77
  }
75
78
  function metricKey(name, labels) {
76
79
  return `${name}|${JSON.stringify(sortedLabels(labels))}`;
@@ -81,10 +84,13 @@ function escapeHelp(value) {
81
84
  function escapeLabelValue(value) {
82
85
  return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, "\\\"");
83
86
  }
87
+ function formatLabelEntry([key, value]) {
88
+ return `${key}="${escapeLabelValue(value)}"`;
89
+ }
84
90
  function formatLabels(labels) {
85
91
  const entries = sortedLabels(labels);
86
92
  if (entries.length === 0) return "";
87
- return `{${entries.map(([key, value]) => `${key}="${escapeLabelValue(value)}"`).join(",")}}`;
93
+ return `{${entries.map(formatLabelEntry).join(",")}}`;
88
94
  }
89
95
  function formatPrometheusNumber(value) {
90
96
  if (!Number.isFinite(value)) return "0";
@@ -95,9 +101,8 @@ function createPrometheusMetricStore() {
95
101
  const gauges = /* @__PURE__ */ new Map();
96
102
  const histograms = /* @__PURE__ */ new Map();
97
103
  let droppedSeries = 0;
98
- const canCreateSeries = (map, key, metricName) => {
104
+ const canCreateSeries = (map, key) => {
99
105
  if (map.has(key)) return true;
100
- if (metricName === DROPPED_SERIES_COUNTER_NAME) return true;
101
106
  if (counters.size + gauges.size + histograms.size < MAX_PROMETHEUS_SERIES) return true;
102
107
  droppedSeries += 1;
103
108
  return false;
@@ -105,7 +110,7 @@ function createPrometheusMetricStore() {
105
110
  const counter = (name, help, labels, amount = 1) => {
106
111
  if (!Number.isFinite(amount) || amount <= 0) return;
107
112
  const key = metricKey(name, labels);
108
- if (!canCreateSeries(counters, key, name)) return;
113
+ if (!canCreateSeries(counters, key)) return;
109
114
  const existing = counters.get(key);
110
115
  if (existing) {
111
116
  existing.value += amount;
@@ -120,7 +125,7 @@ function createPrometheusMetricStore() {
120
125
  const gauge = (name, help, labels, value) => {
121
126
  if (value === void 0 || !Number.isFinite(value)) return;
122
127
  const key = metricKey(name, labels);
123
- if (!canCreateSeries(gauges, key, name)) return;
128
+ if (!canCreateSeries(gauges, key)) return;
124
129
  gauges.set(key, {
125
130
  help,
126
131
  labels,
@@ -130,7 +135,7 @@ function createPrometheusMetricStore() {
130
135
  const histogram = (name, help, labels, value, buckets = DURATION_BUCKETS_SECONDS) => {
131
136
  if (value === void 0 || !Number.isFinite(value) || value < 0) return;
132
137
  const key = metricKey(name, labels);
133
- if (!canCreateSeries(histograms, key, name)) return;
138
+ if (!canCreateSeries(histograms, key)) return;
134
139
  let sample = histograms.get(key);
135
140
  if (!sample) {
136
141
  sample = {
@@ -151,16 +156,16 @@ function createPrometheusMetricStore() {
151
156
  }
152
157
  };
153
158
  const snapshot = () => {
154
- const counterSnapshot = new Map(counters);
155
- if (droppedSeries > 0) counterSnapshot.set(metricKey(DROPPED_SERIES_COUNTER_NAME, {}), {
159
+ const counterSnapshot = [...counters];
160
+ if (droppedSeries > 0) counterSnapshot.push([metricKey(DROPPED_SERIES_COUNTER_NAME, {}), {
156
161
  help: "Prometheus metric series dropped because the exporter series cap was reached.",
157
162
  labels: {},
158
163
  value: droppedSeries
159
- });
164
+ }]);
160
165
  return {
161
166
  counters: counterSnapshot,
162
- gauges: new Map(gauges),
163
- histograms: new Map(histograms)
167
+ gauges: [...gauges],
168
+ histograms: [...histograms]
164
169
  };
165
170
  };
166
171
  const reset = () => {
@@ -194,36 +199,35 @@ function renderPrometheusMetrics(store) {
194
199
  lines.push(`# HELP ${name} ${escapeHelp(help)}`);
195
200
  lines.push(`# TYPE ${name} ${type}`);
196
201
  };
197
- const counterEntries = [...snapshot.counters.entries()].toSorted(([left], [right]) => left.localeCompare(right));
198
- for (const [key, sample] of counterEntries) {
199
- const name = key.split("|", 1)[0] ?? "";
200
- emitHeader(name, "counter", sample.help);
201
- lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
202
- }
203
- const gaugeEntries = [...snapshot.gauges.entries()].toSorted(([left], [right]) => left.localeCompare(right));
204
- for (const [key, sample] of gaugeEntries) {
205
- const name = key.split("|", 1)[0] ?? "";
206
- emitHeader(name, "gauge", sample.help);
207
- lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
202
+ for (const [type, entries] of [["counter", snapshot.counters], ["gauge", snapshot.gauges]]) {
203
+ entries.sort(([left], [right]) => left.localeCompare(right));
204
+ for (const [key, sample] of entries) {
205
+ const name = key.split("|", 1)[0] ?? "";
206
+ emitHeader(name, type, sample.help);
207
+ lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
208
+ }
208
209
  }
209
- const histogramEntries = [...snapshot.histograms.entries()].toSorted(([left], [right]) => left.localeCompare(right));
210
- for (const [key, sample] of histogramEntries) {
210
+ snapshot.histograms.sort(([left], [right]) => left.localeCompare(right));
211
+ for (const [key, sample] of snapshot.histograms) {
211
212
  const name = key.split("|", 1)[0] ?? "";
212
213
  emitHeader(name, "histogram", sample.help);
214
+ const labels = formatLabels(sample.labels);
215
+ const bucketLabels = sortedLabels({
216
+ ...sample.labels,
217
+ le: ""
218
+ });
219
+ const boundIndex = bucketLabels.findIndex(([labelKey]) => labelKey === "le");
220
+ const bucketFragments = bucketLabels.map(formatLabelEntry);
213
221
  for (let index = 0; index < sample.buckets.length; index += 1) {
214
222
  const bucket = sample.buckets[index];
215
223
  if (bucket === void 0) continue;
216
- lines.push(`${name}_bucket${formatLabels({
217
- ...sample.labels,
218
- le: String(bucket)
219
- })} ${formatPrometheusNumber(sample.counts[index] ?? 0)}`);
224
+ bucketFragments[boundIndex] = `le="${String(bucket)}"`;
225
+ lines.push(`${name}_bucket{${bucketFragments.join(",")}} ${formatPrometheusNumber(sample.counts[index] ?? 0)}`);
220
226
  }
221
- lines.push(`${name}_bucket${formatLabels({
222
- ...sample.labels,
223
- le: "+Inf"
224
- })} ${formatPrometheusNumber(sample.count)}`);
225
- lines.push(`${name}_sum${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.sum)}`);
226
- lines.push(`${name}_count${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.count)}`);
227
+ bucketFragments[boundIndex] = "le=\"+Inf\"";
228
+ lines.push(`${name}_bucket{${bucketFragments.join(",")}} ${formatPrometheusNumber(sample.count)}`);
229
+ lines.push(`${name}_sum${labels} ${formatPrometheusNumber(sample.sum)}`);
230
+ lines.push(`${name}_count${labels} ${formatPrometheusNumber(sample.count)}`);
227
231
  }
228
232
  lines.push("");
229
233
  return lines.join("\n");
@@ -373,6 +377,9 @@ function recordModelUsage(store, evt) {
373
377
  function recordDiagnosticEvent(store, evt, metadata) {
374
378
  if (!shouldRecordDiagnosticEvent(metadata)) return;
375
379
  switch (evt.type) {
380
+ case "diagnostic.gc":
381
+ store.histogram("openclaw_gc_duration_seconds", "Elapsed garbage collection duration in seconds for the hosting JavaScript isolate.", {}, seconds(evt.durationMs));
382
+ return;
376
383
  case "gateway.event_loop.sample":
377
384
  store.histogram("openclaw_gateway_event_loop_delay_max_seconds", "Maximum event-loop delay per completed Gateway observation window in seconds.", {}, seconds(evt.delayMaxMs));
378
385
  store.counter("openclaw_gateway_event_loop_observed_seconds_total", "Elapsed seconds covered by completed Gateway event-loop observation windows.", {}, evt.intervalMs / 1e3);
@@ -425,18 +432,16 @@ function recordDiagnosticEvent(store, evt, metadata) {
425
432
  store.histogram("openclaw_harness_run_duration_seconds", "Agent harness run duration in seconds.", harnessLabels(evt), seconds(evt.durationMs));
426
433
  store.counter("openclaw_harness_run_total", "Agent harness runs completed by outcome.", harnessLabels(evt));
427
434
  return;
428
- case "message.processed":
429
- store.counter("openclaw_message_processed_total", "Inbound messages processed by outcome.", {
430
- channel: normalizeDiagnosticValue(evt.channel),
431
- outcome: evt.outcome,
432
- reason: normalizeDiagnosticValue(evt.reason, "none")
433
- });
434
- store.histogram("openclaw_message_processed_duration_seconds", "Inbound message processing duration in seconds.", {
435
+ case "message.processed": {
436
+ const labels = {
435
437
  channel: normalizeDiagnosticValue(evt.channel),
436
438
  outcome: evt.outcome,
437
439
  reason: normalizeDiagnosticValue(evt.reason, "none")
438
- }, seconds(evt.durationMs));
440
+ };
441
+ store.counter("openclaw_message_processed_total", "Inbound messages processed by outcome.", labels);
442
+ store.histogram("openclaw_message_processed_duration_seconds", "Inbound message processing duration in seconds.", labels, seconds(evt.durationMs));
439
443
  return;
444
+ }
440
445
  case "webhook.received":
441
446
  store.counter("openclaw_webhook_received_total", "Webhook requests received by channel and update type.", webhookLabels(evt));
442
447
  return;
@@ -464,35 +469,29 @@ function recordDiagnosticEvent(store, evt, metadata) {
464
469
  source: normalizeDiagnosticValue(evt.source)
465
470
  });
466
471
  return;
467
- case "message.dispatch.completed":
468
- store.counter("openclaw_message_dispatch_completed_total", "Inbound message dispatch attempts completed by outcome.", {
472
+ case "message.dispatch.completed": {
473
+ const labels = {
469
474
  channel: normalizeDiagnosticValue(evt.channel),
470
475
  outcome: evt.outcome,
471
476
  reason: normalizeDiagnosticValue(evt.reason, "none"),
472
477
  source: normalizeDiagnosticValue(evt.source)
473
- });
474
- store.histogram("openclaw_message_dispatch_duration_seconds", "Inbound message dispatch duration in seconds.", {
475
- channel: normalizeDiagnosticValue(evt.channel),
476
- outcome: evt.outcome,
477
- reason: normalizeDiagnosticValue(evt.reason, "none"),
478
- source: normalizeDiagnosticValue(evt.source)
479
- }, seconds(evt.durationMs));
478
+ };
479
+ store.counter("openclaw_message_dispatch_completed_total", "Inbound message dispatch attempts completed by outcome.", labels);
480
+ store.histogram("openclaw_message_dispatch_duration_seconds", "Inbound message dispatch duration in seconds.", labels, seconds(evt.durationMs));
480
481
  return;
482
+ }
481
483
  case "message.delivery.completed":
482
- case "message.delivery.error":
483
- store.counter("openclaw_message_delivery_total", "Outbound message delivery attempts by outcome.", {
484
+ case "message.delivery.error": {
485
+ const labels = {
484
486
  channel: normalizeDiagnosticValue(evt.channel),
485
487
  delivery_kind: normalizeDiagnosticValue(evt.deliveryKind, "other"),
486
488
  error_category: evt.type === "message.delivery.error" ? normalizeDiagnosticValue(evt.errorCategory, "other") : "none",
487
489
  outcome: evt.type === "message.delivery.error" ? "error" : "completed"
488
- });
489
- store.histogram("openclaw_message_delivery_duration_seconds", "Outbound message delivery duration in seconds.", {
490
- channel: normalizeDiagnosticValue(evt.channel),
491
- delivery_kind: normalizeDiagnosticValue(evt.deliveryKind, "other"),
492
- error_category: evt.type === "message.delivery.error" ? normalizeDiagnosticValue(evt.errorCategory, "other") : "none",
493
- outcome: evt.type === "message.delivery.error" ? "error" : "completed"
494
- }, seconds(evt.durationMs));
490
+ };
491
+ store.counter("openclaw_message_delivery_total", "Outbound message delivery attempts by outcome.", labels);
492
+ store.histogram("openclaw_message_delivery_duration_seconds", "Outbound message delivery duration in seconds.", labels, seconds(evt.durationMs));
495
493
  return;
494
+ }
496
495
  case "talk.event":
497
496
  store.counter("openclaw_talk_event_total", "Talk events emitted by type.", talkLabels(evt));
498
497
  store.histogram("openclaw_talk_event_duration_seconds", "Talk event duration in seconds when reported.", talkLabels(evt), seconds(evt.durationMs));
@@ -527,9 +526,11 @@ function recordDiagnosticEvent(store, evt, metadata) {
527
526
  });
528
527
  return;
529
528
  case "diagnostic.memory.sample":
530
- store.gauge("openclaw_memory_bytes", "Latest process memory usage by memory kind.", { kind: "rss" }, evt.memory.rssBytes);
531
- store.gauge("openclaw_memory_bytes", "Latest process memory usage by memory kind.", { kind: "heap_total" }, evt.memory.heapTotalBytes);
532
- store.gauge("openclaw_memory_bytes", "Latest process memory usage by memory kind.", { kind: "heap_used" }, evt.memory.heapUsedBytes);
529
+ for (const [kind, field] of [
530
+ ["rss", "rssBytes"],
531
+ ["heap_total", "heapTotalBytes"],
532
+ ["heap_used", "heapUsedBytes"]
533
+ ]) store.gauge("openclaw_memory_bytes", "Latest process memory usage by memory kind.", { kind }, evt.memory[field]);
533
534
  store.histogram("openclaw_memory_rss_bytes", "RSS memory sample distribution in bytes.", {}, asNonNegativeFiniteNumber(evt.memory.rssBytes), BYTE_BUCKETS);
534
535
  return;
535
536
  case "diagnostic.memory.pressure":
@@ -540,19 +541,26 @@ function recordDiagnosticEvent(store, evt, metadata) {
540
541
  return;
541
542
  case "diagnostic.liveness.warning":
542
543
  store.counter("openclaw_liveness_warning_total", "Diagnostic liveness warning events.", livenessLabels(evt));
543
- store.gauge("openclaw_liveness_sessions", "Latest session counts reported with diagnostic liveness warnings.", { state: "active" }, asNonNegativeFiniteNumber(evt.active));
544
- store.gauge("openclaw_liveness_sessions", "Latest session counts reported with diagnostic liveness warnings.", { state: "waiting" }, asNonNegativeFiniteNumber(evt.waiting));
545
- store.gauge("openclaw_liveness_sessions", "Latest session counts reported with diagnostic liveness warnings.", { state: "queued" }, asNonNegativeFiniteNumber(evt.queued));
544
+ for (const state of [
545
+ "active",
546
+ "waiting",
547
+ "queued"
548
+ ]) store.gauge("openclaw_liveness_sessions", "Latest session counts reported with diagnostic liveness warnings.", { state }, asNonNegativeFiniteNumber(evt[state]));
546
549
  store.histogram("openclaw_liveness_event_loop_delay_p99_seconds", "P99 event-loop delay reported by diagnostic liveness warnings in seconds.", livenessLabels(evt), seconds(evt.eventLoopDelayP99Ms));
547
550
  store.histogram("openclaw_liveness_event_loop_delay_max_seconds", "Maximum event-loop delay reported by diagnostic liveness warnings in seconds.", livenessLabels(evt), seconds(evt.eventLoopDelayMaxMs));
548
551
  store.histogram("openclaw_liveness_event_loop_utilization_ratio", "Event-loop utilization reported by diagnostic liveness warnings.", livenessLabels(evt), asNonNegativeFiniteNumber(evt.eventLoopUtilization), RATIO_BUCKETS);
549
552
  store.histogram("openclaw_liveness_cpu_core_ratio", "Whole-process CPU usage in core equivalents, including worker and native threads; can exceed 1.", livenessLabels(evt), asNonNegativeFiniteNumber(evt.cpuCoreRatio), RATIO_BUCKETS);
550
553
  return;
551
554
  case "diagnostic.async_queue.dropped":
552
- store.counter("openclaw_diagnostic_async_queue_dropped_total", "Async diagnostic queue drops by dropped event class.", { drop_class: "total" }, asNonNegativeFiniteNumber(evt.droppedEvents));
553
- if (evt.droppedTrustedEvents !== void 0) store.counter("openclaw_diagnostic_async_queue_dropped_total", "Async diagnostic queue drops by dropped event class.", { drop_class: "trusted" }, asNonNegativeFiniteNumber(evt.droppedTrustedEvents));
554
- if (evt.droppedUntrustedEvents !== void 0) store.counter("openclaw_diagnostic_async_queue_dropped_total", "Async diagnostic queue drops by dropped event class.", { drop_class: "untrusted" }, asNonNegativeFiniteNumber(evt.droppedUntrustedEvents));
555
- if (evt.droppedPriorityEvents !== void 0) store.counter("openclaw_diagnostic_async_queue_dropped_total", "Async diagnostic queue drops by dropped event class.", { drop_class: "priority" }, asNonNegativeFiniteNumber(evt.droppedPriorityEvents));
555
+ for (const [dropClass, field] of [
556
+ ["total", "droppedEvents"],
557
+ ["trusted", "droppedTrustedEvents"],
558
+ ["untrusted", "droppedUntrustedEvents"],
559
+ ["priority", "droppedPriorityEvents"]
560
+ ]) {
561
+ if (field !== "droppedEvents" && evt[field] === void 0) continue;
562
+ store.counter("openclaw_diagnostic_async_queue_dropped_total", "Async diagnostic queue drops by dropped event class.", { drop_class: dropClass }, asNonNegativeFiniteNumber(evt[field]));
563
+ }
556
564
  store.gauge("openclaw_diagnostic_async_queue_length", "Latest async diagnostic queue length after a drop summary.", {}, asNonNegativeFiniteNumber(evt.queueLength));
557
565
  break;
558
566
  case "diagnostic.heartbeat": break;
@@ -569,8 +577,26 @@ function recordDiagnosticEvent(store, evt, metadata) {
569
577
  store.histogram("openclaw_payload_large_bytes", "Oversized payload byte sizes by surface and action.", payloadLargeLabels(evt), asNonNegativeFiniteNumber(evt.bytes), BYTE_BUCKETS);
570
578
  }
571
579
  }
580
+ const METRICS_READ_SCOPE = "operator.read";
581
+ const METRICS_READ_IMPLYING_SCOPES = [
582
+ METRICS_READ_SCOPE,
583
+ "operator.write",
584
+ "operator.admin"
585
+ ];
586
+ function hasMetricsReadScope() {
587
+ const runtimeScopes = getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes;
588
+ const scopes = Array.isArray(runtimeScopes) ? runtimeScopes : [];
589
+ return METRICS_READ_IMPLYING_SCOPES.some((scope) => scopes.includes(scope));
590
+ }
572
591
  function createMetricsHandler(store) {
573
592
  return (req, res) => {
593
+ if (!hasMetricsReadScope()) {
594
+ res.statusCode = 403;
595
+ res.setHeader("Cache-Control", "no-store");
596
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
597
+ res.end(`missing scope: ${METRICS_READ_SCOPE}`);
598
+ return true;
599
+ }
574
600
  if (req.method !== "GET" && req.method !== "HEAD") {
575
601
  res.statusCode = 405;
576
602
  res.setHeader("Allow", "GET, HEAD");
@@ -610,6 +636,11 @@ function createDiagnosticsPrometheusExporter() {
610
636
  ctx.logger.error("diagnostics-prometheus: internal diagnostics capability unavailable");
611
637
  return;
612
638
  }
639
+ const identity = isDiagnosticsEnabled(ctx.config) ? ctx.internalDiagnostics?.getRuntimeIdentity?.() : void 0;
640
+ if (identity) store.gauge("openclaw_gateway_build_info", "Identity of the hosting process and its loaded build; not a health or exporter epoch.", {
641
+ process_instance_id: identity.processInstanceId,
642
+ ...identity.buildId ? { build_id: identity.buildId } : {}
643
+ }, 1);
613
644
  unsubscribe = subscribe((event, metadata) => {
614
645
  try {
615
646
  recordDiagnosticEvent(store, event, metadata);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/diagnostics-prometheus",
3
- "version": "2026.9.2",
3
+ "version": "2026.9.3",
4
4
  "description": "OpenClaw diagnostics Prometheus exporter for runtime metrics.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.4.25"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.9.2"
21
+ "pluginApi": ">=2026.9.3"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.2"
24
+ "openclawVersion": "2026.9.3"
25
25
  },
26
26
  "release": {
27
27
  "publishToClawHub": true,
@@ -37,7 +37,7 @@
37
37
  "README.md"
38
38
  ],
39
39
  "peerDependencies": {
40
- "openclaw": ">=2026.9.2"
40
+ "openclaw": ">=2026.9.3"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "openclaw": {