@openclaw/diagnostics-otel 2026.1.29 → 2026.2.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/index.ts +0 -1
- package/package.json +10 -7
- package/src/service.test.ts +11 -5
- package/src/service.ts +114 -45
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/diagnostics-otel",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.2",
|
|
5
4
|
"description": "OpenClaw diagnostics OpenTelemetry exporter",
|
|
6
|
-
"
|
|
7
|
-
"extensions": [
|
|
8
|
-
"./index.ts"
|
|
9
|
-
]
|
|
10
|
-
},
|
|
5
|
+
"type": "module",
|
|
11
6
|
"dependencies": {
|
|
12
7
|
"@opentelemetry/api": "^1.9.0",
|
|
13
8
|
"@opentelemetry/api-logs": "^0.211.0",
|
|
@@ -20,5 +15,13 @@
|
|
|
20
15
|
"@opentelemetry/sdk-node": "^0.211.0",
|
|
21
16
|
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
|
22
17
|
"@opentelemetry/semantic-conventions": "^1.39.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"openclaw": "workspace:*"
|
|
21
|
+
},
|
|
22
|
+
"openclaw": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
]
|
|
23
26
|
}
|
|
24
27
|
}
|
package/src/service.test.ts
CHANGED
|
@@ -103,8 +103,8 @@ vi.mock("openclaw/plugin-sdk", async () => {
|
|
|
103
103
|
};
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
import { createDiagnosticsOtelService } from "./service.js";
|
|
107
106
|
import { emitDiagnosticEvent } from "openclaw/plugin-sdk";
|
|
107
|
+
import { createDiagnosticsOtelService } from "./service.js";
|
|
108
108
|
|
|
109
109
|
describe("diagnostics-otel service", () => {
|
|
110
110
|
beforeEach(() => {
|
|
@@ -192,13 +192,19 @@ describe("diagnostics-otel service", () => {
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
expect(telemetryState.counters.get("openclaw.webhook.received")?.add).toHaveBeenCalled();
|
|
195
|
-
expect(
|
|
195
|
+
expect(
|
|
196
|
+
telemetryState.histograms.get("openclaw.webhook.duration_ms")?.record,
|
|
197
|
+
).toHaveBeenCalled();
|
|
196
198
|
expect(telemetryState.counters.get("openclaw.message.queued")?.add).toHaveBeenCalled();
|
|
197
199
|
expect(telemetryState.counters.get("openclaw.message.processed")?.add).toHaveBeenCalled();
|
|
198
|
-
expect(
|
|
200
|
+
expect(
|
|
201
|
+
telemetryState.histograms.get("openclaw.message.duration_ms")?.record,
|
|
202
|
+
).toHaveBeenCalled();
|
|
199
203
|
expect(telemetryState.histograms.get("openclaw.queue.wait_ms")?.record).toHaveBeenCalled();
|
|
200
204
|
expect(telemetryState.counters.get("openclaw.session.stuck")?.add).toHaveBeenCalled();
|
|
201
|
-
expect(
|
|
205
|
+
expect(
|
|
206
|
+
telemetryState.histograms.get("openclaw.session.stuck_age_ms")?.record,
|
|
207
|
+
).toHaveBeenCalled();
|
|
202
208
|
expect(telemetryState.counters.get("openclaw.run.attempt")?.add).toHaveBeenCalled();
|
|
203
209
|
|
|
204
210
|
const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0]);
|
|
@@ -209,7 +215,7 @@ describe("diagnostics-otel service", () => {
|
|
|
209
215
|
expect(registerLogTransportMock).toHaveBeenCalledTimes(1);
|
|
210
216
|
expect(registeredTransports).toHaveLength(1);
|
|
211
217
|
registeredTransports[0]?.({
|
|
212
|
-
0:
|
|
218
|
+
0: '{"subsystem":"diagnostic"}',
|
|
213
219
|
1: "hello",
|
|
214
220
|
_meta: { logLevelName: "INFO", date: new Date() },
|
|
215
221
|
});
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { metrics, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
2
1
|
import type { SeverityNumber } from "@opentelemetry/api-logs";
|
|
2
|
+
import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk";
|
|
3
|
+
import { metrics, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
3
4
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
4
5
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
5
6
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -9,8 +10,6 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
|
9
10
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
10
11
|
import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
|
|
11
12
|
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
|
|
12
|
-
|
|
13
|
-
import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk";
|
|
14
13
|
import { onDiagnosticEvent, registerLogTransport } from "openclaw/plugin-sdk";
|
|
15
14
|
|
|
16
15
|
const DEFAULT_SERVICE_NAME = "openclaw";
|
|
@@ -21,14 +20,22 @@ function normalizeEndpoint(endpoint?: string): string | undefined {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
function resolveOtelUrl(endpoint: string | undefined, path: string): string | undefined {
|
|
24
|
-
if (!endpoint)
|
|
25
|
-
|
|
23
|
+
if (!endpoint) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (endpoint.includes("/v1/")) {
|
|
27
|
+
return endpoint;
|
|
28
|
+
}
|
|
26
29
|
return `${endpoint}/${path}`;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
function resolveSampleRate(value: number | undefined): number | undefined {
|
|
30
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
31
|
-
|
|
33
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (value < 0 || value > 1) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
32
39
|
return value;
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -43,7 +50,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
43
50
|
async start(ctx) {
|
|
44
51
|
const cfg = ctx.config.diagnostics;
|
|
45
52
|
const otel = cfg?.otel;
|
|
46
|
-
if (!cfg?.enabled || !otel?.enabled)
|
|
53
|
+
if (!cfg?.enabled || !otel?.enabled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
47
56
|
|
|
48
57
|
const protocol = otel.protocol ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL ?? "http/protobuf";
|
|
49
58
|
if (protocol !== "http/protobuf") {
|
|
@@ -60,7 +69,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
60
69
|
const tracesEnabled = otel.traces !== false;
|
|
61
70
|
const metricsEnabled = otel.metrics !== false;
|
|
62
71
|
const logsEnabled = otel.logs === true;
|
|
63
|
-
if (!tracesEnabled && !metricsEnabled && !logsEnabled)
|
|
72
|
+
if (!tracesEnabled && !metricsEnabled && !logsEnabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
64
75
|
|
|
65
76
|
const resource = new Resource({
|
|
66
77
|
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
@@ -106,7 +117,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
106
117
|
: {}),
|
|
107
118
|
});
|
|
108
119
|
|
|
109
|
-
|
|
120
|
+
sdk.start();
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
const logSeverityMap: Record<string, SeverityNumber> = {
|
|
@@ -201,11 +212,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
201
212
|
});
|
|
202
213
|
logProvider = new LoggerProvider({ resource });
|
|
203
214
|
logProvider.addLogRecordProcessor(
|
|
204
|
-
new BatchLogRecordProcessor(
|
|
205
|
-
|
|
215
|
+
new BatchLogRecordProcessor(
|
|
216
|
+
logExporter,
|
|
217
|
+
typeof otel.flushIntervalMs === "number"
|
|
206
218
|
? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) }
|
|
207
|
-
: {}
|
|
208
|
-
|
|
219
|
+
: {},
|
|
220
|
+
),
|
|
209
221
|
);
|
|
210
222
|
const otelLogger = logProvider.getLogger("openclaw");
|
|
211
223
|
|
|
@@ -237,7 +249,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
237
249
|
|
|
238
250
|
const numericArgs = Object.entries(logObj)
|
|
239
251
|
.filter(([key]) => /^\d+$/.test(key))
|
|
240
|
-
.
|
|
252
|
+
.toSorted((a, b) => Number(a[0]) - Number(b[0]))
|
|
241
253
|
.map(([, value]) => value);
|
|
242
254
|
|
|
243
255
|
let bindings: Record<string, unknown> | undefined;
|
|
@@ -267,13 +279,19 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
267
279
|
const attributes: Record<string, string | number | boolean> = {
|
|
268
280
|
"openclaw.log.level": logLevelName,
|
|
269
281
|
};
|
|
270
|
-
if (meta?.name)
|
|
282
|
+
if (meta?.name) {
|
|
283
|
+
attributes["openclaw.logger"] = meta.name;
|
|
284
|
+
}
|
|
271
285
|
if (meta?.parentNames?.length) {
|
|
272
286
|
attributes["openclaw.logger.parents"] = meta.parentNames.join(".");
|
|
273
287
|
}
|
|
274
288
|
if (bindings) {
|
|
275
289
|
for (const [key, value] of Object.entries(bindings)) {
|
|
276
|
-
if (
|
|
290
|
+
if (
|
|
291
|
+
typeof value === "string" ||
|
|
292
|
+
typeof value === "number" ||
|
|
293
|
+
typeof value === "boolean"
|
|
294
|
+
) {
|
|
277
295
|
attributes[`openclaw.${key}`] = value;
|
|
278
296
|
} else if (value != null) {
|
|
279
297
|
attributes[`openclaw.${key}`] = safeStringify(value);
|
|
@@ -283,9 +301,15 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
283
301
|
if (numericArgs.length > 0) {
|
|
284
302
|
attributes["openclaw.log.args"] = safeStringify(numericArgs);
|
|
285
303
|
}
|
|
286
|
-
if (meta?.path?.filePath)
|
|
287
|
-
|
|
288
|
-
|
|
304
|
+
if (meta?.path?.filePath) {
|
|
305
|
+
attributes["code.filepath"] = meta.path.filePath;
|
|
306
|
+
}
|
|
307
|
+
if (meta?.path?.fileLine) {
|
|
308
|
+
attributes["code.lineno"] = Number(meta.path.fileLine);
|
|
309
|
+
}
|
|
310
|
+
if (meta?.path?.method) {
|
|
311
|
+
attributes["code.function"] = meta.path.method;
|
|
312
|
+
}
|
|
289
313
|
if (meta?.path?.filePathWithLine) {
|
|
290
314
|
attributes["openclaw.code.location"] = meta.path.filePathWithLine;
|
|
291
315
|
}
|
|
@@ -322,30 +346,47 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
322
346
|
};
|
|
323
347
|
|
|
324
348
|
const usage = evt.usage;
|
|
325
|
-
if (usage.input)
|
|
326
|
-
|
|
327
|
-
|
|
349
|
+
if (usage.input) {
|
|
350
|
+
tokensCounter.add(usage.input, { ...attrs, "openclaw.token": "input" });
|
|
351
|
+
}
|
|
352
|
+
if (usage.output) {
|
|
353
|
+
tokensCounter.add(usage.output, { ...attrs, "openclaw.token": "output" });
|
|
354
|
+
}
|
|
355
|
+
if (usage.cacheRead) {
|
|
328
356
|
tokensCounter.add(usage.cacheRead, { ...attrs, "openclaw.token": "cache_read" });
|
|
329
|
-
|
|
357
|
+
}
|
|
358
|
+
if (usage.cacheWrite) {
|
|
330
359
|
tokensCounter.add(usage.cacheWrite, { ...attrs, "openclaw.token": "cache_write" });
|
|
331
|
-
|
|
360
|
+
}
|
|
361
|
+
if (usage.promptTokens) {
|
|
332
362
|
tokensCounter.add(usage.promptTokens, { ...attrs, "openclaw.token": "prompt" });
|
|
333
|
-
|
|
363
|
+
}
|
|
364
|
+
if (usage.total) {
|
|
365
|
+
tokensCounter.add(usage.total, { ...attrs, "openclaw.token": "total" });
|
|
366
|
+
}
|
|
334
367
|
|
|
335
|
-
if (evt.costUsd)
|
|
336
|
-
|
|
337
|
-
|
|
368
|
+
if (evt.costUsd) {
|
|
369
|
+
costCounter.add(evt.costUsd, attrs);
|
|
370
|
+
}
|
|
371
|
+
if (evt.durationMs) {
|
|
372
|
+
durationHistogram.record(evt.durationMs, attrs);
|
|
373
|
+
}
|
|
374
|
+
if (evt.context?.limit) {
|
|
338
375
|
contextHistogram.record(evt.context.limit, {
|
|
339
376
|
...attrs,
|
|
340
377
|
"openclaw.context": "limit",
|
|
341
378
|
});
|
|
342
|
-
|
|
379
|
+
}
|
|
380
|
+
if (evt.context?.used) {
|
|
343
381
|
contextHistogram.record(evt.context.used, {
|
|
344
382
|
...attrs,
|
|
345
383
|
"openclaw.context": "used",
|
|
346
384
|
});
|
|
385
|
+
}
|
|
347
386
|
|
|
348
|
-
if (!tracesEnabled)
|
|
387
|
+
if (!tracesEnabled) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
349
390
|
const spanAttrs: Record<string, string | number> = {
|
|
350
391
|
...attrs,
|
|
351
392
|
"openclaw.sessionKey": evt.sessionKey ?? "",
|
|
@@ -381,9 +422,13 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
381
422
|
if (typeof evt.durationMs === "number") {
|
|
382
423
|
webhookDurationHistogram.record(evt.durationMs, attrs);
|
|
383
424
|
}
|
|
384
|
-
if (!tracesEnabled)
|
|
425
|
+
if (!tracesEnabled) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
385
428
|
const spanAttrs: Record<string, string | number> = { ...attrs };
|
|
386
|
-
if (evt.chatId !== undefined)
|
|
429
|
+
if (evt.chatId !== undefined) {
|
|
430
|
+
spanAttrs["openclaw.chatId"] = String(evt.chatId);
|
|
431
|
+
}
|
|
387
432
|
const span = spanWithDuration("openclaw.webhook.processed", spanAttrs, evt.durationMs);
|
|
388
433
|
span.end();
|
|
389
434
|
};
|
|
@@ -396,12 +441,16 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
396
441
|
"openclaw.webhook": evt.updateType ?? "unknown",
|
|
397
442
|
};
|
|
398
443
|
webhookErrorCounter.add(1, attrs);
|
|
399
|
-
if (!tracesEnabled)
|
|
444
|
+
if (!tracesEnabled) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
400
447
|
const spanAttrs: Record<string, string | number> = {
|
|
401
448
|
...attrs,
|
|
402
449
|
"openclaw.error": evt.error,
|
|
403
450
|
};
|
|
404
|
-
if (evt.chatId !== undefined)
|
|
451
|
+
if (evt.chatId !== undefined) {
|
|
452
|
+
spanAttrs["openclaw.chatId"] = String(evt.chatId);
|
|
453
|
+
}
|
|
405
454
|
const span = tracer.startSpan("openclaw.webhook.error", {
|
|
406
455
|
attributes: spanAttrs,
|
|
407
456
|
});
|
|
@@ -433,13 +482,25 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
433
482
|
if (typeof evt.durationMs === "number") {
|
|
434
483
|
messageDurationHistogram.record(evt.durationMs, attrs);
|
|
435
484
|
}
|
|
436
|
-
if (!tracesEnabled)
|
|
485
|
+
if (!tracesEnabled) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
437
488
|
const spanAttrs: Record<string, string | number> = { ...attrs };
|
|
438
|
-
if (evt.sessionKey)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (evt.
|
|
442
|
-
|
|
489
|
+
if (evt.sessionKey) {
|
|
490
|
+
spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
|
|
491
|
+
}
|
|
492
|
+
if (evt.sessionId) {
|
|
493
|
+
spanAttrs["openclaw.sessionId"] = evt.sessionId;
|
|
494
|
+
}
|
|
495
|
+
if (evt.chatId !== undefined) {
|
|
496
|
+
spanAttrs["openclaw.chatId"] = String(evt.chatId);
|
|
497
|
+
}
|
|
498
|
+
if (evt.messageId !== undefined) {
|
|
499
|
+
spanAttrs["openclaw.messageId"] = String(evt.messageId);
|
|
500
|
+
}
|
|
501
|
+
if (evt.reason) {
|
|
502
|
+
spanAttrs["openclaw.reason"] = evt.reason;
|
|
503
|
+
}
|
|
443
504
|
const span = spanWithDuration("openclaw.message.processed", spanAttrs, evt.durationMs);
|
|
444
505
|
if (evt.outcome === "error") {
|
|
445
506
|
span.setStatus({ code: SpanStatusCode.ERROR, message: evt.error });
|
|
@@ -470,7 +531,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
470
531
|
evt: Extract<DiagnosticEventPayload, { type: "session.state" }>,
|
|
471
532
|
) => {
|
|
472
533
|
const attrs: Record<string, string> = { "openclaw.state": evt.state };
|
|
473
|
-
if (evt.reason)
|
|
534
|
+
if (evt.reason) {
|
|
535
|
+
attrs["openclaw.reason"] = evt.reason;
|
|
536
|
+
}
|
|
474
537
|
sessionStateCounter.add(1, attrs);
|
|
475
538
|
};
|
|
476
539
|
|
|
@@ -482,10 +545,16 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
|
|
482
545
|
if (typeof evt.ageMs === "number") {
|
|
483
546
|
sessionStuckAgeHistogram.record(evt.ageMs, attrs);
|
|
484
547
|
}
|
|
485
|
-
if (!tracesEnabled)
|
|
548
|
+
if (!tracesEnabled) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
486
551
|
const spanAttrs: Record<string, string | number> = { ...attrs };
|
|
487
|
-
if (evt.sessionKey)
|
|
488
|
-
|
|
552
|
+
if (evt.sessionKey) {
|
|
553
|
+
spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
|
|
554
|
+
}
|
|
555
|
+
if (evt.sessionId) {
|
|
556
|
+
spanAttrs["openclaw.sessionId"] = evt.sessionId;
|
|
557
|
+
}
|
|
489
558
|
spanAttrs["openclaw.queueDepth"] = evt.queueDepth ?? 0;
|
|
490
559
|
spanAttrs["openclaw.ageMs"] = evt.ageMs;
|
|
491
560
|
const span = tracer.startSpan("openclaw.session.stuck", { attributes: spanAttrs });
|