@openclaw/diagnostics-prometheus 2026.11.3 → 2026.11.5-beta.2
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.
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isDiagnosticsEnabled, normalizeDiagnosticLane, normalizeDiagnosticValue
|
|
|
4
4
|
import { asNonNegativeFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
|
5
5
|
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
|
6
6
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
7
|
+
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
7
8
|
//#region extensions/diagnostics-prometheus/src/prometheus-format.ts
|
|
8
9
|
function sortedLabels(labels) {
|
|
9
10
|
const entries = Object.entries(labels);
|
|
@@ -33,7 +34,167 @@ function seconds(ms) {
|
|
|
33
34
|
return value === void 0 ? void 0 : value / 1e3;
|
|
34
35
|
}
|
|
35
36
|
//#endregion
|
|
37
|
+
//#region extensions/diagnostics-prometheus/src/prometheus-family-budget.ts
|
|
38
|
+
const PROMETHEUS_FAMILY_RESERVES = {
|
|
39
|
+
openclaw_chat_history_requests_total: 4,
|
|
40
|
+
openclaw_chat_history_response_bytes_total: 4,
|
|
41
|
+
openclaw_chat_send_phase_seconds: 32,
|
|
42
|
+
openclaw_child_process_spawn_total: 4,
|
|
43
|
+
openclaw_code_mode_worker_heap_bytes: 128,
|
|
44
|
+
openclaw_code_mode_worker_memory_bytes: 128,
|
|
45
|
+
openclaw_control_ui_document_duration_seconds: 4,
|
|
46
|
+
openclaw_cron_list_requests_total: 4,
|
|
47
|
+
openclaw_diagnostic_async_queue_dropped_total: 4,
|
|
48
|
+
openclaw_diagnostic_async_queue_length: 4,
|
|
49
|
+
openclaw_diff_cache_misses_total: 4,
|
|
50
|
+
openclaw_diff_stage_seconds: 4,
|
|
51
|
+
openclaw_gateway_active_work: 4,
|
|
52
|
+
openclaw_gateway_build_info: 4,
|
|
53
|
+
openclaw_gateway_event_loop_delay_max_seconds: 4,
|
|
54
|
+
openclaw_gateway_event_loop_observed_seconds_total: 4,
|
|
55
|
+
openclaw_gateway_http_cancelled_total: 4,
|
|
56
|
+
openclaw_gateway_main_thread_stall_seconds: 4,
|
|
57
|
+
openclaw_gateway_main_thread_stalls_dropped_total: 4,
|
|
58
|
+
openclaw_gateway_rpc_admission_seconds: 256,
|
|
59
|
+
openclaw_gateway_rpc_client_requests_total: 512,
|
|
60
|
+
openclaw_gateway_rpc_first_response_seconds: 256,
|
|
61
|
+
openclaw_gateway_rpc_handler_heap_delta_bytes: 256,
|
|
62
|
+
openclaw_gateway_rpc_handler_heap_delta_exclusive_total: 256,
|
|
63
|
+
openclaw_gateway_rpc_handler_seconds: 256,
|
|
64
|
+
openclaw_gateway_rpc_outcomes_total: 256,
|
|
65
|
+
openclaw_gateway_rpc_queue_wait_seconds: 256,
|
|
66
|
+
openclaw_gateway_rpc_requests_total: 256,
|
|
67
|
+
openclaw_gateway_rpc_response_bytes: 256,
|
|
68
|
+
openclaw_gateway_rpc_stage_seconds: 256,
|
|
69
|
+
openclaw_gateway_rpc_stage_thread_cpu_seconds: 256,
|
|
70
|
+
openclaw_gateway_ws_event_bytes_total: 256,
|
|
71
|
+
openclaw_gateway_ws_event_frame_bytes: 2048,
|
|
72
|
+
openclaw_gateway_ws_event_wire_bytes_total: 256,
|
|
73
|
+
openclaw_gateway_ws_events_total: 256,
|
|
74
|
+
openclaw_gateway_ws_operator_connections: 4,
|
|
75
|
+
openclaw_gateway_ws_presence_bytes_total: 4,
|
|
76
|
+
openclaw_gateway_ws_presence_frames_total: 4,
|
|
77
|
+
openclaw_gateway_ws_session_event_bytes_total: 256,
|
|
78
|
+
openclaw_gateway_ws_session_event_frames_total: 256,
|
|
79
|
+
openclaw_gc_duration_seconds: 384,
|
|
80
|
+
openclaw_gen_ai_client_token_usage: 128,
|
|
81
|
+
openclaw_harness_run_duration_seconds: 128,
|
|
82
|
+
openclaw_harness_run_total: 128,
|
|
83
|
+
openclaw_heap_old_generation_after_gc_bytes: 128,
|
|
84
|
+
openclaw_heap_old_generation_after_gc_ratio: 128,
|
|
85
|
+
openclaw_heap_size_limit_bytes: 128,
|
|
86
|
+
openclaw_heap_space_bytes: 256,
|
|
87
|
+
openclaw_heap_used_after_major_gc_bytes: 64,
|
|
88
|
+
openclaw_heap_used_after_major_gc_timestamp_seconds: 64,
|
|
89
|
+
openclaw_heap_used_ratio: 128,
|
|
90
|
+
openclaw_liveness_cpu_core_ratio: 4,
|
|
91
|
+
openclaw_liveness_event_loop_delay_max_seconds: 4,
|
|
92
|
+
openclaw_liveness_event_loop_delay_p99_seconds: 4,
|
|
93
|
+
openclaw_liveness_event_loop_utilization_ratio: 4,
|
|
94
|
+
openclaw_liveness_sessions: 4,
|
|
95
|
+
openclaw_liveness_warning_total: 4,
|
|
96
|
+
openclaw_memory_bytes: 128,
|
|
97
|
+
openclaw_memory_pressure_total: 4,
|
|
98
|
+
openclaw_memory_rss_bytes: 4,
|
|
99
|
+
openclaw_message_delivery_duration_seconds: 4,
|
|
100
|
+
openclaw_message_delivery_started_total: 4,
|
|
101
|
+
openclaw_message_delivery_total: 4,
|
|
102
|
+
openclaw_message_dispatch_completed_total: 4,
|
|
103
|
+
openclaw_message_dispatch_duration_seconds: 4,
|
|
104
|
+
openclaw_message_dispatch_started_total: 4,
|
|
105
|
+
openclaw_message_processed_duration_seconds: 4,
|
|
106
|
+
openclaw_message_processed_total: 4,
|
|
107
|
+
openclaw_message_received_total: 4,
|
|
108
|
+
openclaw_model_call_duration_seconds: 128,
|
|
109
|
+
openclaw_model_call_total: 128,
|
|
110
|
+
openclaw_model_cost_usd_total: 128,
|
|
111
|
+
openclaw_model_failover_total: 128,
|
|
112
|
+
openclaw_model_tokens_total: 128,
|
|
113
|
+
openclaw_model_usage_duration_seconds: 128,
|
|
114
|
+
openclaw_models_list_requests_total: 4,
|
|
115
|
+
openclaw_payload_large_bytes: 4,
|
|
116
|
+
openclaw_payload_large_total: 4,
|
|
117
|
+
openclaw_process_anonymous_memory_bytes: 4,
|
|
118
|
+
openclaw_queue_lane_size: 4,
|
|
119
|
+
openclaw_queue_lane_wait_seconds: 4,
|
|
120
|
+
openclaw_run_completed_total: 128,
|
|
121
|
+
openclaw_run_duration_seconds: 128,
|
|
122
|
+
openclaw_secret_egress_requests_total: 4,
|
|
123
|
+
openclaw_session_event_rows_reset_total: 4,
|
|
124
|
+
openclaw_session_event_rows_retained_bytes: 4,
|
|
125
|
+
openclaw_session_event_rows_retained_entries: 4,
|
|
126
|
+
openclaw_session_queue_depth: 4,
|
|
127
|
+
openclaw_session_recovery_age_seconds: 4,
|
|
128
|
+
openclaw_session_recovery_total: 4,
|
|
129
|
+
openclaw_session_row_selection_resets_total: 4,
|
|
130
|
+
openclaw_session_row_partition_cache_total: 32,
|
|
131
|
+
openclaw_session_row_partition_revisions_total: 8,
|
|
132
|
+
openclaw_session_row_selection_rows_total: 64,
|
|
133
|
+
openclaw_session_row_selections_total: 64,
|
|
134
|
+
openclaw_session_state_total: 4,
|
|
135
|
+
openclaw_session_stuck_age_seconds: 4,
|
|
136
|
+
openclaw_session_stuck_total: 4,
|
|
137
|
+
openclaw_session_turn_created_total: 4,
|
|
138
|
+
openclaw_sessions_active: 4,
|
|
139
|
+
openclaw_sessions_list_base_misses_total: 4,
|
|
140
|
+
openclaw_sessions_list_baseline_retirements_total: 4,
|
|
141
|
+
openclaw_sessions_list_requests_total: 4,
|
|
142
|
+
openclaw_sessions_list_response_bytes_total: 4,
|
|
143
|
+
openclaw_sessions_list_retained_bytes: 4,
|
|
144
|
+
openclaw_sessions_list_retained_refs: 4,
|
|
145
|
+
openclaw_sessions_list_retained_views: 4,
|
|
146
|
+
openclaw_skill_used_total: 4,
|
|
147
|
+
openclaw_talk_audio_bytes: 4,
|
|
148
|
+
openclaw_talk_event_duration_seconds: 4,
|
|
149
|
+
openclaw_talk_event_total: 4,
|
|
150
|
+
openclaw_telemetry_exporter_total: 4,
|
|
151
|
+
openclaw_tool_catalog_tools_total: 128,
|
|
152
|
+
openclaw_tool_catalog_total: 128,
|
|
153
|
+
openclaw_tool_execution_blocked_total: 128,
|
|
154
|
+
openclaw_tool_execution_duration_seconds: 128,
|
|
155
|
+
openclaw_tool_execution_total: 128,
|
|
156
|
+
openclaw_webhook_duration_seconds: 4,
|
|
157
|
+
openclaw_webhook_error_total: 4,
|
|
158
|
+
openclaw_webhook_received_total: 4,
|
|
159
|
+
openclaw_worker_array_buffers_bytes: 128,
|
|
160
|
+
openclaw_worker_count: 128,
|
|
161
|
+
openclaw_worker_external_bytes: 128,
|
|
162
|
+
openclaw_worker_gc_collections_total: 128,
|
|
163
|
+
openclaw_worker_gc_duration_seconds_total: 128,
|
|
164
|
+
openclaw_worker_gc_reclaimed_bytes_total: 128,
|
|
165
|
+
openclaw_worker_heap_sampled_count: 128,
|
|
166
|
+
openclaw_worker_heap_space_used_bytes: 640,
|
|
167
|
+
openclaw_worker_heap_used_bytes: 128,
|
|
168
|
+
openclaw_worker_heap_used_high_water_bytes: 128,
|
|
169
|
+
openclaw_worker_message_payload_bytes_total: 512,
|
|
170
|
+
openclaw_worker_message_unmeasured_total: 64,
|
|
171
|
+
openclaw_worker_messages_total: 64,
|
|
172
|
+
openclaw_worker_queue_depth: 512,
|
|
173
|
+
openclaw_worker_queue_wait_seconds: 512,
|
|
174
|
+
openclaw_worker_request_phase_seconds: 1024,
|
|
175
|
+
openclaw_worker_request_seconds: 512,
|
|
176
|
+
openclaw_worker_retired_total: 64,
|
|
177
|
+
openclaw_worker_started_total: 64,
|
|
178
|
+
openclaw_worker_tasks_completed_total: 128,
|
|
179
|
+
openclaw_worktree_preparation_seconds: 32
|
|
180
|
+
};
|
|
181
|
+
const SHARED_PROMETHEUS_SERIES = 16384 - Object.values(PROMETHEUS_FAMILY_RESERVES).reduce((sum, n) => sum + n, 0);
|
|
182
|
+
//#endregion
|
|
36
183
|
//#region extensions/diagnostics-prometheus/src/prometheus-metric-store.ts
|
|
184
|
+
function createSeries() {
|
|
185
|
+
return {
|
|
186
|
+
samples: [],
|
|
187
|
+
shapes: /* @__PURE__ */ new Map(),
|
|
188
|
+
sorted: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function sortedSamples(series) {
|
|
192
|
+
if (!series.sorted) {
|
|
193
|
+
series.samples.sort((left, right) => left.key.localeCompare(right.key));
|
|
194
|
+
series.sorted = true;
|
|
195
|
+
}
|
|
196
|
+
return series.samples;
|
|
197
|
+
}
|
|
37
198
|
const DURATION_BUCKETS_SECONDS = [
|
|
38
199
|
.005,
|
|
39
200
|
.01,
|
|
@@ -58,101 +219,270 @@ const AGENT_DURATION_BUCKETS_SECONDS = [
|
|
|
58
219
|
1800,
|
|
59
220
|
3600
|
|
60
221
|
];
|
|
61
|
-
const MAX_PROMETHEUS_SERIES = 2048;
|
|
62
222
|
const DROPPED_SERIES_COUNTER_NAME = "openclaw_prometheus_series_dropped_total";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
223
|
+
const DROPPED_FAMILY_COUNTER_NAME = "openclaw_prometheus_family_series_dropped_total";
|
|
224
|
+
const warnedFamilies = /* @__PURE__ */ new Set();
|
|
225
|
+
function createPrometheusMetricStore(onSeriesDropped) {
|
|
226
|
+
const counters = createSeries();
|
|
227
|
+
const gauges = createSeries();
|
|
228
|
+
const histograms = createSeries();
|
|
67
229
|
let droppedSeries = 0;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
230
|
+
let sharedSeries = 0;
|
|
231
|
+
const familyCounts = /* @__PURE__ */ new Map();
|
|
232
|
+
const familyDrops = /* @__PURE__ */ new Map();
|
|
233
|
+
const headers = /* @__PURE__ */ new Map();
|
|
234
|
+
let prefixBuffer = Buffer.alloc(0);
|
|
235
|
+
let prefixOffset = 0;
|
|
236
|
+
const encodePrefix = (text) => {
|
|
237
|
+
const bytes = Buffer.byteLength(text);
|
|
238
|
+
if (bytes > 4096) {
|
|
239
|
+
const prefix = Buffer.allocUnsafeSlow(bytes);
|
|
240
|
+
prefix.write(text);
|
|
241
|
+
return prefix;
|
|
242
|
+
}
|
|
243
|
+
if (prefixOffset + bytes > prefixBuffer.length) {
|
|
244
|
+
prefixBuffer = Buffer.allocUnsafeSlow(4096);
|
|
245
|
+
prefixOffset = 0;
|
|
246
|
+
}
|
|
247
|
+
const start = prefixOffset;
|
|
248
|
+
prefixOffset += prefixBuffer.write(text, prefixOffset);
|
|
249
|
+
return prefixBuffer.subarray(start, prefixOffset);
|
|
73
250
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
251
|
+
let scratch = Buffer.alloc(0);
|
|
252
|
+
const series = (map, name, labels) => {
|
|
253
|
+
let labelCount = 0;
|
|
254
|
+
for (const key in labels) if (Object.hasOwn(labels, key)) labelCount++;
|
|
255
|
+
let shape;
|
|
256
|
+
shapes: for (const candidate of map.shapes.get(name) ?? []) {
|
|
257
|
+
if (candidate.keys.length !== labelCount) continue;
|
|
258
|
+
let node = candidate.root;
|
|
259
|
+
for (const key of candidate.keys) {
|
|
260
|
+
const value = labels[key];
|
|
261
|
+
if (value === void 0 || !Object.prototype.propertyIsEnumerable.call(labels, key)) continue shapes;
|
|
262
|
+
node = node?.values?.get(value);
|
|
263
|
+
}
|
|
264
|
+
shape = candidate;
|
|
265
|
+
if (node?.sample) return node;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
const count = familyCounts.get(name) ?? 0;
|
|
269
|
+
const reserve = PROMETHEUS_FAMILY_RESERVES[name];
|
|
270
|
+
if (count >= Math.max(128, reserve) || count >= reserve && sharedSeries >= SHARED_PROMETHEUS_SERIES) {
|
|
271
|
+
droppedSeries++;
|
|
272
|
+
familyDrops.set(name, (familyDrops.get(name) ?? 0) + 1);
|
|
273
|
+
if (onSeriesDropped && !warnedFamilies.has(name)) {
|
|
274
|
+
warnedFamilies.add(name);
|
|
275
|
+
onSeriesDropped(name);
|
|
276
|
+
}
|
|
81
277
|
return;
|
|
82
278
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
value
|
|
97
|
-
|
|
279
|
+
familyCounts.set(name, count + 1);
|
|
280
|
+
if (count >= reserve) sharedSeries++;
|
|
281
|
+
if (!shape) {
|
|
282
|
+
shape = {
|
|
283
|
+
keys: Object.keys(labels).toSorted((left, right) => left.localeCompare(right)),
|
|
284
|
+
root: {}
|
|
285
|
+
};
|
|
286
|
+
const shapes = map.shapes.get(name);
|
|
287
|
+
if (shapes) shapes.push(shape);
|
|
288
|
+
else map.shapes.set(name, [shape]);
|
|
289
|
+
}
|
|
290
|
+
let node = shape.root;
|
|
291
|
+
for (const key of shape.keys) {
|
|
292
|
+
const value = expectDefined(labels[key], "Prometheus label");
|
|
293
|
+
const values = node.values ??= /* @__PURE__ */ new Map();
|
|
294
|
+
let child = values.get(value);
|
|
295
|
+
if (!child) {
|
|
296
|
+
child = {};
|
|
297
|
+
values.set(value, child);
|
|
298
|
+
}
|
|
299
|
+
node = child;
|
|
300
|
+
}
|
|
301
|
+
return node;
|
|
98
302
|
};
|
|
99
|
-
const
|
|
100
|
-
if (!Number.isFinite(value) || value < 0) return;
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
303
|
+
const scalar = (map, name, help, labels, value, mode) => {
|
|
304
|
+
if (value === void 0 || !Number.isFinite(value) || mode !== "set" && value < 0 || mode === "add" && value === 0) return;
|
|
305
|
+
const node = series(map, name, labels);
|
|
306
|
+
if (!node) return;
|
|
307
|
+
if (node.sample) {
|
|
308
|
+
const next = mode === "add" ? node.sample.value + value : mode === "max" ? Math.max(node.sample.value, value) : value;
|
|
309
|
+
if (next !== node.sample.value) {
|
|
310
|
+
node.sample.value = next;
|
|
311
|
+
node.sample.renderedDirty = true;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
node.sample = {
|
|
315
|
+
help,
|
|
316
|
+
key: metricKey(name, labels),
|
|
317
|
+
name,
|
|
318
|
+
prefix: encodePrefix(`${name}${formatLabels(labels)} `),
|
|
319
|
+
value,
|
|
320
|
+
renderedDirty: true
|
|
321
|
+
};
|
|
322
|
+
map.samples.push(node.sample);
|
|
323
|
+
map.sorted = false;
|
|
324
|
+
}
|
|
107
325
|
};
|
|
326
|
+
const counter = (name, help, labels, amount = 1) => scalar(counters, name, help, labels, amount, "add");
|
|
327
|
+
const counterValue = (name, help, labels, value) => scalar(counters, name, help, labels, value, "max");
|
|
328
|
+
const gauge = (name, help, labels, value) => scalar(gauges, name, help, labels, value, "set");
|
|
108
329
|
const clearGauges = (name) => {
|
|
109
|
-
|
|
330
|
+
headers.delete(name);
|
|
331
|
+
const previousLength = gauges.samples.length;
|
|
332
|
+
gauges.shapes.delete(name);
|
|
333
|
+
gauges.samples = gauges.samples.filter((sample) => sample.name !== name);
|
|
334
|
+
const count = familyCounts.get(name) ?? 0;
|
|
335
|
+
const remaining = count - (previousLength - gauges.samples.length);
|
|
336
|
+
const reserve = PROMETHEUS_FAMILY_RESERVES[name];
|
|
337
|
+
sharedSeries -= Math.max(0, count - reserve) - Math.max(0, remaining - reserve);
|
|
338
|
+
familyCounts.set(name, remaining);
|
|
110
339
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let sample = histograms.get(key);
|
|
340
|
+
const getHistogram = (name, help, labels, buckets = DURATION_BUCKETS_SECONDS) => {
|
|
341
|
+
const node = series(histograms, name, labels);
|
|
342
|
+
if (!node) return;
|
|
343
|
+
let sample = node.sample;
|
|
116
344
|
if (!sample) {
|
|
117
345
|
sample = {
|
|
118
346
|
buckets,
|
|
119
|
-
bucketPrefixes: [...buckets, "+Inf"].map((le) => `${name}_bucket${formatLabels({
|
|
347
|
+
bucketPrefixes: [...buckets, "+Inf"].map((le) => encodePrefix(`${name}_bucket${formatLabels({
|
|
120
348
|
...labels,
|
|
121
349
|
le: String(le)
|
|
122
|
-
})} `),
|
|
350
|
+
})} `)),
|
|
123
351
|
counts: buckets.map(() => 0),
|
|
124
352
|
count: 0,
|
|
125
353
|
help,
|
|
126
|
-
|
|
127
|
-
|
|
354
|
+
key: metricKey(name, labels),
|
|
355
|
+
name,
|
|
356
|
+
sumPrefix: encodePrefix(`${name}_sum${formatLabels(labels)} `),
|
|
357
|
+
countPrefix: encodePrefix(`${name}_count${formatLabels(labels)} `),
|
|
358
|
+
sum: 0,
|
|
359
|
+
renderedDirty: true
|
|
128
360
|
};
|
|
129
|
-
|
|
361
|
+
node.sample = sample;
|
|
362
|
+
histograms.samples.push(sample);
|
|
363
|
+
histograms.sorted = false;
|
|
130
364
|
}
|
|
365
|
+
return sample;
|
|
366
|
+
};
|
|
367
|
+
const reserveHistogram = (name, help, labels) => {
|
|
368
|
+
getHistogram(name, help, labels);
|
|
369
|
+
};
|
|
370
|
+
const histogram = (name, help, labels, value, buckets = DURATION_BUCKETS_SECONDS) => {
|
|
371
|
+
if (value === void 0 || !Number.isFinite(value)) return;
|
|
372
|
+
const sample = getHistogram(name, help, labels, buckets);
|
|
373
|
+
if (!sample) return;
|
|
131
374
|
sample.count += 1;
|
|
132
375
|
sample.sum += value;
|
|
376
|
+
sample.renderedDirty = true;
|
|
133
377
|
for (let index = 0; index < sample.buckets.length; index += 1) {
|
|
134
378
|
const bucket = sample.buckets[index];
|
|
135
379
|
if (bucket !== void 0 && value <= bucket) sample.counts[index] = (sample.counts[index] ?? 0) + 1;
|
|
136
380
|
}
|
|
137
381
|
};
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
counters: counterSnapshot,
|
|
147
|
-
gauges: [...gauges],
|
|
148
|
-
histograms: [...histograms]
|
|
382
|
+
const render = () => {
|
|
383
|
+
let scratchOffset = 0;
|
|
384
|
+
let totalBytes = 0;
|
|
385
|
+
const chunks = [];
|
|
386
|
+
const append = (chunk) => {
|
|
387
|
+
chunks.push(chunk);
|
|
388
|
+
totalBytes += chunk.length;
|
|
149
389
|
};
|
|
390
|
+
const write = (prefix, value) => {
|
|
391
|
+
const number = formatPrometheusNumber(value);
|
|
392
|
+
const required = scratchOffset + prefix.length + number.length + 1;
|
|
393
|
+
if (required > scratch.length) {
|
|
394
|
+
const grown = Buffer.allocUnsafeSlow(Math.max(required, scratch.length * 2));
|
|
395
|
+
grown.set(scratch.subarray(0, scratchOffset));
|
|
396
|
+
scratch = grown;
|
|
397
|
+
}
|
|
398
|
+
scratch.set(prefix, scratchOffset);
|
|
399
|
+
scratchOffset += prefix.length;
|
|
400
|
+
scratchOffset += scratch.write(number, scratchOffset, "ascii");
|
|
401
|
+
scratch[scratchOffset++] = 10;
|
|
402
|
+
};
|
|
403
|
+
const finish = (previous) => {
|
|
404
|
+
const output = previous?.length === scratchOffset ? previous : new Uint8Array(scratchOffset);
|
|
405
|
+
scratch.copy(output, 0, 0, scratchOffset);
|
|
406
|
+
scratchOffset = 0;
|
|
407
|
+
return output;
|
|
408
|
+
};
|
|
409
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
410
|
+
let previousName;
|
|
411
|
+
const header = (name, type, help) => {
|
|
412
|
+
if (previousName === name) return;
|
|
413
|
+
previousName = name;
|
|
414
|
+
if (!emitted.has(name)) {
|
|
415
|
+
emitted.add(name);
|
|
416
|
+
let cached = headers.get(name);
|
|
417
|
+
if (!cached || cached.type !== type || cached.help !== help) {
|
|
418
|
+
cached = {
|
|
419
|
+
type,
|
|
420
|
+
help,
|
|
421
|
+
bytes: encodePrefix(`# HELP ${name} ${escapeHelp(help)}\n# TYPE ${name} ${type}\n`)
|
|
422
|
+
};
|
|
423
|
+
headers.set(name, cached);
|
|
424
|
+
}
|
|
425
|
+
append(cached.bytes);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
for (const [type, map] of [["counter", counters], ["gauge", gauges]]) {
|
|
429
|
+
for (const sample of sortedSamples(map)) {
|
|
430
|
+
header(sample.name, type, sample.help);
|
|
431
|
+
if (!sample.rendered || sample.renderedDirty) {
|
|
432
|
+
write(sample.prefix, sample.value);
|
|
433
|
+
sample.rendered = finish(sample.rendered);
|
|
434
|
+
sample.renderedDirty = false;
|
|
435
|
+
}
|
|
436
|
+
append(sample.rendered);
|
|
437
|
+
}
|
|
438
|
+
if (type === "counter" && droppedSeries > 0) {
|
|
439
|
+
header(DROPPED_SERIES_COUNTER_NAME, type, "New-series observations dropped because an exporter series budget was reached.");
|
|
440
|
+
write(Buffer.from(`${DROPPED_SERIES_COUNTER_NAME} `), droppedSeries);
|
|
441
|
+
append(finish());
|
|
442
|
+
header(DROPPED_FAMILY_COUNTER_NAME, type, "New-series observations dropped by bounded metric family.");
|
|
443
|
+
for (const [family, count] of familyDrops) write(Buffer.from(`${DROPPED_FAMILY_COUNTER_NAME}${formatLabels({ family })} `), count);
|
|
444
|
+
append(finish());
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const sample of sortedSamples(histograms)) {
|
|
448
|
+
if (sample.count === 0) continue;
|
|
449
|
+
header(sample.name, "histogram", sample.help);
|
|
450
|
+
if (!sample.rendered || sample.renderedDirty) {
|
|
451
|
+
sample.bucketPrefixes.forEach((prefix, bucketIndex) => {
|
|
452
|
+
write(prefix, sample.counts[bucketIndex] ?? sample.count);
|
|
453
|
+
});
|
|
454
|
+
write(sample.sumPrefix, sample.sum);
|
|
455
|
+
write(sample.countPrefix, sample.count);
|
|
456
|
+
sample.rendered = finish(sample.rendered);
|
|
457
|
+
sample.renderedDirty = false;
|
|
458
|
+
}
|
|
459
|
+
append(sample.rendered);
|
|
460
|
+
}
|
|
461
|
+
const output = Buffer.from(new ArrayBuffer(totalBytes));
|
|
462
|
+
let offset = 0;
|
|
463
|
+
for (const chunk of chunks) {
|
|
464
|
+
output.set(chunk, offset);
|
|
465
|
+
offset += chunk.length;
|
|
466
|
+
}
|
|
467
|
+
return output;
|
|
150
468
|
};
|
|
151
469
|
const reset = () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
470
|
+
for (const map of [
|
|
471
|
+
counters,
|
|
472
|
+
gauges,
|
|
473
|
+
histograms
|
|
474
|
+
]) {
|
|
475
|
+
map.samples.length = 0;
|
|
476
|
+
map.shapes.clear();
|
|
477
|
+
}
|
|
155
478
|
droppedSeries = 0;
|
|
479
|
+
sharedSeries = 0;
|
|
480
|
+
familyCounts.clear();
|
|
481
|
+
familyDrops.clear();
|
|
482
|
+
headers.clear();
|
|
483
|
+
scratch = Buffer.alloc(0);
|
|
484
|
+
prefixBuffer = Buffer.alloc(0);
|
|
485
|
+
prefixOffset = 0;
|
|
156
486
|
};
|
|
157
487
|
return {
|
|
158
488
|
counter,
|
|
@@ -160,31 +490,142 @@ function createPrometheusMetricStore() {
|
|
|
160
490
|
gauge,
|
|
161
491
|
clearGauges,
|
|
162
492
|
histogram,
|
|
493
|
+
reserveHistogram,
|
|
163
494
|
reset,
|
|
164
|
-
|
|
495
|
+
render
|
|
165
496
|
};
|
|
166
497
|
}
|
|
167
498
|
//#endregion
|
|
168
|
-
//#region extensions/diagnostics-prometheus/src/service-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
499
|
+
//#region extensions/diagnostics-prometheus/src/service-gateway-ws.ts
|
|
500
|
+
function createGatewayWsMetricsRecorder(store) {
|
|
501
|
+
const labels = { event: "" };
|
|
502
|
+
const frameLabels = {
|
|
503
|
+
event: "",
|
|
504
|
+
le: ""
|
|
505
|
+
};
|
|
506
|
+
const presenceLabels = { mode: "" };
|
|
507
|
+
const sessionLabels = {
|
|
508
|
+
event: "",
|
|
509
|
+
reason: "",
|
|
510
|
+
delivery: ""
|
|
511
|
+
};
|
|
512
|
+
return (event, count, bytes, wireBytes, buckets, bucketLabels, sessionEvent, presenceMode) => {
|
|
513
|
+
labels.event = frameLabels.event = sessionLabels.event = event;
|
|
514
|
+
store.counter("openclaw_gateway_ws_events_total", "Outbound Gateway WebSocket event frames accepted by the sender per socket.", labels, count);
|
|
515
|
+
store.counter("openclaw_gateway_ws_event_bytes_total", "Encoded outbound Gateway WebSocket event bytes before framing or compression per socket.", labels, bytes);
|
|
516
|
+
if (wireBytes !== void 0) store.counter("openclaw_gateway_ws_event_wire_bytes_total", "Outbound Gateway WebSocket payload bytes after compression per socket, excluding framing.", labels, wireBytes);
|
|
517
|
+
if (count > 0) {
|
|
518
|
+
let cumulative = 0;
|
|
519
|
+
for (let i = 0; i < bucketLabels.length; i++) {
|
|
520
|
+
cumulative += buckets[i];
|
|
521
|
+
frameLabels.le = bucketLabels[i];
|
|
522
|
+
(cumulative === 0 ? store.counterValue : store.counter)("openclaw_gateway_ws_event_frame_bytes", "Cumulative counts of accepted raw Gateway event frames by byte-size upper bound.", frameLabels, cumulative);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (presenceMode) {
|
|
526
|
+
presenceLabels.mode = presenceMode;
|
|
527
|
+
store.counter("openclaw_gateway_ws_presence_frames_total", "Outbound presence frames accepted by the sender per socket, by delivery mode.", presenceLabels, count);
|
|
528
|
+
store.counter("openclaw_gateway_ws_presence_bytes_total", "Encoded outbound presence bytes before framing or compression per socket, by delivery mode.", presenceLabels, bytes);
|
|
529
|
+
}
|
|
530
|
+
if (sessionEvent) {
|
|
531
|
+
sessionLabels.reason = sessionEvent.reason;
|
|
532
|
+
sessionLabels.delivery = sessionEvent.delivery;
|
|
533
|
+
store.counter("openclaw_gateway_ws_session_event_frames_total", "Outbound session event frames by reason and exact message subscription per socket.", sessionLabels, count);
|
|
534
|
+
store.counter("openclaw_gateway_ws_session_event_bytes_total", "Encoded outbound session event bytes by reason and exact message subscription per socket.", sessionLabels, bytes);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function recordGatewayWsConnections(store, operators) {
|
|
539
|
+
store.gauge("openclaw_gateway_ws_operator_connections", "Connected authenticated Gateway operator WebSockets.", {}, operators);
|
|
175
540
|
}
|
|
176
541
|
//#endregion
|
|
177
542
|
//#region extensions/diagnostics-prometheus/src/service-memory.ts
|
|
543
|
+
const WORKER_MEMORY_METRICS = [
|
|
544
|
+
[
|
|
545
|
+
"heapUsed",
|
|
546
|
+
"openclaw_worker_heap_used_bytes",
|
|
547
|
+
"heap usage"
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
"external",
|
|
551
|
+
"openclaw_worker_external_bytes",
|
|
552
|
+
"external memory"
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
"arrayBuffers",
|
|
556
|
+
"openclaw_worker_array_buffers_bytes",
|
|
557
|
+
"ArrayBuffer memory (included in external)"
|
|
558
|
+
]
|
|
559
|
+
];
|
|
560
|
+
const HEAP_HEADROOM_METRICS = [
|
|
561
|
+
[
|
|
562
|
+
"limit",
|
|
563
|
+
"openclaw_heap_size_limit_bytes",
|
|
564
|
+
"Smallest live isolate V8 heap limit by thread group."
|
|
565
|
+
],
|
|
566
|
+
[
|
|
567
|
+
"usedRatio",
|
|
568
|
+
"openclaw_heap_used_ratio",
|
|
569
|
+
"Largest live isolate used/limit ratio by thread group."
|
|
570
|
+
],
|
|
571
|
+
[
|
|
572
|
+
"oldBytes",
|
|
573
|
+
"openclaw_heap_old_generation_after_gc_bytes",
|
|
574
|
+
"Largest live isolate old_space plus large_object_space usage at its last observed major GC."
|
|
575
|
+
],
|
|
576
|
+
[
|
|
577
|
+
"oldRatio",
|
|
578
|
+
"openclaw_heap_old_generation_after_gc_ratio",
|
|
579
|
+
"Largest live isolate post-major-GC old-generation/heap-limit ratio by thread group."
|
|
580
|
+
]
|
|
581
|
+
];
|
|
582
|
+
function collectHeapHeadroom(headroom, used, limit, oldBytes) {
|
|
583
|
+
if (limit === void 0 || limit <= 0) return;
|
|
584
|
+
headroom.limit = Math.min(headroom.limit ?? limit, limit);
|
|
585
|
+
headroom.usedRatio = Math.max(headroom.usedRatio ?? 0, used / limit);
|
|
586
|
+
if (oldBytes !== void 0) {
|
|
587
|
+
headroom.oldBytes = Math.max(headroom.oldBytes ?? 0, oldBytes);
|
|
588
|
+
headroom.oldRatio = Math.max(headroom.oldRatio ?? 0, oldBytes / limit);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function recordHeapHeadroom(store, thread, headroom) {
|
|
592
|
+
for (const [field, name, description] of HEAP_HEADROOM_METRICS) store.gauge(name, description, { thread }, headroom[field]);
|
|
593
|
+
}
|
|
178
594
|
function recordMemorySample(store, memory, byteBuckets) {
|
|
595
|
+
for (const [, name] of HEAP_HEADROOM_METRICS) store.clearGauges(name);
|
|
596
|
+
const mainHeadroom = {};
|
|
597
|
+
collectHeapHeadroom(mainHeadroom, memory.heapUsedBytes, memory.heapSizeLimitBytes, memory.oldGenerationAfterGcBytes);
|
|
598
|
+
recordHeapHeadroom(store, "main", mainHeadroom);
|
|
599
|
+
store.clearGauges("openclaw_process_anonymous_memory_bytes");
|
|
600
|
+
for (const [kind, field] of [["rss", "anonymousRssBytes"], ["swap", "anonymousSwapBytes"]]) store.gauge("openclaw_process_anonymous_memory_bytes", "Latest Linux process anonymous RSS and swap; includes managed heaps and buffers, not allocator totals.", { kind }, asNonNegativeFiniteNumber(memory[field]));
|
|
179
601
|
for (const message of memory.workerMessages ?? []) {
|
|
180
602
|
const labels = {
|
|
181
603
|
kind: message.kind,
|
|
182
604
|
direction: message.direction
|
|
183
605
|
};
|
|
184
606
|
store.counterValue("openclaw_worker_messages_total", "Messages crossing the Gateway main-thread worker boundary.", labels, message.messages);
|
|
185
|
-
store.counterValue("openclaw_worker_message_payload_bytes_total", "Known worker payload bytes, excluding transport framing and unmeasured object graphs.",
|
|
607
|
+
for (const [requestClass, bytes] of Object.entries(message.payloadBytesByRequestClass ?? { [message.kind === "sqlite_request" || message.kind === "sqlite_reply" || message.kind === "authority" ? "other" : ""]: message.payloadBytes })) store.counterValue("openclaw_worker_message_payload_bytes_total", "Known worker payload bytes, excluding transport framing and unmeasured object graphs.", requestClass ? {
|
|
608
|
+
...labels,
|
|
609
|
+
request_class: requestClass
|
|
610
|
+
} : labels, bytes);
|
|
186
611
|
store.counterValue("openclaw_worker_message_unmeasured_total", "Worker messages without a known payload size; these do not contribute to payload bytes.", labels, message.unmeasuredMessages);
|
|
187
612
|
}
|
|
613
|
+
for (const selection of memory.sessionRowSelections?.selections ?? []) {
|
|
614
|
+
const labels = {
|
|
615
|
+
kind: selection.kind,
|
|
616
|
+
task: selection.task
|
|
617
|
+
};
|
|
618
|
+
store.counterValue("openclaw_session_row_selections_total", "Successful resident session-row selections by query shape and enclosing task.", labels, selection.calls);
|
|
619
|
+
store.counterValue("openclaw_session_row_selection_rows_total", "Resident session rows returned by selections, before federation and visibility filtering.", labels, selection.rows);
|
|
620
|
+
}
|
|
621
|
+
if (memory.sessionRowSelections) {
|
|
622
|
+
for (const partition of memory.sessionRowSelections.partitions) for (const outcome of ["hits", "misses"]) store.counterValue("openclaw_session_row_partition_cache_total", "Retained session-list partition lookups by outcome and enclosing selection task.", {
|
|
623
|
+
task: partition.task,
|
|
624
|
+
outcome: outcome === "hits" ? "hit" : "miss"
|
|
625
|
+
}, partition[outcome]);
|
|
626
|
+
for (const revision of memory.sessionRowSelections.partitionRevisions) store.counterValue("openclaw_session_row_partition_revisions_total", "Session-list partition revisions by synchronous internal change scope; excludes initial partitions.", { reason: revision.reason }, revision.changes);
|
|
627
|
+
store.counterValue("openclaw_session_row_selection_resets_total", "Broad session-row selection invalidations, including topology replacement and disposal.", {}, memory.sessionRowSelections.resets);
|
|
628
|
+
}
|
|
188
629
|
for (const worker of memory.workerLifecycle ?? []) {
|
|
189
630
|
store.counterValue("openclaw_worker_started_total", "Worker isolates created by bounded script basename.", { script: worker.script }, worker.started);
|
|
190
631
|
for (const { reason, count } of worker.retired) store.counterValue("openclaw_worker_retired_total", "Worker isolates confirmed exited by owner retirement reason.", {
|
|
@@ -192,6 +633,23 @@ function recordMemorySample(store, memory, byteBuckets) {
|
|
|
192
633
|
reason
|
|
193
634
|
}, count);
|
|
194
635
|
}
|
|
636
|
+
for (const worker of memory.workerTaskMetrics ?? []) {
|
|
637
|
+
const labels = {
|
|
638
|
+
worker_class: worker.workerClass,
|
|
639
|
+
script: worker.script
|
|
640
|
+
};
|
|
641
|
+
store.counterValue("openclaw_worker_tasks_completed_total", "Observed terminal task replies from pool workers, including handled failures.", labels, worker.tasksCompleted);
|
|
642
|
+
store.gauge("openclaw_worker_heap_used_high_water_bytes", "Largest observed pool worker heap usage, retained across worker retirement.", labels, worker.heapUsedHighWaterBytes);
|
|
643
|
+
for (const gc of worker.gc) {
|
|
644
|
+
const gcLabels = {
|
|
645
|
+
...labels,
|
|
646
|
+
kind: gc.kind
|
|
647
|
+
};
|
|
648
|
+
store.counterValue("openclaw_worker_gc_duration_seconds_total", "Observed pool worker garbage collection duration, retained across worker retirement.", gcLabels, gc.durationMs / 1e3);
|
|
649
|
+
store.counterValue("openclaw_worker_gc_collections_total", "Observed pool worker garbage collections, retained across worker retirement.", gcLabels, gc.count);
|
|
650
|
+
store.counterValue("openclaw_worker_gc_reclaimed_bytes_total", "Sum of positive heap usage decreases across observed pool worker garbage collections.", gcLabels, gc.reclaimedBytes);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
195
653
|
for (const [kind, field] of [
|
|
196
654
|
["rss", "rssBytes"],
|
|
197
655
|
["heap_total", "heapTotalBytes"],
|
|
@@ -217,36 +675,54 @@ function recordMemorySample(store, memory, byteBuckets) {
|
|
|
217
675
|
}, space[field]);
|
|
218
676
|
const workers = /* @__PURE__ */ new Map();
|
|
219
677
|
store.clearGauges("openclaw_code_mode_worker_heap_bytes");
|
|
678
|
+
store.clearGauges("openclaw_code_mode_worker_memory_bytes");
|
|
220
679
|
for (const worker of memory.workerHeaps ?? []) {
|
|
221
|
-
if (worker.workerSlot !== void 0)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
680
|
+
if (worker.workerSlot !== void 0) {
|
|
681
|
+
for (const [kind, value] of [["used", worker.heapUsed], ["limit", worker.heapSizeLimitBytes]]) store.gauge("openclaw_code_mode_worker_heap_bytes", "Latest live Code Mode isolate heap usage and limit by reusable worker slot.", {
|
|
682
|
+
slot: String(worker.workerSlot),
|
|
683
|
+
kind
|
|
684
|
+
}, value);
|
|
685
|
+
for (const [kind, value] of [["external", worker.external], ["array_buffers", worker.arrayBuffers]]) store.gauge("openclaw_code_mode_worker_memory_bytes", "Latest live Code Mode isolate external and ArrayBuffer memory (included in external) by reusable worker slot.", {
|
|
686
|
+
slot: String(worker.workerSlot),
|
|
687
|
+
kind
|
|
688
|
+
}, value);
|
|
689
|
+
}
|
|
225
690
|
const totals = workers.get(worker.script) ?? {
|
|
226
691
|
count: 0,
|
|
227
|
-
sampled: 0
|
|
228
|
-
heapUsed: void 0
|
|
692
|
+
sampled: 0
|
|
229
693
|
};
|
|
230
694
|
totals.count++;
|
|
231
695
|
totals.sampled++;
|
|
232
|
-
|
|
233
|
-
|
|
696
|
+
collectHeapHeadroom(totals, worker.heapUsed, worker.heapSizeLimitBytes, worker.oldGenerationAfterGcBytes);
|
|
697
|
+
for (const [field] of WORKER_MEMORY_METRICS) {
|
|
698
|
+
const value = worker[field];
|
|
699
|
+
if (value !== void 0) totals[field] = (totals[field] ?? 0) + value;
|
|
700
|
+
}
|
|
701
|
+
for (const space of worker.heapSpaces ?? []) {
|
|
702
|
+
totals.heapSpaces ??= /* @__PURE__ */ new Map();
|
|
703
|
+
totals.heapSpaces.set(space.space_name, (totals.heapSpaces.get(space.space_name) ?? 0) + space.space_used_size);
|
|
704
|
+
}
|
|
234
705
|
workers.set(worker.script, totals);
|
|
235
706
|
}
|
|
236
707
|
for (const worker of memory.workerMemoryMissing ?? []) {
|
|
237
708
|
const totals = workers.get(worker.script) ?? {
|
|
238
709
|
count: 0,
|
|
239
|
-
sampled: 0
|
|
240
|
-
heapUsed: void 0
|
|
710
|
+
sampled: 0
|
|
241
711
|
};
|
|
242
712
|
totals.count++;
|
|
243
713
|
workers.set(worker.script, totals);
|
|
244
714
|
}
|
|
245
|
-
store.clearGauges(
|
|
715
|
+
for (const [, name] of WORKER_MEMORY_METRICS) store.clearGauges(name);
|
|
716
|
+
store.clearGauges("openclaw_worker_heap_space_used_bytes");
|
|
246
717
|
for (const [script, totals] of workers) {
|
|
718
|
+
recordHeapHeadroom(store, script, totals);
|
|
247
719
|
store.gauge("openclaw_worker_count", "Worker isolate counts.", { script }, totals.count);
|
|
248
720
|
store.gauge("openclaw_worker_heap_sampled_count", "Worker isolate counts.", { script }, totals.sampled);
|
|
249
|
-
store.gauge(
|
|
721
|
+
for (const [field, name, description] of WORKER_MEMORY_METRICS) store.gauge(name, `Latest live Worker ${description} by bounded script basename.`, { script }, totals[field]);
|
|
722
|
+
for (const [space, bytes] of totals.heapSpaces ?? []) store.gauge("openclaw_worker_heap_space_used_bytes", "Latest live Worker V8 heap space usage by bounded script basename.", {
|
|
723
|
+
script,
|
|
724
|
+
space
|
|
725
|
+
}, bytes);
|
|
250
726
|
}
|
|
251
727
|
store.histogram("openclaw_memory_rss_bytes", "RSS memory sample distribution in bytes.", {}, asNonNegativeFiniteNumber(memory.rssBytes), byteBuckets);
|
|
252
728
|
}
|
|
@@ -297,6 +773,20 @@ function recordModelUsage(store, evt) {
|
|
|
297
773
|
}
|
|
298
774
|
//#endregion
|
|
299
775
|
//#region extensions/diagnostics-prometheus/src/service-operation-timing.ts
|
|
776
|
+
const GC_DURATION_NAME = "openclaw_gc_duration_seconds";
|
|
777
|
+
const GC_DURATION_HELP = "Elapsed garbage collection duration in seconds by collection kind, callback flags, and isolate role.";
|
|
778
|
+
function reserveMainGcHistograms(store) {
|
|
779
|
+
for (const kind of [
|
|
780
|
+
"scavenge",
|
|
781
|
+
"mark_sweep_compact",
|
|
782
|
+
"incremental",
|
|
783
|
+
"weak_processing"
|
|
784
|
+
]) store.reserveHistogram(GC_DURATION_NAME, GC_DURATION_HELP, {
|
|
785
|
+
kind,
|
|
786
|
+
reason: "other",
|
|
787
|
+
thread: "main"
|
|
788
|
+
});
|
|
789
|
+
}
|
|
300
790
|
const RPC_DURATION_BUCKETS_SECONDS = [
|
|
301
791
|
.005,
|
|
302
792
|
.01,
|
|
@@ -341,6 +831,14 @@ const RPC_DURATION_BUCKETS_SECONDS = [
|
|
|
341
831
|
600
|
|
342
832
|
];
|
|
343
833
|
const RESPONSE_BYTE_BUCKETS = Array.from({ length: 17 }, (_, index) => 1024 * 2 ** index);
|
|
834
|
+
const MAIN_THREAD_STALL_BUCKETS_SECONDS = [
|
|
835
|
+
.25,
|
|
836
|
+
.5,
|
|
837
|
+
1,
|
|
838
|
+
2.5,
|
|
839
|
+
5,
|
|
840
|
+
10
|
|
841
|
+
];
|
|
344
842
|
const HEAP_DELTA_BYTE_BUCKETS = [
|
|
345
843
|
...RESPONSE_BYTE_BUCKETS.toReversed().map((bytes) => -bytes),
|
|
346
844
|
0,
|
|
@@ -356,9 +854,41 @@ const CATALOG_LIST_PHASES = /* @__PURE__ */ new Set([
|
|
|
356
854
|
"projection_final",
|
|
357
855
|
"delivery"
|
|
358
856
|
]);
|
|
857
|
+
const CHAT_STARTUP_PHASE = /^chat\.startup\.(session_entry|pending_inputs|history_page|startup_projection|projection_final|session_info)$/;
|
|
858
|
+
const DIFF_CACHE_MISS_REASONS = /* @__PURE__ */ new Set([
|
|
859
|
+
"cold",
|
|
860
|
+
"evicted",
|
|
861
|
+
"pending",
|
|
862
|
+
"no-revision",
|
|
863
|
+
"no-revision-environment",
|
|
864
|
+
"no-revision-platform",
|
|
865
|
+
"no-revision-metadata",
|
|
866
|
+
"no-revision-git-installation",
|
|
867
|
+
"no-revision-config",
|
|
868
|
+
"no-revision-reftable",
|
|
869
|
+
"no-revision-alternates",
|
|
870
|
+
"no-revision-symlink",
|
|
871
|
+
"no-revision-nested-repository",
|
|
872
|
+
"no-revision-status",
|
|
873
|
+
"no-revision-unmerged",
|
|
874
|
+
"no-revision-submodule",
|
|
875
|
+
"no-revision-permission",
|
|
876
|
+
"no-revision-missing",
|
|
877
|
+
"no-revision-io",
|
|
878
|
+
"oversized",
|
|
879
|
+
"revision-changed-head",
|
|
880
|
+
"revision-changed-index",
|
|
881
|
+
"revision-changed-untracked-set",
|
|
882
|
+
"revision-changed-worktree-mtime",
|
|
883
|
+
"revision-changed-metadata"
|
|
884
|
+
]);
|
|
359
885
|
const CHAT_SEND_PHASE = /^chat\.send\.(authority|admission|preparation|attachments|replyContext|authoring|persist|runAdmission|replyInitialization|snapshot|worktree|effects|response|dispatch)$/;
|
|
886
|
+
const CONTROL_UI_DOCUMENT_STAGE = /^control-ui\.document\.(auth_grants|identity_avatar|plugin_catalog|git_branch|preferences|render|total)$/;
|
|
360
887
|
function recordOperationTimingEvent(store, evt, metadata) {
|
|
361
888
|
switch (evt.type) {
|
|
889
|
+
case "sessions.list.baseline.retired":
|
|
890
|
+
if (metadata.trusted) store.counter("openclaw_sessions_list_baseline_retirements_total", "Session list baseline retirement events by cause, not rows or views retired.", { cause: evt.cause });
|
|
891
|
+
return;
|
|
362
892
|
case "secret.egress":
|
|
363
893
|
store.counter("openclaw_secret_egress_requests_total", "Secret egress proxy requests by outcome and credential substitution.", {
|
|
364
894
|
outcome: evt.outcome,
|
|
@@ -378,20 +908,87 @@ function recordOperationTimingEvent(store, evt, metadata) {
|
|
|
378
908
|
store.counter("openclaw_gateway_http_cancelled_total", "Gateway HTTP requests cancelled before completion.", { source: evt.source });
|
|
379
909
|
return;
|
|
380
910
|
case "diagnostic.gc":
|
|
381
|
-
|
|
911
|
+
if (evt.kind === "mark_sweep_compact") {
|
|
912
|
+
const labels = { thread: evt.thread ?? "main" };
|
|
913
|
+
store.gauge("openclaw_heap_used_after_major_gc_bytes", "Native used heap after the latest received major collection by isolate role.", labels, evt.heapUsedAfterMajorGcBytes);
|
|
914
|
+
store.gauge("openclaw_heap_used_after_major_gc_timestamp_seconds", "Unix observation time of the latest received native major-GC heap sample.", labels, evt.heapUsedAfterMajorGcTimestampSeconds);
|
|
915
|
+
}
|
|
916
|
+
store.histogram(GC_DURATION_NAME, GC_DURATION_HELP, {
|
|
382
917
|
kind: evt.kind ?? "unknown",
|
|
918
|
+
reason: evt.reason ?? "other",
|
|
383
919
|
thread: evt.thread ?? "main",
|
|
384
920
|
...evt.workerSlot === void 0 ? {} : { slot: String(evt.workerSlot) }
|
|
385
921
|
}, seconds(evt.durationMs));
|
|
386
922
|
return;
|
|
923
|
+
case "gateway.main_thread.stall":
|
|
924
|
+
for (const stall of evt.stalls) store.histogram("openclaw_gateway_main_thread_stall_seconds", "Elapsed main-thread tasks lasting at least 250 milliseconds, by work scope.", { task: normalizeDiagnosticValue(stall.task) }, seconds(stall.durationMs), MAIN_THREAD_STALL_BUCKETS_SECONDS);
|
|
925
|
+
store.counter("openclaw_gateway_main_thread_stalls_dropped_total", "Main-thread stall records dropped because the attribution buffer was full.", {}, evt.dropped);
|
|
926
|
+
return;
|
|
387
927
|
case "gateway.event_loop.sample":
|
|
388
928
|
store.histogram("openclaw_gateway_event_loop_delay_max_seconds", "Maximum event-loop delay per completed Gateway observation window in seconds.", {}, seconds(evt.delayMaxMs));
|
|
389
929
|
store.counter("openclaw_gateway_event_loop_observed_seconds_total", "Elapsed seconds covered by completed Gateway event-loop observation windows.", {}, evt.intervalMs / 1e3);
|
|
390
930
|
return;
|
|
391
931
|
case "diagnostic.phase.completed": {
|
|
392
932
|
if (!metadata.trusted) return;
|
|
933
|
+
const documentStage = CONTROL_UI_DOCUMENT_STAGE.exec(evt.name)?.[1];
|
|
934
|
+
if (documentStage) {
|
|
935
|
+
store.histogram("openclaw_control_ui_document_duration_seconds", "Elapsed Control UI HTML document handler stages in seconds; stages can overlap.", { stage: documentStage }, seconds(evt.durationMs));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (evt.name === "sessions.diff" || evt.name === "checkout.diff") {
|
|
939
|
+
const outcome = evt.details?.outcome;
|
|
940
|
+
if (outcome !== "cache-hit" && outcome !== "cache-miss") return;
|
|
941
|
+
const reason = evt.details?.missReason;
|
|
942
|
+
if (outcome === "cache-miss" && typeof reason === "string" && DIFF_CACHE_MISS_REASONS.has(reason)) store.counter("openclaw_diff_cache_misses_total", "Local diff cache misses by bounded reason; pending reads join an existing load.", {
|
|
943
|
+
method: evt.name,
|
|
944
|
+
reason
|
|
945
|
+
});
|
|
946
|
+
for (const stage of [
|
|
947
|
+
"admission",
|
|
948
|
+
"checkout-resolve",
|
|
949
|
+
"cache-validate",
|
|
950
|
+
"worker-queue",
|
|
951
|
+
"git-wall",
|
|
952
|
+
"encode"
|
|
953
|
+
]) {
|
|
954
|
+
const elapsed = evt.details?.[stage];
|
|
955
|
+
store.histogram("openclaw_diff_stage_seconds", "Elapsed local diff stages; cache validation includes its worker queue and Git wall time.", {
|
|
956
|
+
method: evt.name,
|
|
957
|
+
stage,
|
|
958
|
+
outcome
|
|
959
|
+
}, typeof elapsed === "number" ? seconds(elapsed) : void 0, RPC_DURATION_BUCKETS_SECONDS);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
393
963
|
if (evt.name === "worktree.preparation") {
|
|
394
|
-
|
|
964
|
+
const { kind, template, outcome } = evt.details ?? {};
|
|
965
|
+
if (kind !== "managed" && kind !== "sandbox" || ![
|
|
966
|
+
"warm",
|
|
967
|
+
"cold",
|
|
968
|
+
"unavailable",
|
|
969
|
+
"reused"
|
|
970
|
+
].includes(String(template)) || outcome !== "returned" && outcome !== "threw") return;
|
|
971
|
+
for (const preparationPhase of [
|
|
972
|
+
"total",
|
|
973
|
+
"allocate",
|
|
974
|
+
"checkout",
|
|
975
|
+
"setup",
|
|
976
|
+
"templatePrepare",
|
|
977
|
+
"templateApply",
|
|
978
|
+
"snapshot",
|
|
979
|
+
"synchronizeCanonical",
|
|
980
|
+
"synchronizeProjection",
|
|
981
|
+
"containerStart",
|
|
982
|
+
"workspaceLayout"
|
|
983
|
+
]) {
|
|
984
|
+
const elapsed = preparationPhase === "total" ? evt.durationMs : evt.details?.[preparationPhase];
|
|
985
|
+
store.histogram("openclaw_worktree_preparation_seconds", "Elapsed managed worktree preparation time; nested phases are inclusive.", {
|
|
986
|
+
kind,
|
|
987
|
+
template: String(template),
|
|
988
|
+
outcome,
|
|
989
|
+
phase: preparationPhase
|
|
990
|
+
}, typeof elapsed === "number" ? seconds(elapsed) : void 0);
|
|
991
|
+
}
|
|
395
992
|
return;
|
|
396
993
|
}
|
|
397
994
|
const sendPhase = CHAT_SEND_PHASE.exec(evt.name)?.[1];
|
|
@@ -403,6 +1000,14 @@ function recordOperationTimingEvent(store, evt, metadata) {
|
|
|
403
1000
|
}, seconds(evt.durationMs));
|
|
404
1001
|
return;
|
|
405
1002
|
}
|
|
1003
|
+
const startupPhase = CHAT_STARTUP_PHASE.exec(evt.name)?.[1];
|
|
1004
|
+
if (startupPhase) {
|
|
1005
|
+
store.histogram("openclaw_gateway_rpc_stage_seconds", "Elapsed time for completed Gateway RPC owner stages.", {
|
|
1006
|
+
method: "chat.startup",
|
|
1007
|
+
phase: startupPhase
|
|
1008
|
+
}, seconds(evt.durationMs));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
406
1011
|
if (!evt.name.startsWith(CATALOG_LIST_PHASE_PREFIX)) return;
|
|
407
1012
|
const phase = evt.name.slice(CATALOG_LIST_PHASE_PREFIX.length);
|
|
408
1013
|
const duration = seconds(evt.durationMs);
|
|
@@ -420,6 +1025,10 @@ function recordOperationTimingEvent(store, evt, metadata) {
|
|
|
420
1025
|
const labels = { method: evt.method };
|
|
421
1026
|
if (evt.phase === "received") {
|
|
422
1027
|
store.counter("openclaw_gateway_rpc_requests_total", "Authenticated Gateway WebSocket requests received.", labels);
|
|
1028
|
+
store.counter("openclaw_gateway_rpc_client_requests_total", "Gateway RPC requests by bounded handshake client kind.", {
|
|
1029
|
+
method: evt.method,
|
|
1030
|
+
client_kind: evt.clientKind ?? "other"
|
|
1031
|
+
});
|
|
423
1032
|
if (evt.method === "models.list" && evt.modelsList) {
|
|
424
1033
|
const catalog = evt.modelsList;
|
|
425
1034
|
store.counter("openclaw_models_list_requests_total", "Model catalog requests by client kind and bounded request shape.", {
|
|
@@ -431,10 +1040,54 @@ function recordOperationTimingEvent(store, evt, metadata) {
|
|
|
431
1040
|
prepared_only: String(catalog.preparedOnly)
|
|
432
1041
|
});
|
|
433
1042
|
}
|
|
1043
|
+
if (evt.method === "sessions.list" && evt.sessionsList) {
|
|
1044
|
+
const sessions = evt.sessionsList;
|
|
1045
|
+
store.counter("openclaw_sessions_list_requests_total", "Session list requests by client kind and bounded request shape.", {
|
|
1046
|
+
client_kind: sessions.clientKind,
|
|
1047
|
+
source: sessions.source,
|
|
1048
|
+
caller_source: sessions.callerSource,
|
|
1049
|
+
row_mode: sessions.rowMode,
|
|
1050
|
+
filter: sessions.filter
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (evt.method === "cron.list" && evt.cronList) {
|
|
1054
|
+
const cron = evt.cronList;
|
|
1055
|
+
store.counter("openclaw_cron_list_requests_total", "Cron list requests by client kind and bounded request shape.", {
|
|
1056
|
+
client_kind: cron.clientKind,
|
|
1057
|
+
source: cron.source,
|
|
1058
|
+
scope: cron.scope,
|
|
1059
|
+
compact: String(cron.compact),
|
|
1060
|
+
page: cron.page
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
434
1063
|
return;
|
|
435
1064
|
}
|
|
436
1065
|
if (evt.phase === "response" && (evt.outcome === "ok" || evt.outcome === "error")) {
|
|
1066
|
+
if (evt.outcome === "ok" && evt.firstResponse !== false && evt.sessionsList?.baseMissReason) store.counter("openclaw_sessions_list_base_misses_total", "Successful full sidebar responses with a supplied base, by miss reason.", { reason: evt.sessionsList.baseMissReason });
|
|
437
1067
|
store.histogram("openclaw_gateway_rpc_response_bytes", "Encoded Gateway RPC response frame size in bytes, including later frames.", labels, evt.responseBytes, RESPONSE_BYTE_BUCKETS);
|
|
1068
|
+
if (evt.responseBytes !== void 0 && evt.sessionsList) {
|
|
1069
|
+
const sessions = evt.sessionsList;
|
|
1070
|
+
store.counter("openclaw_sessions_list_response_bytes_total", "Encoded successful session list response bytes by request shape and row outcome.", {
|
|
1071
|
+
client_kind: sessions.clientKind,
|
|
1072
|
+
source: sessions.source,
|
|
1073
|
+
caller_source: sessions.callerSource,
|
|
1074
|
+
row_mode: sessions.rowMode,
|
|
1075
|
+
filter: sessions.filter,
|
|
1076
|
+
rows_outcome: sessions.rowsOutcome
|
|
1077
|
+
}, evt.responseBytes);
|
|
1078
|
+
}
|
|
1079
|
+
if (evt.chatHistory) {
|
|
1080
|
+
const history = evt.chatHistory;
|
|
1081
|
+
const historyLabels = {
|
|
1082
|
+
client_kind: history.clientKind,
|
|
1083
|
+
source: history.source,
|
|
1084
|
+
shape: history.shape,
|
|
1085
|
+
limit_bucket: history.limitBucket,
|
|
1086
|
+
outcome: history.outcome
|
|
1087
|
+
};
|
|
1088
|
+
if (evt.firstResponse !== false) store.counter("openclaw_chat_history_requests_total", "Successful chat history requests by request shape and response outcome.", historyLabels);
|
|
1089
|
+
if (evt.responseBytes !== void 0) store.counter("openclaw_chat_history_response_bytes_total", "Encoded successful chat history response bytes by request shape and response outcome.", historyLabels, evt.responseBytes);
|
|
1090
|
+
}
|
|
438
1091
|
if (evt.firstResponse === false) return;
|
|
439
1092
|
}
|
|
440
1093
|
store.counter("openclaw_gateway_rpc_outcomes_total", "Gateway RPC observations by phase and outcome.", {
|
|
@@ -451,53 +1104,24 @@ function recordOperationTimingEvent(store, evt, metadata) {
|
|
|
451
1104
|
}
|
|
452
1105
|
}
|
|
453
1106
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
"
|
|
459
|
-
|
|
460
|
-
"reused"
|
|
461
|
-
].includes(String(template)) || outcome !== "returned" && outcome !== "threw") return;
|
|
462
|
-
for (const phase of [
|
|
463
|
-
"total",
|
|
464
|
-
"allocate",
|
|
465
|
-
"checkout",
|
|
466
|
-
"setup",
|
|
467
|
-
"templatePrepare",
|
|
468
|
-
"templateApply",
|
|
469
|
-
"snapshot",
|
|
470
|
-
"synchronizeCanonical",
|
|
471
|
-
"synchronizeProjection",
|
|
472
|
-
"containerStart",
|
|
473
|
-
"workspaceLayout"
|
|
474
|
-
]) {
|
|
475
|
-
const elapsed = phase === "total" ? evt.durationMs : evt.details?.[phase];
|
|
476
|
-
store.histogram("openclaw_worktree_preparation_seconds", "Elapsed managed worktree preparation time; nested phases are inclusive.", {
|
|
477
|
-
kind,
|
|
478
|
-
template: String(template),
|
|
479
|
-
outcome,
|
|
480
|
-
phase
|
|
481
|
-
}, typeof elapsed === "number" ? seconds(elapsed) : void 0);
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region extensions/diagnostics-prometheus/src/service-retention.ts
|
|
1109
|
+
function recordSessionRetention(store, evt) {
|
|
1110
|
+
if (evt.type === "sessions.event.rows.reset") {
|
|
1111
|
+
store.counter("openclaw_session_event_rows_reset_total", "Retained session-event wire baselines retired by cause.", { cause: evt.cause }, evt.count);
|
|
1112
|
+
return;
|
|
482
1113
|
}
|
|
1114
|
+
if (evt.type === "sessions.list.retention") {
|
|
1115
|
+
store.gauge("openclaw_sessions_list_retained_views", "Retained session-list views across all publications.", {}, evt.views);
|
|
1116
|
+
store.gauge("openclaw_sessions_list_retained_refs", "Row references and selection entries retained by session-list views.", {}, evt.refs);
|
|
1117
|
+
store.gauge("openclaw_sessions_list_retained_bytes", "Estimated memory retained by session-list views.", {}, evt.estimatedBytes);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
store.gauge("openclaw_session_event_rows_retained_entries", "Session-event field-hash vectors retained across all connections.", {}, evt.entries);
|
|
1121
|
+
store.gauge("openclaw_session_event_rows_retained_bytes", "Estimated memory retained by session-event field-hash vectors.", {}, evt.estimatedBytes);
|
|
483
1122
|
}
|
|
484
1123
|
//#endregion
|
|
485
1124
|
//#region extensions/diagnostics-prometheus/src/service-sessions.ts
|
|
486
|
-
function createGatewayWorkMetricsRecorder(store) {
|
|
487
|
-
return (snapshot) => {
|
|
488
|
-
if (!snapshot) {
|
|
489
|
-
store.clearGauges("openclaw_sessions_active");
|
|
490
|
-
store.clearGauges("openclaw_gateway_active_work");
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
for (const state of ["running", "queued"]) store.gauge("openclaw_sessions_active", "Current active sessions in the Gateway session roster by run state.", { state }, snapshot.sessions[state]);
|
|
494
|
-
for (const kind of [
|
|
495
|
-
"agentRuns",
|
|
496
|
-
"chatRuns",
|
|
497
|
-
"queuedTurns"
|
|
498
|
-
]) store.gauge("openclaw_gateway_active_work", "Current Gateway work by owner; categories may overlap.", { kind }, snapshot.work[kind]);
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
1125
|
function recordSessionDiagnosticEvent(store, evt) {
|
|
502
1126
|
switch (evt.type) {
|
|
503
1127
|
case "session.recovery.requested":
|
|
@@ -537,60 +1161,62 @@ function recordSessionDiagnosticEvent(store, evt) {
|
|
|
537
1161
|
}
|
|
538
1162
|
//#endregion
|
|
539
1163
|
//#region extensions/diagnostics-prometheus/src/service-worker.ts
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1164
|
+
function createWorkerRequestMetricsRecorder(store) {
|
|
1165
|
+
return (kind, requestClass, requestPhase, queueDepth, queueWaitMs, durationMs, timing) => {
|
|
1166
|
+
store.gauge("openclaw_worker_queue_depth", "Requests awaiting dispatch across worker owners of this kind.", { kind }, queueDepth);
|
|
1167
|
+
const labels = {
|
|
1168
|
+
kind,
|
|
1169
|
+
request_class: requestClass
|
|
1170
|
+
};
|
|
1171
|
+
store.histogram("openclaw_worker_queue_wait_seconds", "Elapsed time from worker request enqueue to dispatch.", labels, seconds(queueWaitMs));
|
|
1172
|
+
store.histogram("openclaw_worker_request_seconds", "Elapsed time from worker dispatch to reply or failure, including preparation and transport.", labels, seconds(durationMs));
|
|
1173
|
+
if (requestPhase !== "completed" || !timing) return;
|
|
1174
|
+
for (const [phase, start, end] of [
|
|
1175
|
+
[
|
|
1176
|
+
"dispatch",
|
|
1177
|
+
"dispatched",
|
|
1178
|
+
"posted"
|
|
1179
|
+
],
|
|
1180
|
+
[
|
|
1181
|
+
"delivery",
|
|
1182
|
+
"posted",
|
|
1183
|
+
"received"
|
|
1184
|
+
],
|
|
1185
|
+
[
|
|
1186
|
+
"input",
|
|
1187
|
+
"received",
|
|
1188
|
+
"inputReady"
|
|
1189
|
+
],
|
|
1190
|
+
[
|
|
1191
|
+
"prepare",
|
|
1192
|
+
"inputReady",
|
|
1193
|
+
"executeStarted"
|
|
1194
|
+
],
|
|
1195
|
+
[
|
|
1196
|
+
"execute",
|
|
1197
|
+
"executeStarted",
|
|
1198
|
+
"executeFinished"
|
|
1199
|
+
],
|
|
1200
|
+
[
|
|
1201
|
+
"result",
|
|
1202
|
+
"executeFinished",
|
|
1203
|
+
"replied"
|
|
1204
|
+
],
|
|
1205
|
+
[
|
|
1206
|
+
"reply",
|
|
1207
|
+
"replied",
|
|
1208
|
+
"replyReceived"
|
|
1209
|
+
]
|
|
1210
|
+
]) {
|
|
1211
|
+
const started = timing[start];
|
|
1212
|
+
const finished = timing[end];
|
|
1213
|
+
if (started === void 0 || finished === void 0) continue;
|
|
1214
|
+
store.histogram("openclaw_worker_request_phase_seconds", "Elapsed time within an observed SQLite worker request phase, including transport.", {
|
|
1215
|
+
...labels,
|
|
1216
|
+
phase
|
|
1217
|
+
}, seconds(finished - started));
|
|
1218
|
+
}
|
|
545
1219
|
};
|
|
546
|
-
store.histogram("openclaw_worker_queue_wait_seconds", "Elapsed time from worker request enqueue to dispatch.", labels, seconds(event.queueWaitMs));
|
|
547
|
-
store.histogram("openclaw_worker_request_seconds", "Elapsed time from worker dispatch to reply or failure, including preparation and transport.", labels, seconds(event.durationMs));
|
|
548
|
-
if (event.phase !== "completed" || !event.timing) return;
|
|
549
|
-
for (const [phase, start, end] of [
|
|
550
|
-
[
|
|
551
|
-
"dispatch",
|
|
552
|
-
"dispatched",
|
|
553
|
-
"posted"
|
|
554
|
-
],
|
|
555
|
-
[
|
|
556
|
-
"delivery",
|
|
557
|
-
"posted",
|
|
558
|
-
"received"
|
|
559
|
-
],
|
|
560
|
-
[
|
|
561
|
-
"input",
|
|
562
|
-
"received",
|
|
563
|
-
"inputReady"
|
|
564
|
-
],
|
|
565
|
-
[
|
|
566
|
-
"prepare",
|
|
567
|
-
"inputReady",
|
|
568
|
-
"executeStarted"
|
|
569
|
-
],
|
|
570
|
-
[
|
|
571
|
-
"execute",
|
|
572
|
-
"executeStarted",
|
|
573
|
-
"executeFinished"
|
|
574
|
-
],
|
|
575
|
-
[
|
|
576
|
-
"result",
|
|
577
|
-
"executeFinished",
|
|
578
|
-
"replied"
|
|
579
|
-
],
|
|
580
|
-
[
|
|
581
|
-
"reply",
|
|
582
|
-
"replied",
|
|
583
|
-
"replyReceived"
|
|
584
|
-
]
|
|
585
|
-
]) {
|
|
586
|
-
const started = event.timing[start];
|
|
587
|
-
const finished = event.timing[end];
|
|
588
|
-
if (started === void 0 || finished === void 0) continue;
|
|
589
|
-
store.histogram("openclaw_worker_request_phase_seconds", "Elapsed time within an observed SQLite worker request phase, including transport.", {
|
|
590
|
-
...labels,
|
|
591
|
-
phase
|
|
592
|
-
}, seconds(finished - started));
|
|
593
|
-
}
|
|
594
1220
|
}
|
|
595
1221
|
//#endregion
|
|
596
1222
|
//#region extensions/diagnostics-prometheus/src/service.ts
|
|
@@ -626,36 +1252,6 @@ function safeErrorMessage(err) {
|
|
|
626
1252
|
const message = err instanceof Error ? err.message ?? err.name : String(err);
|
|
627
1253
|
return truncateUtf16Safe(redactSensitiveText(message).replaceAll("\0", " ").replace(/[\r\n\t\u2028\u2029]/gu, " "), 500);
|
|
628
1254
|
}
|
|
629
|
-
function renderPrometheusMetrics(store) {
|
|
630
|
-
const snapshot = store.snapshot();
|
|
631
|
-
const lines = [];
|
|
632
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
633
|
-
const emitHeader = (name, type, help) => {
|
|
634
|
-
if (emitted.has(name)) return;
|
|
635
|
-
emitted.add(name);
|
|
636
|
-
lines.push(`# HELP ${name} ${escapeHelp(help)}`);
|
|
637
|
-
lines.push(`# TYPE ${name} ${type}`);
|
|
638
|
-
};
|
|
639
|
-
for (const [type, entries] of [["counter", snapshot.counters], ["gauge", snapshot.gauges]]) {
|
|
640
|
-
entries.sort(([left], [right]) => left.localeCompare(right));
|
|
641
|
-
for (const [key, sample] of entries) {
|
|
642
|
-
const name = key.split("|", 1)[0] ?? "";
|
|
643
|
-
emitHeader(name, type, sample.help);
|
|
644
|
-
lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
snapshot.histograms.sort(([left], [right]) => left.localeCompare(right));
|
|
648
|
-
for (const [key, sample] of snapshot.histograms) {
|
|
649
|
-
const name = key.split("|", 1)[0] ?? "";
|
|
650
|
-
emitHeader(name, "histogram", sample.help);
|
|
651
|
-
for (let index = 0; index < sample.buckets.length; index += 1) lines.push(`${sample.bucketPrefixes[index]}${formatPrometheusNumber(sample.counts[index] ?? 0)}`);
|
|
652
|
-
lines.push(`${sample.bucketPrefixes[sample.buckets.length]}${formatPrometheusNumber(sample.count)}`);
|
|
653
|
-
lines.push(`${name}_sum${sample.labels} ${formatPrometheusNumber(sample.sum)}`);
|
|
654
|
-
lines.push(`${name}_count${sample.labels} ${formatPrometheusNumber(sample.count)}`);
|
|
655
|
-
}
|
|
656
|
-
lines.push("");
|
|
657
|
-
return lines.join("\n");
|
|
658
|
-
}
|
|
659
1255
|
function webhookLabels(evt) {
|
|
660
1256
|
return {
|
|
661
1257
|
channel: normalizeDiagnosticValue(evt.channel),
|
|
@@ -665,16 +1261,23 @@ function webhookLabels(evt) {
|
|
|
665
1261
|
function recordDiagnosticEvent(store, evt, metadata) {
|
|
666
1262
|
if (!metadata.trusted && !isInternalDiagnosticEventMetadata(metadata)) return;
|
|
667
1263
|
switch (evt.type) {
|
|
668
|
-
case "
|
|
669
|
-
if (metadata.trusted)
|
|
1264
|
+
case "gateway.ws.connections":
|
|
1265
|
+
if (metadata.trusted) recordGatewayWsConnections(store, evt.operators);
|
|
1266
|
+
return;
|
|
1267
|
+
case "sessions.list.retention":
|
|
1268
|
+
case "sessions.event.retention":
|
|
1269
|
+
case "sessions.event.rows.reset":
|
|
1270
|
+
if (metadata.trusted) recordSessionRetention(store, evt);
|
|
670
1271
|
return;
|
|
671
1272
|
case "diagnostic.phase.completed":
|
|
672
1273
|
case "secret.egress":
|
|
673
1274
|
case "tool.catalog":
|
|
674
1275
|
case "gateway.rpc":
|
|
1276
|
+
case "sessions.list.baseline.retired":
|
|
675
1277
|
case "gateway.http.cancelled":
|
|
676
1278
|
case "diagnostic.gc":
|
|
677
1279
|
case "gateway.event_loop.sample":
|
|
1280
|
+
case "gateway.main_thread.stall":
|
|
678
1281
|
recordOperationTimingEvent(store, evt, metadata);
|
|
679
1282
|
return;
|
|
680
1283
|
case "model.usage":
|
|
@@ -850,17 +1453,22 @@ function recordDiagnosticEvent(store, evt, metadata) {
|
|
|
850
1453
|
recordSessionDiagnosticEvent(store, evt);
|
|
851
1454
|
return;
|
|
852
1455
|
case "queue.lane.enqueue":
|
|
853
|
-
case "queue.lane.dequeue":
|
|
854
|
-
|
|
855
|
-
|
|
1456
|
+
case "queue.lane.dequeue": {
|
|
1457
|
+
const labels = { lane: normalizeDiagnosticLane(evt.lane) };
|
|
1458
|
+
store.gauge("openclaw_queue_lane_size", "Current diagnostic queue lane size.", labels, asNonNegativeFiniteNumber(evt.queueSize));
|
|
1459
|
+
if (evt.type === "queue.lane.dequeue") store.histogram("openclaw_queue_lane_wait_seconds", "Queue lane wait time in seconds.", labels, seconds(evt.waitMs));
|
|
856
1460
|
return;
|
|
1461
|
+
}
|
|
857
1462
|
case "session.state":
|
|
858
1463
|
case "session.stuck":
|
|
859
1464
|
case "session.turn.created":
|
|
860
1465
|
recordSessionDiagnosticEvent(store, evt);
|
|
861
1466
|
return;
|
|
862
1467
|
case "diagnostic.child_process.spawn":
|
|
863
|
-
|
|
1468
|
+
store.counter("openclaw_child_process_spawn_total", "Successful child launches through the shared spawn and exec owners.", {
|
|
1469
|
+
family: normalizeDiagnosticValue(evt.family),
|
|
1470
|
+
operation: normalizeDiagnosticValue(evt.operation ?? (evt.family === "git" ? "unknown" : "none"))
|
|
1471
|
+
}, asNonNegativeFiniteNumber(evt.count) ?? 0);
|
|
864
1472
|
return;
|
|
865
1473
|
case "diagnostic.memory.sample":
|
|
866
1474
|
recordMemorySample(store, evt.memory, BYTE_BUCKETS);
|
|
@@ -945,23 +1553,24 @@ function createMetricsHandler(store) {
|
|
|
945
1553
|
res.end("Method Not Allowed");
|
|
946
1554
|
return true;
|
|
947
1555
|
}
|
|
948
|
-
const body =
|
|
1556
|
+
const body = store.render();
|
|
949
1557
|
res.statusCode = 200;
|
|
950
1558
|
res.setHeader("Cache-Control", "no-store");
|
|
951
1559
|
res.setHeader("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
|
|
952
|
-
res.setHeader("Content-Length", String(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
res.end(body);
|
|
1560
|
+
res.setHeader("Content-Length", String(body.length));
|
|
1561
|
+
res.flushHeaders();
|
|
1562
|
+
res.end(req.method === "HEAD" ? void 0 : body, (error) => {
|
|
1563
|
+
if (!error) body.buffer.transfer(0);
|
|
1564
|
+
});
|
|
958
1565
|
return true;
|
|
959
1566
|
};
|
|
960
1567
|
}
|
|
961
1568
|
function createDiagnosticsPrometheusExporter() {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1569
|
+
let logger;
|
|
1570
|
+
const store = createPrometheusMetricStore((family) => {
|
|
1571
|
+
logger?.warn(`diagnostics-prometheus: series budget reached for ${family}; new label sets are being dropped. See openclaw_prometheus_family_series_dropped_total.`);
|
|
1572
|
+
});
|
|
1573
|
+
let subscriptions = [];
|
|
965
1574
|
let internalDiagnostics;
|
|
966
1575
|
const reportExporterStatus = (update) => {
|
|
967
1576
|
try {
|
|
@@ -976,7 +1585,6 @@ function createDiagnosticsPrometheusExporter() {
|
|
|
976
1585
|
};
|
|
977
1586
|
return {
|
|
978
1587
|
handler: createMetricsHandler(store),
|
|
979
|
-
render: () => renderPrometheusMetrics(store),
|
|
980
1588
|
service: {
|
|
981
1589
|
id: "diagnostics-prometheus",
|
|
982
1590
|
start(ctx) {
|
|
@@ -985,19 +1593,44 @@ function createDiagnosticsPrometheusExporter() {
|
|
|
985
1593
|
ctx.logger.error("diagnostics-prometheus: internal diagnostics capability unavailable");
|
|
986
1594
|
return;
|
|
987
1595
|
}
|
|
1596
|
+
logger = ctx.logger;
|
|
1597
|
+
reserveMainGcHistograms(store);
|
|
988
1598
|
const identity = isDiagnosticsEnabled(ctx.config) ? ctx.internalDiagnostics?.getRuntimeIdentity?.() : void 0;
|
|
989
1599
|
if (identity) store.gauge("openclaw_gateway_build_info", "Identity of the hosting process and its loaded build; not a health or exporter epoch.", {
|
|
990
1600
|
process_instance_id: identity.processInstanceId,
|
|
991
1601
|
...identity.buildId ? { build_id: identity.buildId } : {}
|
|
992
1602
|
}, 1);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1603
|
+
subscriptions = [
|
|
1604
|
+
ctx.internalDiagnostics?.onGatewayWorkMetrics?.((snapshot) => {
|
|
1605
|
+
if (!snapshot) {
|
|
1606
|
+
store.clearGauges("openclaw_sessions_active");
|
|
1607
|
+
store.clearGauges("openclaw_gateway_active_work");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
for (const state of ["running", "queued"]) store.gauge("openclaw_sessions_active", "Current active sessions in the Gateway session roster by run state.", { state }, snapshot.sessions[state]);
|
|
1611
|
+
for (const kind of [
|
|
1612
|
+
"agentRuns",
|
|
1613
|
+
"chatRuns",
|
|
1614
|
+
"queuedTurns"
|
|
1615
|
+
]) store.gauge("openclaw_gateway_active_work", "Current Gateway work by owner; categories may overlap.", { kind }, snapshot.work[kind]);
|
|
1616
|
+
}),
|
|
1617
|
+
ctx.internalDiagnostics?.onGatewayWsMetrics?.(createGatewayWsMetricsRecorder(store)),
|
|
1618
|
+
ctx.internalDiagnostics?.onWorkerRequestMetrics?.(createWorkerRequestMetricsRecorder(store)),
|
|
1619
|
+
subscribe((event, metadata) => {
|
|
1620
|
+
try {
|
|
1621
|
+
recordDiagnosticEvent(store, event, metadata);
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
ctx.logger.error(`diagnostics-prometheus: event handler failed (${event.type}): ${safeErrorMessage(err)}`);
|
|
1624
|
+
}
|
|
1625
|
+
}, {
|
|
1626
|
+
exclude: [
|
|
1627
|
+
"log.record",
|
|
1628
|
+
"worker.request",
|
|
1629
|
+
"gateway.ws.event"
|
|
1630
|
+
],
|
|
1631
|
+
includePromptStats: false
|
|
1632
|
+
}, { includePrivateData: false })
|
|
1633
|
+
];
|
|
1001
1634
|
internalDiagnostics = ctx.internalDiagnostics;
|
|
1002
1635
|
reportExporterStatus({
|
|
1003
1636
|
signal: "metrics",
|
|
@@ -1007,10 +1640,8 @@ function createDiagnosticsPrometheusExporter() {
|
|
|
1007
1640
|
});
|
|
1008
1641
|
},
|
|
1009
1642
|
stop() {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
unsubscribe?.();
|
|
1013
|
-
unsubscribe = void 0;
|
|
1643
|
+
for (const unsubscribe of subscriptions) unsubscribe?.();
|
|
1644
|
+
subscriptions = [];
|
|
1014
1645
|
reportExporterStatus({
|
|
1015
1646
|
signal: "metrics",
|
|
1016
1647
|
transport: "prometheus-scrape",
|
|
@@ -1018,6 +1649,7 @@ function createDiagnosticsPrometheusExporter() {
|
|
|
1018
1649
|
});
|
|
1019
1650
|
internalDiagnostics = void 0;
|
|
1020
1651
|
store.reset();
|
|
1652
|
+
logger = void 0;
|
|
1021
1653
|
}
|
|
1022
1654
|
}
|
|
1023
1655
|
};
|