@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 CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
-
4
3
  import { createDiagnosticsOtelService } from "./src/service.js";
5
4
 
6
5
  const plugin = {
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "@openclaw/diagnostics-otel",
3
- "version": "2026.1.29",
4
- "type": "module",
3
+ "version": "2026.2.2",
5
4
  "description": "OpenClaw diagnostics OpenTelemetry exporter",
6
- "openclaw": {
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
  }
@@ -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(telemetryState.histograms.get("openclaw.webhook.duration_ms")?.record).toHaveBeenCalled();
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(telemetryState.histograms.get("openclaw.message.duration_ms")?.record).toHaveBeenCalled();
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(telemetryState.histograms.get("openclaw.session.stuck_age_ms")?.record).toHaveBeenCalled();
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: "{\"subsystem\":\"diagnostic\"}",
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) return undefined;
25
- if (endpoint.includes("/v1/")) return endpoint;
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)) return undefined;
31
- if (value < 0 || value > 1) return undefined;
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) return;
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) return;
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
- await sdk.start();
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(logExporter, {
205
- ...(typeof otel.flushIntervalMs === "number"
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
- .sort((a, b) => Number(a[0]) - Number(b[0]))
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) attributes["openclaw.logger"] = 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 (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
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) attributes["code.filepath"] = meta.path.filePath;
287
- if (meta?.path?.fileLine) attributes["code.lineno"] = Number(meta.path.fileLine);
288
- if (meta?.path?.method) attributes["code.function"] = meta.path.method;
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) tokensCounter.add(usage.input, { ...attrs, "openclaw.token": "input" });
326
- if (usage.output) tokensCounter.add(usage.output, { ...attrs, "openclaw.token": "output" });
327
- if (usage.cacheRead)
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
- if (usage.cacheWrite)
357
+ }
358
+ if (usage.cacheWrite) {
330
359
  tokensCounter.add(usage.cacheWrite, { ...attrs, "openclaw.token": "cache_write" });
331
- if (usage.promptTokens)
360
+ }
361
+ if (usage.promptTokens) {
332
362
  tokensCounter.add(usage.promptTokens, { ...attrs, "openclaw.token": "prompt" });
333
- if (usage.total) tokensCounter.add(usage.total, { ...attrs, "openclaw.token": "total" });
363
+ }
364
+ if (usage.total) {
365
+ tokensCounter.add(usage.total, { ...attrs, "openclaw.token": "total" });
366
+ }
334
367
 
335
- if (evt.costUsd) costCounter.add(evt.costUsd, attrs);
336
- if (evt.durationMs) durationHistogram.record(evt.durationMs, attrs);
337
- if (evt.context?.limit)
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
- if (evt.context?.used)
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) return;
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) return;
425
+ if (!tracesEnabled) {
426
+ return;
427
+ }
385
428
  const spanAttrs: Record<string, string | number> = { ...attrs };
386
- if (evt.chatId !== undefined) spanAttrs["openclaw.chatId"] = String(evt.chatId);
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) return;
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) spanAttrs["openclaw.chatId"] = String(evt.chatId);
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) return;
485
+ if (!tracesEnabled) {
486
+ return;
487
+ }
437
488
  const spanAttrs: Record<string, string | number> = { ...attrs };
438
- if (evt.sessionKey) spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
439
- if (evt.sessionId) spanAttrs["openclaw.sessionId"] = evt.sessionId;
440
- if (evt.chatId !== undefined) spanAttrs["openclaw.chatId"] = String(evt.chatId);
441
- if (evt.messageId !== undefined) spanAttrs["openclaw.messageId"] = String(evt.messageId);
442
- if (evt.reason) spanAttrs["openclaw.reason"] = evt.reason;
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) attrs["openclaw.reason"] = 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) return;
548
+ if (!tracesEnabled) {
549
+ return;
550
+ }
486
551
  const spanAttrs: Record<string, string | number> = { ...attrs };
487
- if (evt.sessionKey) spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
488
- if (evt.sessionId) spanAttrs["openclaw.sessionId"] = evt.sessionId;
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 });