import { afterEach, beforeEach, describe, expect, test, vi } from
"vitest" ;
const telemetryState = vi.hoisted(() => {
const counters =
new Map<string, { add: ReturnType<
typeof vi.fn> }>();
const histograms =
new Map<string, { record: ReturnType<
typeof vi.fn> }>();
const spans: Array<{
name: string;
end: ReturnType<
typeof vi.fn>;
setStatus: ReturnType<
typeof vi.fn>;
}> = [];
const tracer = {
startSpan: vi.fn((name: string, _opts?: unknown, _ctx?: unknown) => {
const span = {
end: vi.fn(),
setStatus: vi.fn(),
};
spans.push({ name, ...span });
return span;
}),
setSpanContext: vi.fn((_ctx: unknown, spanContext: unknown) => ({ spanContext })),
};
const meter = {
createCounter: vi.fn((name: string) => {
const counter = { add: vi.fn() };
counters.set(name, counter);
return counter;
}),
createHistogram: vi.fn((name: string) => {
const histogram = { record: vi.fn() };
histograms.set(name, histogram);
return histogram;
}),
};
return { counters, histograms, spans, tracer, meter };
});
const sdkStart = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const sdkShutdown = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const logEmit = vi.hoisted(() => vi.fn());
const logShutdown = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const traceExporterCtor = vi.hoisted(() => vi.fn());
vi.mock(
"@opentelemetry/api" , () => ({
context: {
active: () => ({}),
},
metrics: {
getMeter: () => telemetryState.meter,
},
trace: {
getTracer: () => telemetryState.tracer,
setSpanContext: telemetryState.tracer.setSpanContext,
},
TraceFlags: {
NONE:
0 ,
SAMPLED:
1 ,
},
SpanStatusCode: {
ERROR:
2 ,
},
}));
vi.mock(
"@opentelemetry/sdk-node" , () => ({
NodeSDK:
class {
start = sdkStart;
shutdown = sdkShutdown;
},
}));
vi.mock(
"@opentelemetry/exporter-metrics-otlp-proto" , () => ({
OTLPMetricExporter:
function OTLPMetricExporter() {},
}));
vi.mock(
"@opentelemetry/exporter-trace-otlp-proto" , () => ({
OTLPTraceExporter:
function OTLPTraceExporter(options?: unknown) {
traceExporterCtor(options);
},
}));
vi.mock(
"@opentelemetry/exporter-logs-otlp-proto" , () => ({
OTLPLogExporter:
function OTLPLogExporter() {},
}));
vi.mock(
"@opentelemetry/sdk-logs" , () => ({
BatchLogRecordProcessor:
function BatchLogRecordProcessor() {},
LoggerProvider:
class {
getLogger = vi.fn(() => ({
emit: logEmit,
}));
shutdown = logShutdown;
},
}));
vi.mock(
"@opentelemetry/sdk-metrics" , () => ({
PeriodicExportingMetricReader:
function PeriodicExportingMetricReader() {},
}));
vi.mock(
"@opentelemetry/sdk-trace-base" , () => ({
ParentBasedSampler:
function ParentBasedSampler() {},
TraceIdRatioBasedSampler:
function TraceIdRatioBasedSampler() {},
}));
vi.mock(
"@opentelemetry/resources" , () => ({
resourceFromAttributes: vi.fn((attrs: Record<string, unknown>) => attrs),
Resource:
function Resource(_value?: unknown) {
// Constructor shape required by the mocked OpenTelemetry API.
},
}));
vi.mock(
"@opentelemetry/semantic-conventions" , () => ({
ATTR_SERVICE_NAME:
"service.name" ,
}));
import type { OpenClawPluginServiceContext } from
"../api.js" ;
import { emitDiagnosticEvent } from
"../api.js" ;
import { createDiagnosticsOtelService } from
"./service.js" ;
const OTEL_TEST_STATE_DIR =
"/tmp/openclaw-diagnostics-otel-test" ;
const OTEL_TEST_ENDPOINT =
"http://otel-collector:4318 ";
const OTEL_TEST_PROTOCOL =
"http/protobuf" ;
const TRACE_ID =
"4bf92f3577b34da6a3ce929d0e0e4736" ;
const SPAN_ID =
"00f067aa0ba902b7" ;
const CHILD_SPAN_ID =
"1111111111111111" ;
const GRANDCHILD_SPAN_ID =
"2222222222222222" ;
const PROTO_KEY =
"__proto__" ;
const MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS =
4096 ;
const OTEL_TRUNCATED_SUFFIX_MAX_CHARS =
20 ;
const ORIGINAL_OPENCLAW_OTEL_PRELOADED = process.env.OPENCLAW_OTEL_PRELOADED;
function createLogger() {
return {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
}
type OtelContextFlags = {
traces?:
boolean ;
metrics?:
boolean ;
logs?:
boolean ;
captureContent?: NonNullable<
NonNullable<OpenClawPluginServiceContext[
"config" ][
"diagnostics" ]>[
"otel" ]
>[
"captureContent" ];
};
function createOtelContext(
endpoint: string,
{ traces =
false , metrics =
false , logs =
false , captureContent }: OtelContextFlags = {},
): OpenClawPluginServiceContext {
return {
config: {
diagnostics: {
enabled:
true ,
otel: {
enabled:
true ,
endpoint,
protocol: OTEL_TEST_PROTOCOL,
traces,
metrics,
logs,
...(captureContent !== undefined ? { captureContent } : {}),
},
},
},
logger: createLogger(),
stateDir: OTEL_TEST_STATE_DIR,
};
}
function createTraceOnlyContext(endpoint: string): OpenClawPluginServiceContext {
return createOtelContext(endpoint, { traces:
true });
}
async
function emitAndCaptureLog(
event: Omit<Extract<Parameters<
typeof emitDiagnosticEvent>[
0 ], { type:
"log.record" }>,
"type" >,
) {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs:
true });
await service.start(ctx);
emitDiagnosticEvent({
type:
"log.record" ,
...event,
});
await flushDiagnosticEvents();
expect(logEmit).toHaveBeenCalled();
const emitCall = logEmit.mock.calls[
0 ]?.[
0 ];
await service.stop?.(ctx);
return emitCall;
}
function flushDiagnosticEvents() {
return new Promise<
void >((resolve) => setImmediate(resolve));
}
describe(
"diagnostics-otel service" , () => {
beforeEach(() => {
delete process.env.OPENCLAW_OTEL_PRELOADED;
telemetryState.counters.clear();
telemetryState.histograms.clear();
telemetryState.spans.length =
0 ;
telemetryState.tracer.startSpan.mockClear();
telemetryState.tracer.setSpanContext.mockClear();
telemetryState.meter.createCounter.mockClear();
telemetryState.meter.createHistogram.mockClear();
sdkStart.mockClear();
sdkShutdown.mockClear();
logEmit.mockReset();
logShutdown.mockClear();
traceExporterCtor.mockClear();
});
afterEach(() => {
if (ORIGINAL_OPENCLAW_OTEL_PRELOADED === undefined) {
delete process.env.OPENCLAW_OTEL_PRELOADED;
}
else {
process.env.OPENCLAW_OTEL_PRELOADED = ORIGINAL_OPENCLAW_OTEL_PRELOADED;
}
});
test(
"records message-flow metrics and spans" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces:
true , metrics:
true , logs:
true });
await service.start(ctx);
emitDiagnosticEvent({
type:
"webhook.received" ,
channel:
"telegram" ,
updateType:
"telegram-post" ,
});
emitDiagnosticEvent({
type:
"webhook.processed" ,
channel:
"telegram" ,
updateType:
"telegram-post" ,
durationMs:
120 ,
});
emitDiagnosticEvent({
type:
"message.queued" ,
channel:
"telegram" ,
source:
"telegram" ,
queueDepth:
2 ,
});
emitDiagnosticEvent({
type:
"message.processed" ,
channel:
"telegram" ,
outcome:
"completed" ,
durationMs:
55 ,
});
emitDiagnosticEvent({
type:
"queue.lane.dequeue" ,
lane:
"main" ,
queueSize:
3 ,
waitMs:
10 ,
});
emitDiagnosticEvent({
type:
"session.stuck" ,
state:
"processing" ,
ageMs:
125 _
000 ,
});
emitDiagnosticEvent({
type:
"run.attempt" ,
runId:
"run-1" ,
attempt:
2 ,
});
expect(telemetryState.counters.get(
"openclaw.webhook.received" )?.add).toHaveBeen
Called();
expect(
telemetryState.histograms.get("openclaw.webhook.duration_ms" )?.record,
).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.message.queued" )?.add).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.message.processed" )?.add).toHaveBeenCalled();
expect(
telemetryState.histograms.get("openclaw.message.duration_ms" )?.record,
).toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.queue.wait_ms" )?.record).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.session.stuck" )?.add).toHaveBeenCalled();
expect(
telemetryState.histograms.get("openclaw.session.stuck_age_ms" )?.record,
).toHaveBeenCalled();
expect(telemetryState.counters.get("openclaw.run.attempt" )?.add).toHaveBeenCalled();
const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0 ]);
expect(spanNames).toContain("openclaw.webhook.processed" );
expect(spanNames).toContain("openclaw.message.processed" );
expect(spanNames).toContain("openclaw.session.stuck" );
emitDiagnosticEvent({
type: "log.record" ,
level: "INFO" ,
message: "hello" ,
attributes: { subsystem: "diagnostic" },
});
await flushDiagnosticEvents();
expect(logEmit).toHaveBeenCalled();
await service.stop?.(ctx);
});
test("restarts without retaining prior listeners or log transports" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true , logs: true });
await service.start(ctx);
await service.start(ctx);
expect(logShutdown).toHaveBeenCalledTimes(1 );
expect(sdkShutdown).toHaveBeenCalledTimes(1 );
telemetryState.tracer.startSpan.mockClear();
emitDiagnosticEvent({
type: "message.processed" ,
channel: "telegram" ,
outcome: "completed" ,
durationMs: 10 ,
});
expect(telemetryState.tracer.startSpan).toHaveBeenCalledTimes(1 );
await service.stop?.(ctx);
expect(logShutdown).toHaveBeenCalledTimes(2 );
expect(sdkShutdown).toHaveBeenCalledTimes(2 );
telemetryState.tracer.startSpan.mockClear();
emitDiagnosticEvent({
type: "message.processed" ,
channel: "telegram" ,
outcome: "completed" ,
durationMs: 10 ,
});
expect(telemetryState.tracer.startSpan).not.toHaveBeenCalled();
});
test("uses a preloaded OpenTelemetry SDK without dropping diagnostic listeners" , async () => {
process.env.OPENCLAW_OTEL_PRELOADED = "1" ;
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true , logs: true });
await service.start(ctx);
expect(sdkStart).not.toHaveBeenCalled();
expect(traceExporterCtor).not.toHaveBeenCalled();
expect(ctx.logger.info).toHaveBeenCalledWith(
"diagnostics-otel: using preloaded OpenTelemetry SDK" ,
);
emitDiagnosticEvent({
type: "run.completed" ,
runId: "run-1" ,
provider: "openai" ,
model: "gpt-5.4" ,
outcome: "completed" ,
durationMs: 100 ,
});
emitDiagnosticEvent({
type: "log.record" ,
level: "INFO" ,
message: "preloaded log" ,
});
await flushDiagnosticEvents();
expect(telemetryState.histograms.get("openclaw.run.duration_ms" )?.record).toHaveBeenCalledWith(
100 ,
expect.objectContaining({
"openclaw.provider" : "openai" ,
"openclaw.model" : "gpt-5.4" ,
}),
);
expect(telemetryState.tracer.startSpan).toHaveBeenCalledWith(
"openclaw.run" ,
expect.objectContaining({
attributes: expect.objectContaining({
"openclaw.outcome" : "completed" ,
}),
}),
undefined,
);
expect(logEmit).toHaveBeenCalled();
await service.stop?.(ctx);
expect(sdkShutdown).not.toHaveBeenCalled();
expect(logShutdown).toHaveBeenCalledTimes(1 );
});
test("honors disabled traces when an OpenTelemetry SDK is preloaded" , async () => {
process.env.OPENCLAW_OTEL_PRELOADED = "1" ;
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: false , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "run.completed" ,
runId: "run-1" ,
provider: "openai" ,
model: "gpt-5.4" ,
outcome: "completed" ,
durationMs: 100 ,
});
expect(sdkStart).not.toHaveBeenCalled();
expect(telemetryState.histograms.get("openclaw.run.duration_ms" )?.record).toHaveBeenCalledWith(
100 ,
expect.objectContaining({
"openclaw.provider" : "openai" ,
}),
);
expect(telemetryState.tracer.startSpan).not.toHaveBeenCalled();
await service.stop?.(ctx);
expect(sdkShutdown).not.toHaveBeenCalled();
});
test("tears down active handles when restarted with diagnostics disabled" , async () => {
const service = createDiagnosticsOtelService();
const enabledCtx = createOtelContext(OTEL_TEST_ENDPOINT, {
traces: true ,
metrics: true ,
logs: true ,
});
await service.start(enabledCtx);
await service.start({
...enabledCtx,
config: { diagnostics: { enabled: false } },
});
expect(logShutdown).toHaveBeenCalledTimes(1 );
expect(sdkShutdown).toHaveBeenCalledTimes(1 );
telemetryState.tracer.startSpan.mockClear();
emitDiagnosticEvent({
type: "message.processed" ,
channel: "telegram" ,
outcome: "completed" ,
durationMs: 10 ,
});
expect(telemetryState.tracer.startSpan).not.toHaveBeenCalled();
});
test("appends signal path when endpoint contains non-signal /v1 segment" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createTraceOnlyContext("https://www.comet.com/opik/api/v1/private/otel ");
await service.start(ctx);
const options = traceExporterCtor.mock.calls[0 ]?.[0 ] as { url?: string } | undefined;
expect(options?.url).toBe("https://www.comet.com/opik/api/v1/private/otel/v1/traces ");
await service.stop?.(ctx);
});
test("keeps already signal-qualified endpoint unchanged" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createTraceOnlyContext("https://collector.example.com/v1/traces ");
await service.start(ctx);
const options = traceExporterCtor.mock.calls[0 ]?.[0 ] as { url?: string } | undefined;
expect(options?.url).toBe("https://collector.example.com/v1/traces ");
await service.stop?.(ctx);
});
test("keeps signal-qualified endpoint unchanged when it has query params" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createTraceOnlyContext("https://collector.example.com/v1/traces?timeout=30s ");
await service.start(ctx);
const options = traceExporterCtor.mock.calls[0 ]?.[0 ] as { url?: string } | undefined;
expect(options?.url).toBe("https://collector.example.com/v1/traces?timeout=30s ");
await service.stop?.(ctx);
});
test("keeps signal-qualified endpoint unchanged when signal path casing differs" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createTraceOnlyContext("https://collector.example.com/v1/Traces ");
await service.start(ctx);
const options = traceExporterCtor.mock.calls[0 ]?.[0 ] as { url?: string } | undefined;
expect(options?.url).toBe("https://collector.example.com/v1/Traces ");
await service.stop?.(ctx);
});
test("redacts sensitive data from log messages before export" , async () => {
const emitCall = await emitAndCaptureLog({
level: "INFO" ,
message: "Using API key sk-1234567890abcdef1234567890abcdef" ,
});
expect(emitCall?.body).not.toContain("sk-1234567890abcdef1234567890abcdef" );
expect(emitCall?.body).toContain("sk-123" );
expect(emitCall?.body).toContain("…" );
});
test("redacts sensitive data from log attributes before export" , async () => {
const emitCall = await emitAndCaptureLog({
level: "DEBUG" ,
message: "auth configured" ,
attributes: {
token: "ghp_abcdefghijklmnopqrstuvwxyz123456" , // pragma: allowlist secret
},
});
const tokenAttr = emitCall?.attributes?.["openclaw.token" ];
expect(tokenAttr).not.toBe("ghp_abcdefghijklmnopqrstuvwxyz123456" ); // pragma: allowlist secret
if (typeof tokenAttr === "string" ) {
expect(tokenAttr).toContain("…" );
}
});
test("attaches diagnostic trace context to exported logs" , async () => {
const emitCall = await emitAndCaptureLog({
level: "INFO" ,
message: "traceable log" ,
attributes: {
subsystem: "diagnostic" ,
},
trace: {
traceId: TRACE_ID,
spanId: SPAN_ID,
traceFlags: "01" ,
},
});
expect(emitCall?.attributes).toMatchObject({
"openclaw.traceFlags" : "01" ,
});
expect(emitCall?.attributes).toEqual(
expect.not.objectContaining({
"openclaw.traceId" : expect.anything(),
"openclaw.spanId" : expect.anything(),
}),
);
expect(telemetryState.tracer.setSpanContext).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
traceId: TRACE_ID,
spanId: SPAN_ID,
traceFlags: 1 ,
isRemote: true ,
}),
);
expect(emitCall?.context).toEqual({
spanContext: expect.objectContaining({
traceId: TRACE_ID,
spanId: SPAN_ID,
}),
});
});
test("bounds plugin-emitted log attributes and omits source paths" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
await service.start(ctx);
const attributes = Object.create(null ) as Record<string, string>;
attributes.good = "y" .repeat(6000 );
attributes["bad key" ] = "drop-me" ;
attributes[PROTO_KEY] = "pollute" ;
attributes["constructor" ] = "pollute" ;
attributes["prototype" ] = "pollute" ;
attributes["sk-1234567890abcdef1234567890abcdef" ] = "secret-key" ; // pragma: allowlist secret
emitDiagnosticEvent({
type: "log.record" ,
level: "INFO" ,
message: "x" .repeat(6000 ),
attributes,
code: {
filepath: "/Users/alice/openclaw/src/private.ts" ,
line: 42 ,
functionName: "handler" ,
location: "/Users/alice/openclaw/src/private.ts:42" ,
},
} as Parameters<typeof emitDiagnosticEvent>[0 ]);
await flushDiagnosticEvents();
const emitCall = logEmit.mock.calls[0 ]?.[0 ];
expect(emitCall?.body.length).toBeLessThanOrEqual(4200 );
expect(emitCall?.attributes).toMatchObject({
"openclaw.good" : expect.stringMatching(/^y+/),
"code.lineno" : 42 ,
"code.function" : "handler" ,
});
expect(String(emitCall?.attributes?.["openclaw.good" ]).length).toBeLessThanOrEqual(4200 );
expect(Object.hasOwn(emitCall?.attributes ?? {}, `openclaw.${PROTO_KEY}`)).toBe(false );
expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.constructor" )).toBe(false );
expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.prototype" )).toBe(false );
expect(
Object.hasOwn(
emitCall?.attributes ?? {},
"openclaw.sk-1234567890abcdef1234567890abcdef" , // pragma: allowlist secret
),
).toBe(false );
expect(emitCall?.attributes).toEqual(
expect.not.objectContaining({
"openclaw.bad key" : expect.anything(),
"code.filepath" : expect.anything(),
"openclaw.code.location" : expect.anything(),
}),
);
await service.stop?.(ctx);
});
test("rate-limits repeated log export failure reports" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
const nowSpy = vi.spyOn(Date, "now" ).mockReturnValue(1 _000 );
logEmit.mockImplementation(() => {
throw new Error("export failed" );
});
try {
await service.start(ctx);
emitDiagnosticEvent({
type: "log.record" ,
level: "ERROR" ,
message: "first failing log" ,
});
emitDiagnosticEvent({
type: "log.record" ,
level: "ERROR" ,
message: "second failing log" ,
});
await flushDiagnosticEvents();
expect(ctx.logger.error).toHaveBeenCalledTimes(1 );
nowSpy.mockReturnValue(62 _000 );
emitDiagnosticEvent({
type: "log.record" ,
level: "ERROR" ,
message: "third failing log" ,
});
await flushDiagnosticEvents();
expect(ctx.logger.error).toHaveBeenCalledTimes(2 );
} finally {
nowSpy.mockRestore();
await service.stop?.(ctx);
}
});
test("does not parent diagnostic event spans from plugin-emittable trace context" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "model.usage" ,
trace: {
traceId: TRACE_ID,
spanId: SPAN_ID,
traceFlags: "01" ,
},
provider: "openai" ,
model: "gpt-5.4" ,
usage: { total: 4 },
durationMs: 12 ,
});
const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.model.usage" ,
);
expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
expect(modelUsageCall?.[2 ]).toBeUndefined();
await service.stop?.(ctx);
});
test("exports run, model call, and tool execution lifecycle spans" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "run.completed" ,
runId: "run-1" ,
sessionKey: "session-key" ,
provider: "openai" ,
model: "gpt-5.4" ,
channel: "webchat" ,
outcome: "completed" ,
durationMs: 100 ,
trace: {
traceId: TRACE_ID,
spanId: SPAN_ID,
traceFlags: "01" ,
},
});
emitDiagnosticEvent({
type: "model.call.completed" ,
runId: "run-1" ,
callId: "call-1" ,
provider: "openai" ,
model: "gpt-5.4" ,
api: "completions" ,
transport: "http" ,
durationMs: 80 ,
trace: {
traceId: TRACE_ID,
spanId: CHILD_SPAN_ID,
parentSpanId: SPAN_ID,
traceFlags: "01" ,
},
});
emitDiagnosticEvent({
type: "tool.execution.error" ,
runId: "run-1" ,
toolName: "read" ,
toolCallId: "tool-1" ,
paramsSummary: { kind: "object" },
durationMs: 20 ,
errorCategory: "TypeError" ,
errorCode: "429" ,
trace: {
traceId: TRACE_ID,
spanId: GRANDCHILD_SPAN_ID,
parentSpanId: CHILD_SPAN_ID,
traceFlags: "01" ,
},
});
await flushDiagnosticEvents();
const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0 ]);
expect(spanNames).toEqual(
expect.arrayContaining(["openclaw.run" , "openclaw.model.call" , "openclaw.tool.execution" ]),
);
const runCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.run" ,
);
expect(runCall?.[1 ]).toMatchObject({
attributes: {
"openclaw.outcome" : "completed" ,
"openclaw.provider" : "openai" ,
"openclaw.model" : "gpt-5.4" ,
"openclaw.channel" : "webchat" ,
},
startTime: expect.any(Number),
});
expect(runCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"gen_ai.system" : expect.anything(),
"gen_ai.request.model" : expect.anything(),
"openclaw.runId" : expect.anything(),
"openclaw.sessionKey" : expect.anything(),
"openclaw.traceId" : expect.anything(),
}),
startTime: expect.any(Number),
});
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.model.call" ,
);
expect(modelCall?.[1 ]).toMatchObject({
attributes: {
"gen_ai.system" : "openai" ,
"gen_ai.request.model" : "gpt-5.4" ,
"gen_ai.operation.name" : "text_completion" ,
},
});
expect(modelCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.callId" : expect.anything(),
"openclaw.runId" : expect.anything(),
"openclaw.sessionKey" : expect.anything(),
}),
startTime: expect.any(Number),
});
expect(modelCall?.[2 ]).toBeUndefined();
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.tool.execution" ,
);
expect(toolCall?.[1 ]).toMatchObject({
attributes: {
"openclaw.toolName" : "read" ,
"openclaw.errorCategory" : "TypeError" ,
"openclaw.errorCode" : "429" ,
"openclaw.tool.params.kind" : "object" ,
"gen_ai.tool.name" : "read" ,
},
});
expect(toolCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.toolCallId" : expect.anything(),
"openclaw.runId" : expect.anything(),
"openclaw.sessionKey" : expect.anything(),
}),
startTime: expect.any(Number),
});
expect(toolCall?.[2 ]).toBeUndefined();
expect(
telemetryState.histograms.get("openclaw.model_call.duration_ms" )?.record,
).toHaveBeenCalledWith(
80 ,
expect.objectContaining({
"openclaw.provider" : "openai" ,
"openclaw.model" : "gpt-5.4" ,
}),
);
expect(telemetryState.histograms.get("openclaw.run.duration_ms" )?.record).toHaveBeenCalledWith(
100 ,
expect.not.objectContaining({
"openclaw.runId" : expect.anything(),
}),
);
expect(
telemetryState.histograms.get("openclaw.tool.execution.duration_ms" )?.record,
).toHaveBeenCalledWith(
20 ,
expect.not.objectContaining({
"openclaw.errorCode" : expect.anything(),
"openclaw.runId" : expect.anything(),
}),
);
const toolSpan = telemetryState.spans.find((span) => span.name === "openclaw.tool.execution" );
expect(toolSpan?.setStatus).toHaveBeenCalledWith({
code: 2 ,
message: "TypeError" ,
});
expect(toolSpan?.end).toHaveBeenCalledWith(expect.any(Number));
expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
await service.stop?.(ctx);
});
test("exports exec process spans without command text" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "exec.process.completed" ,
target: "host" ,
mode: "child" ,
outcome: "failed" ,
durationMs: 30 ,
commandLength: 42 ,
exitCode: 1 ,
timedOut: false ,
failureKind: "runtime-error" ,
});
await flushDiagnosticEvents();
expect(telemetryState.histograms.get("openclaw.exec.duration_ms" )?.record).toHaveBeenCalledWith(
30 ,
expect.objectContaining({
"openclaw.exec.target" : "host" ,
"openclaw.exec.mode" : "child" ,
"openclaw.outcome" : "failed" ,
"openclaw.failureKind" : "runtime-error" ,
}),
);
const execCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.exec" ,
);
expect(execCall?.[1 ]).toMatchObject({
attributes: {
"openclaw.exec.target" : "host" ,
"openclaw.exec.mode" : "child" ,
"openclaw.outcome" : "failed" ,
"openclaw.exec.command_length" : 42 ,
"openclaw.exec.exit_code" : 1 ,
"openclaw.exec.timed_out" : false ,
"openclaw.failureKind" : "runtime-error" ,
},
startTime: expect.any(Number),
});
expect(execCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.exec.command" : expect.anything(),
"openclaw.exec.workdir" : expect.anything(),
"openclaw.sessionKey" : expect.anything(),
}),
startTime: expect.any(Number),
});
const execSpan = telemetryState.spans.find((span) => span.name === "openclaw.exec" );
expect(execSpan?.setStatus).toHaveBeenCalledWith({
code: 2 ,
message: "runtime-error" ,
});
expect(execSpan?.end).toHaveBeenCalledWith(expect.any(Number));
await service.stop?.(ctx);
});
test("does not export model or tool content unless capture is explicitly enabled" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "model.call.completed" ,
runId: "run-1" ,
callId: "call-1" ,
provider: "openai" ,
model: "gpt-5.4" ,
durationMs: 80 ,
inputMessages: ["private user prompt" ],
outputMessages: ["private model reply" ],
systemPrompt: "private system prompt" ,
} as Parameters<typeof emitDiagnosticEvent>[0 ]);
emitDiagnosticEvent({
type: "tool.execution.completed" ,
runId: "run-1" ,
toolName: "read" ,
toolCallId: "tool-1" ,
durationMs: 20 ,
toolInput: "private tool input" ,
toolOutput: "private tool output" ,
} as Parameters<typeof emitDiagnosticEvent>[0 ]);
await flushDiagnosticEvents();
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.model.call" ,
);
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.tool.execution" ,
);
expect(modelCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.content.input_messages" : expect.anything(),
"openclaw.content.output_messages" : expect.anything(),
"openclaw.content.system_prompt" : expect.anything(),
}),
startTime: expect.any(Number),
});
expect(toolCall?.[1 ]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.content.tool_input" : expect.anything(),
"openclaw.content.tool_output" : expect.anything(),
}),
startTime: expect.any(Number),
});
await service.stop?.(ctx);
});
test("exports bounded redacted content when capture fields are opted in" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, {
traces: true ,
metrics: true ,
captureContent: {
enabled: true ,
inputMessages: true ,
outputMessages: true ,
toolInputs: true ,
toolOutputs: true ,
systemPrompt: true ,
},
});
await service.start(ctx);
emitDiagnosticEvent({
type: "model.call.completed" ,
runId: "run-1" ,
callId: "call-1" ,
provider: "openai" ,
model: "gpt-5.4" ,
durationMs: 80 ,
inputMessages: ["use key sk-1234567890abcdef1234567890abcdef" ], // pragma: allowlist secret
outputMessages: ["model reply" ],
systemPrompt: "system prompt" ,
} as Parameters<typeof emitDiagnosticEvent>[0 ]);
emitDiagnosticEvent({
type: "tool.execution.completed" ,
runId: "run-1" ,
toolName: "read" ,
toolCallId: "tool-1" ,
durationMs: 20 ,
toolInput: "tool input" ,
toolOutput: `${"x" .repeat(4077 )} Bearer ${"a" .repeat(80 )}`, // pragma: allowlist secret
} as Parameters<typeof emitDiagnosticEvent>[0 ]);
await flushDiagnosticEvents();
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.model.call" ,
);
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.tool.execution" ,
);
const modelAttrs = (modelCall?.[1 ] as { attributes?: Record<string, unknown> } | undefined)
?.attributes;
const toolAttrs = (toolCall?.[1 ] as { attributes?: Record<string, unknown> } | undefined)
?.attributes;
expect(modelAttrs).toMatchObject({
"openclaw.content.output_messages" : "model reply" ,
"openclaw.content.system_prompt" : "system prompt" ,
});
expect(String(modelAttrs?.["openclaw.content.input_messages" ])).not.toContain(
"sk-1234567890abcdef1234567890abcdef" , // pragma: allowlist secret
);
expect(toolAttrs).toMatchObject({
"openclaw.content.tool_input" : "tool input" ,
});
expect(String(toolAttrs?.["openclaw.content.tool_output" ]).length).toBeLessThanOrEqual(
MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS + OTEL_TRUNCATED_SUFFIX_MAX_CHARS,
);
expect(String(toolAttrs?.["openclaw.content.tool_output" ])).not.toContain("a" .repeat(11 ));
await service.stop?.(ctx);
});
test("ignores invalid diagnostic event trace parents" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true , metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "model.usage" ,
trace: {
traceId: "0" .repeat(32 ),
spanId: "not-a-span" ,
traceFlags: "zz" ,
},
provider: "openai" ,
model: "gpt-5.4" ,
usage: { total: 4 },
durationMs: 12 ,
});
const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0 ] === "openclaw.model.usage" ,
);
expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
expect(modelUsageCall?.[2 ]).toBeUndefined();
await service.stop?.(ctx);
});
test("redacts sensitive reason in session.state metric attributes" , async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "session.state" ,
state: "waiting" ,
reason: "token=ghp_abcdefghijklmnopqrstuvwxyz123456" , // pragma: allowlist secret
});
const sessionCounter = telemetryState.counters.get("openclaw.session.state" );
expect(sessionCounter?.add).toHaveBeenCalledWith(
1 ,
expect.objectContaining({
"openclaw.reason" : expect.stringContaining("…" ),
}),
);
const attrs = sessionCounter?.add.mock.calls[0 ]?.[1 ] as Record<string, unknown> | undefined;
expect(typeof attrs?.["openclaw.reason" ]).toBe("string" );
expect(String(attrs?.["openclaw.reason" ])).not.toContain(
"ghp_abcdefghijklmnopqrstuvwxyz123456" , // pragma: allowlist secret
);
await service.stop?.(ctx);
});
});
Messung V0.5 in Prozent C=99 H=100 G=99
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-06-04)
¤
*© Formatika GbR, Deutschland