@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 +4 -4
- package/src/service.test.ts +103 -32
- package/src/service.ts +37 -25
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/diagnostics-otel",
|
|
3
|
-
"version": "2026.2.
|
|
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-
|
|
10
|
-
"@opentelemetry/exporter-metrics-otlp-
|
|
11
|
-
"@opentelemetry/exporter-trace-otlp-
|
|
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",
|
package/src/service.test.ts
CHANGED
|
@@ -51,11 +51,11 @@ vi.mock("@opentelemetry/sdk-node", () => ({
|
|
|
51
51
|
},
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
-
vi.mock("@opentelemetry/exporter-metrics-otlp-
|
|
54
|
+
vi.mock("@opentelemetry/exporter-metrics-otlp-proto", () => ({
|
|
55
55
|
OTLPMetricExporter: class {},
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
|
-
vi.mock("@opentelemetry/exporter-trace-otlp-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
131
|
-
traces
|
|
132
|
-
metrics
|
|
133
|
-
logs
|
|
142
|
+
protocol: OTEL_TEST_PROTOCOL,
|
|
143
|
+
traces,
|
|
144
|
+
metrics,
|
|
145
|
+
logs,
|
|
134
146
|
},
|
|
135
147
|
},
|
|
136
148
|
},
|
|
137
149
|
logger: createLogger(),
|
|
138
|
-
stateDir:
|
|
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
|
|
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:
|
|
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-
|
|
4
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-
|
|
5
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-
|
|
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":
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
660
|
+
ctx.logger.info("diagnostics-otel: logs exporter enabled (OTLP/Protobuf)");
|
|
649
661
|
}
|
|
650
662
|
},
|
|
651
663
|
async stop() {
|