@openclaw/diagnostics-otel 2026.2.22 → 2026.2.23

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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@openclaw/diagnostics-otel",
3
- "version": "2026.2.22",
3
+ "version": "2026.2.23",
4
4
  "description": "OpenClaw diagnostics OpenTelemetry exporter",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@opentelemetry/api": "^1.9.0",
8
8
  "@opentelemetry/api-logs": "^0.212.0",
9
- "@opentelemetry/exporter-logs-otlp-http": "^0.212.0",
10
- "@opentelemetry/exporter-metrics-otlp-http": "^0.212.0",
11
- "@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
9
+ "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0",
10
+ "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0",
11
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0",
12
12
  "@opentelemetry/resources": "^2.5.1",
13
13
  "@opentelemetry/sdk-logs": "^0.212.0",
14
14
  "@opentelemetry/sdk-metrics": "^2.5.1",
@@ -51,11 +51,11 @@ vi.mock("@opentelemetry/sdk-node", () => ({
51
51
  },
52
52
  }));
53
53
 
54
- vi.mock("@opentelemetry/exporter-metrics-otlp-http", () => ({
54
+ vi.mock("@opentelemetry/exporter-metrics-otlp-proto", () => ({
55
55
  OTLPMetricExporter: class {},
56
56
  }));
57
57
 
58
- vi.mock("@opentelemetry/exporter-trace-otlp-http", () => ({
58
+ vi.mock("@opentelemetry/exporter-trace-otlp-proto", () => ({
59
59
  OTLPTraceExporter: class {
60
60
  constructor(options?: unknown) {
61
61
  traceExporterCtor(options);
@@ -63,7 +63,7 @@ vi.mock("@opentelemetry/exporter-trace-otlp-http", () => ({
63
63
  },
64
64
  }));
65
65
 
66
- vi.mock("@opentelemetry/exporter-logs-otlp-http", () => ({
66
+ vi.mock("@opentelemetry/exporter-logs-otlp-proto", () => ({
67
67
  OTLPLogExporter: class {},
68
68
  }));
69
69
 
@@ -110,6 +110,10 @@ import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk";
110
110
  import { emitDiagnosticEvent } from "openclaw/plugin-sdk";
111
111
  import { createDiagnosticsOtelService } from "./service.js";
112
112
 
113
+ const OTEL_TEST_STATE_DIR = "/tmp/openclaw-diagnostics-otel-test";
114
+ const OTEL_TEST_ENDPOINT = "http://otel-collector:4318";
115
+ const OTEL_TEST_PROTOCOL = "http/protobuf";
116
+
113
117
  function createLogger() {
114
118
  return {
115
119
  info: vi.fn(),
@@ -119,7 +123,15 @@ function createLogger() {
119
123
  };
120
124
  }
121
125
 
122
- function createTraceOnlyContext(endpoint: string): OpenClawPluginServiceContext {
126
+ type OtelContextFlags = {
127
+ traces?: boolean;
128
+ metrics?: boolean;
129
+ logs?: boolean;
130
+ };
131
+ function createOtelContext(
132
+ endpoint: string,
133
+ { traces = false, metrics = false, logs = false }: OtelContextFlags = {},
134
+ ): OpenClawPluginServiceContext {
123
135
  return {
124
136
  config: {
125
137
  diagnostics: {
@@ -127,17 +139,46 @@ function createTraceOnlyContext(endpoint: string): OpenClawPluginServiceContext
127
139
  otel: {
128
140
  enabled: true,
129
141
  endpoint,
130
- protocol: "http/protobuf",
131
- traces: true,
132
- metrics: false,
133
- logs: false,
142
+ protocol: OTEL_TEST_PROTOCOL,
143
+ traces,
144
+ metrics,
145
+ logs,
134
146
  },
135
147
  },
136
148
  },
137
149
  logger: createLogger(),
138
- stateDir: "/tmp/openclaw-diagnostics-otel-test",
150
+ stateDir: OTEL_TEST_STATE_DIR,
139
151
  };
140
152
  }
153
+
154
+ function createTraceOnlyContext(endpoint: string): OpenClawPluginServiceContext {
155
+ return createOtelContext(endpoint, { traces: true });
156
+ }
157
+
158
+ type RegisteredLogTransport = (logObj: Record<string, unknown>) => void;
159
+ function setupRegisteredTransports() {
160
+ const registeredTransports: RegisteredLogTransport[] = [];
161
+ const stopTransport = vi.fn();
162
+ registerLogTransportMock.mockImplementation((transport) => {
163
+ registeredTransports.push(transport);
164
+ return stopTransport;
165
+ });
166
+ return { registeredTransports, stopTransport };
167
+ }
168
+
169
+ async function emitAndCaptureLog(logObj: Record<string, unknown>) {
170
+ const { registeredTransports } = setupRegisteredTransports();
171
+ const service = createDiagnosticsOtelService();
172
+ const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
173
+ await service.start(ctx);
174
+ expect(registeredTransports).toHaveLength(1);
175
+ registeredTransports[0]?.(logObj);
176
+ expect(logEmit).toHaveBeenCalled();
177
+ const emitCall = logEmit.mock.calls[0]?.[0];
178
+ await service.stop?.(ctx);
179
+ return emitCall;
180
+ }
181
+
141
182
  describe("diagnostics-otel service", () => {
142
183
  beforeEach(() => {
143
184
  telemetryState.counters.clear();
@@ -154,31 +195,10 @@ describe("diagnostics-otel service", () => {
154
195
  });
155
196
 
156
197
  test("records message-flow metrics and spans", async () => {
157
- const registeredTransports: Array<(logObj: Record<string, unknown>) => void> = [];
158
- const stopTransport = vi.fn();
159
- registerLogTransportMock.mockImplementation((transport) => {
160
- registeredTransports.push(transport);
161
- return stopTransport;
162
- });
198
+ const { registeredTransports } = setupRegisteredTransports();
163
199
 
164
200
  const service = createDiagnosticsOtelService();
165
- const ctx: OpenClawPluginServiceContext = {
166
- config: {
167
- diagnostics: {
168
- enabled: true,
169
- otel: {
170
- enabled: true,
171
- endpoint: "http://otel-collector:4318",
172
- protocol: "http/protobuf",
173
- traces: true,
174
- metrics: true,
175
- logs: true,
176
- },
177
- },
178
- },
179
- logger: createLogger(),
180
- stateDir: "/tmp/openclaw-diagnostics-otel-test",
181
- };
201
+ const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true, logs: true });
182
202
  await service.start(ctx);
183
203
 
184
204
  emitDiagnosticEvent({
@@ -293,4 +313,55 @@ describe("diagnostics-otel service", () => {
293
313
  expect(options?.url).toBe("https://collector.example.com/v1/Traces");
294
314
  await service.stop?.(ctx);
295
315
  });
316
+
317
+ test("redacts sensitive data from log messages before export", async () => {
318
+ const emitCall = await emitAndCaptureLog({
319
+ 0: "Using API key sk-1234567890abcdef1234567890abcdef",
320
+ _meta: { logLevelName: "INFO", date: new Date() },
321
+ });
322
+
323
+ expect(emitCall?.body).not.toContain("sk-1234567890abcdef1234567890abcdef");
324
+ expect(emitCall?.body).toContain("sk-123");
325
+ expect(emitCall?.body).toContain("…");
326
+ });
327
+
328
+ test("redacts sensitive data from log attributes before export", async () => {
329
+ const emitCall = await emitAndCaptureLog({
330
+ 0: '{"token":"ghp_abcdefghijklmnopqrstuvwxyz123456"}',
331
+ 1: "auth configured",
332
+ _meta: { logLevelName: "DEBUG", date: new Date() },
333
+ });
334
+
335
+ const tokenAttr = emitCall?.attributes?.["openclaw.token"];
336
+ expect(tokenAttr).not.toBe("ghp_abcdefghijklmnopqrstuvwxyz123456");
337
+ if (typeof tokenAttr === "string") {
338
+ expect(tokenAttr).toContain("…");
339
+ }
340
+ });
341
+
342
+ test("redacts sensitive reason in session.state metric attributes", async () => {
343
+ const service = createDiagnosticsOtelService();
344
+ const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { metrics: true });
345
+ await service.start(ctx);
346
+
347
+ emitDiagnosticEvent({
348
+ type: "session.state",
349
+ state: "waiting",
350
+ reason: "token=ghp_abcdefghijklmnopqrstuvwxyz123456",
351
+ });
352
+
353
+ const sessionCounter = telemetryState.counters.get("openclaw.session.state");
354
+ expect(sessionCounter?.add).toHaveBeenCalledWith(
355
+ 1,
356
+ expect.objectContaining({
357
+ "openclaw.reason": expect.stringContaining("…"),
358
+ }),
359
+ );
360
+ const attrs = sessionCounter?.add.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
361
+ expect(typeof attrs?.["openclaw.reason"]).toBe("string");
362
+ expect(String(attrs?.["openclaw.reason"])).not.toContain(
363
+ "ghp_abcdefghijklmnopqrstuvwxyz123456",
364
+ );
365
+ await service.stop?.(ctx);
366
+ });
296
367
  });
package/src/service.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { metrics, trace, SpanStatusCode } from "@opentelemetry/api";
2
2
  import type { SeverityNumber } from "@opentelemetry/api-logs";
3
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
4
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
5
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
3
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
4
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
5
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
6
6
  import { resourceFromAttributes } from "@opentelemetry/resources";
7
7
  import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
8
8
  import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
@@ -10,7 +10,7 @@ import { NodeSDK } from "@opentelemetry/sdk-node";
10
10
  import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
11
11
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
12
12
  import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk";
13
- import { onDiagnosticEvent, registerLogTransport } from "openclaw/plugin-sdk";
13
+ import { onDiagnosticEvent, redactSensitiveText, registerLogTransport } from "openclaw/plugin-sdk";
14
14
 
15
15
  const DEFAULT_SERVICE_NAME = "openclaw";
16
16
 
@@ -54,6 +54,14 @@ function formatError(err: unknown): string {
54
54
  }
55
55
  }
56
56
 
57
+ function redactOtelAttributes(attributes: Record<string, string | number | boolean>) {
58
+ const redactedAttributes: Record<string, string | number | boolean> = {};
59
+ for (const [key, value] of Object.entries(attributes)) {
60
+ redactedAttributes[key] = typeof value === "string" ? redactSensitiveText(value) : value;
61
+ }
62
+ return redactedAttributes;
63
+ }
64
+
57
65
  export function createDiagnosticsOtelService(): OpenClawPluginService {
58
66
  let sdk: NodeSDK | null = null;
59
67
  let logProvider: LoggerProvider | null = null;
@@ -336,11 +344,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
336
344
  attributes["openclaw.code.location"] = meta.path.filePathWithLine;
337
345
  }
338
346
 
347
+ // OTLP can leave the host boundary, so redact string fields before export.
339
348
  otelLogger.emit({
340
- body: message,
349
+ body: redactSensitiveText(message),
341
350
  severityText: logLevelName,
342
351
  severityNumber,
343
- attributes,
352
+ attributes: redactOtelAttributes(attributes),
344
353
  timestamp: meta?.date ?? new Date(),
345
354
  });
346
355
  } catch (err) {
@@ -469,9 +478,10 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
469
478
  if (!tracesEnabled) {
470
479
  return;
471
480
  }
481
+ const redactedError = redactSensitiveText(evt.error);
472
482
  const spanAttrs: Record<string, string | number> = {
473
483
  ...attrs,
474
- "openclaw.error": evt.error,
484
+ "openclaw.error": redactedError,
475
485
  };
476
486
  if (evt.chatId !== undefined) {
477
487
  spanAttrs["openclaw.chatId"] = String(evt.chatId);
@@ -479,7 +489,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
479
489
  const span = tracer.startSpan("openclaw.webhook.error", {
480
490
  attributes: spanAttrs,
481
491
  });
482
- span.setStatus({ code: SpanStatusCode.ERROR, message: evt.error });
492
+ span.setStatus({ code: SpanStatusCode.ERROR, message: redactedError });
483
493
  span.end();
484
494
  };
485
495
 
@@ -496,6 +506,18 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
496
506
  }
497
507
  };
498
508
 
509
+ const addSessionIdentityAttrs = (
510
+ spanAttrs: Record<string, string | number>,
511
+ evt: { sessionKey?: string; sessionId?: string },
512
+ ) => {
513
+ if (evt.sessionKey) {
514
+ spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
515
+ }
516
+ if (evt.sessionId) {
517
+ spanAttrs["openclaw.sessionId"] = evt.sessionId;
518
+ }
519
+ };
520
+
499
521
  const recordMessageProcessed = (
500
522
  evt: Extract<DiagnosticEventPayload, { type: "message.processed" }>,
501
523
  ) => {
@@ -511,12 +533,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
511
533
  return;
512
534
  }
513
535
  const spanAttrs: Record<string, string | number> = { ...attrs };
514
- if (evt.sessionKey) {
515
- spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
516
- }
517
- if (evt.sessionId) {
518
- spanAttrs["openclaw.sessionId"] = evt.sessionId;
519
- }
536
+ addSessionIdentityAttrs(spanAttrs, evt);
520
537
  if (evt.chatId !== undefined) {
521
538
  spanAttrs["openclaw.chatId"] = String(evt.chatId);
522
539
  }
@@ -524,11 +541,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
524
541
  spanAttrs["openclaw.messageId"] = String(evt.messageId);
525
542
  }
526
543
  if (evt.reason) {
527
- spanAttrs["openclaw.reason"] = evt.reason;
544
+ spanAttrs["openclaw.reason"] = redactSensitiveText(evt.reason);
528
545
  }
529
546
  const span = spanWithDuration("openclaw.message.processed", spanAttrs, evt.durationMs);
530
- if (evt.outcome === "error") {
531
- span.setStatus({ code: SpanStatusCode.ERROR, message: evt.error });
547
+ if (evt.outcome === "error" && evt.error) {
548
+ span.setStatus({ code: SpanStatusCode.ERROR, message: redactSensitiveText(evt.error) });
532
549
  }
533
550
  span.end();
534
551
  };
@@ -557,7 +574,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
557
574
  ) => {
558
575
  const attrs: Record<string, string> = { "openclaw.state": evt.state };
559
576
  if (evt.reason) {
560
- attrs["openclaw.reason"] = evt.reason;
577
+ attrs["openclaw.reason"] = redactSensitiveText(evt.reason);
561
578
  }
562
579
  sessionStateCounter.add(1, attrs);
563
580
  };
@@ -574,12 +591,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
574
591
  return;
575
592
  }
576
593
  const spanAttrs: Record<string, string | number> = { ...attrs };
577
- if (evt.sessionKey) {
578
- spanAttrs["openclaw.sessionKey"] = evt.sessionKey;
579
- }
580
- if (evt.sessionId) {
581
- spanAttrs["openclaw.sessionId"] = evt.sessionId;
582
- }
594
+ addSessionIdentityAttrs(spanAttrs, evt);
583
595
  spanAttrs["openclaw.queueDepth"] = evt.queueDepth ?? 0;
584
596
  spanAttrs["openclaw.ageMs"] = evt.ageMs;
585
597
  const span = tracer.startSpan("openclaw.session.stuck", { attributes: spanAttrs });
@@ -645,7 +657,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
645
657
  });
646
658
 
647
659
  if (logsEnabled) {
648
- ctx.logger.info("diagnostics-otel: logs exporter enabled (OTLP/HTTP)");
660
+ ctx.logger.info("diagnostics-otel: logs exporter enabled (OTLP/Protobuf)");
649
661
  }
650
662
  },
651
663
  async stop() {