






















@@ -1,7 +1,5 @@
11import { beforeEach, describe, expect, test, vi } from "vitest";
223-const registerLogTransportMock = vi.hoisted(() => vi.fn());
4-53const telemetryState = vi.hoisted(() => {
64const counters = new Map<string, { add: ReturnType<typeof vi.fn> }>();
75const histograms = new Map<string, { record: ReturnType<typeof vi.fn> }>();
@@ -113,14 +111,6 @@ vi.mock("@opentelemetry/semantic-conventions", () => ({
113111ATTR_SERVICE_NAME: "service.name",
114112}));
115113116-vi.mock("../api.js", async () => {
117-const actual = await vi.importActual<typeof import("../api.js")>("../api.js");
118-return {
119- ...actual,
120-registerLogTransport: registerLogTransportMock,
121-};
122-});
123-124114import type { OpenClawPluginServiceContext } from "../api.js";
125115import { emitDiagnosticEvent } from "../api.js";
126116import { createDiagnosticsOtelService } from "./service.js";
@@ -132,6 +122,7 @@ const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
132122const SPAN_ID = "00f067aa0ba902b7";
133123const CHILD_SPAN_ID = "1111111111111111";
134124const GRANDCHILD_SPAN_ID = "2222222222222222";
125+const PROTO_KEY = "__proto__";
135126136127function createLogger() {
137128return {
@@ -174,26 +165,17 @@ function createTraceOnlyContext(endpoint: string): OpenClawPluginServiceContext
174165return createOtelContext(endpoint, { traces: true });
175166}
176167177-type RegisteredLogTransport = (logObj: Record<string, unknown>) => void;
178-function setupRegisteredTransports() {
179-const registeredTransports: RegisteredLogTransport[] = [];
180-const stopTransports: ReturnType<typeof vi.fn>[] = [];
181-registerLogTransportMock.mockImplementation((transport) => {
182-registeredTransports.push(transport);
183-const stopTransport = vi.fn();
184-stopTransports.push(stopTransport);
185-return stopTransport;
186-});
187-return { registeredTransports, stopTransports };
188-}
189-190-async function emitAndCaptureLog(logObj: Record<string, unknown>) {
191-const { registeredTransports } = setupRegisteredTransports();
168+async function emitAndCaptureLog(
169+event: Omit<Extract<Parameters<typeof emitDiagnosticEvent>[0], { type: "log.record" }>, "type">,
170+) {
192171const service = createDiagnosticsOtelService();
193172const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
194173await service.start(ctx);
195-expect(registeredTransports).toHaveLength(1);
196-registeredTransports[0]?.(logObj);
174+emitDiagnosticEvent({
175+type: "log.record",
176+ ...event,
177+});
178+await flushDiagnosticEvents();
197179expect(logEmit).toHaveBeenCalled();
198180const emitCall = logEmit.mock.calls[0]?.[0];
199181await service.stop?.(ctx);
@@ -215,15 +197,12 @@ describe("diagnostics-otel service", () => {
215197telemetryState.meter.createHistogram.mockClear();
216198sdkStart.mockClear();
217199sdkShutdown.mockClear();
218-logEmit.mockClear();
200+logEmit.mockReset();
219201logShutdown.mockClear();
220202traceExporterCtor.mockClear();
221-registerLogTransportMock.mockReset();
222203});
223204224205test("records message-flow metrics and spans", async () => {
225-const { registeredTransports } = setupRegisteredTransports();
226-227206const service = createDiagnosticsOtelService();
228207const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true, logs: true });
229208await service.start(ctx);
@@ -289,29 +268,24 @@ describe("diagnostics-otel service", () => {
289268expect(spanNames).toContain("openclaw.message.processed");
290269expect(spanNames).toContain("openclaw.session.stuck");
291270292-expect(registerLogTransportMock).toHaveBeenCalledTimes(1);
293-expect(registeredTransports).toHaveLength(1);
294-registeredTransports[0]?.({
295-0: '{"subsystem":"diagnostic"}',
296-1: "hello",
297-_meta: { logLevelName: "INFO", date: new Date() },
271+emitDiagnosticEvent({
272+type: "log.record",
273+level: "INFO",
274+message: "hello",
275+attributes: { subsystem: "diagnostic" },
298276});
277+await flushDiagnosticEvents();
299278expect(logEmit).toHaveBeenCalled();
300279301280await service.stop?.(ctx);
302281});
303282304283test("restarts without retaining prior listeners or log transports", async () => {
305-const { registeredTransports, stopTransports } = setupRegisteredTransports();
306-307284const service = createDiagnosticsOtelService();
308285const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true, logs: true });
309286await service.start(ctx);
310287await service.start(ctx);
311288312-expect(registerLogTransportMock).toHaveBeenCalledTimes(2);
313-expect(registeredTransports).toHaveLength(2);
314-expect(stopTransports[0]).toHaveBeenCalledTimes(1);
315289expect(logShutdown).toHaveBeenCalledTimes(1);
316290expect(sdkShutdown).toHaveBeenCalledTimes(1);
317291@@ -325,7 +299,6 @@ describe("diagnostics-otel service", () => {
325299expect(telemetryState.tracer.startSpan).toHaveBeenCalledTimes(1);
326300327301await service.stop?.(ctx);
328-expect(stopTransports[1]).toHaveBeenCalledTimes(1);
329302expect(logShutdown).toHaveBeenCalledTimes(2);
330303expect(sdkShutdown).toHaveBeenCalledTimes(2);
331304@@ -340,8 +313,6 @@ describe("diagnostics-otel service", () => {
340313});
341314342315test("tears down active handles when restarted with diagnostics disabled", async () => {
343-const { stopTransports } = setupRegisteredTransports();
344-345316const service = createDiagnosticsOtelService();
346317const enabledCtx = createOtelContext(OTEL_TEST_ENDPOINT, {
347318traces: true,
@@ -354,7 +325,6 @@ describe("diagnostics-otel service", () => {
354325config: { diagnostics: { enabled: false } },
355326});
356327357-expect(stopTransports[0]).toHaveBeenCalledTimes(1);
358328expect(logShutdown).toHaveBeenCalledTimes(1);
359329expect(sdkShutdown).toHaveBeenCalledTimes(1);
360330@@ -410,8 +380,8 @@ describe("diagnostics-otel service", () => {
410380411381test("redacts sensitive data from log messages before export", async () => {
412382const emitCall = await emitAndCaptureLog({
413-0: "Using API key sk-1234567890abcdef1234567890abcdef",
414-_meta: { logLevelName: "INFO", date: new Date() },
383+level: "INFO",
384+message: "Using API key sk-1234567890abcdef1234567890abcdef",
415385});
416386417387expect(emitCall?.body).not.toContain("sk-1234567890abcdef1234567890abcdef");
@@ -421,9 +391,11 @@ describe("diagnostics-otel service", () => {
421391422392test("redacts sensitive data from log attributes before export", async () => {
423393const emitCall = await emitAndCaptureLog({
424-0: '{"token":"ghp_abcdefghijklmnopqrstuvwxyz123456"}', // pragma: allowlist secret
425-1: "auth configured",
426-_meta: { logLevelName: "DEBUG", date: new Date() },
394+level: "DEBUG",
395+message: "auth configured",
396+attributes: {
397+token: "ghp_abcdefghijklmnopqrstuvwxyz123456", // pragma: allowlist secret
398+},
427399});
428400429401const tokenAttr = emitCall?.attributes?.["openclaw.token"];
@@ -435,16 +407,16 @@ describe("diagnostics-otel service", () => {
435407436408test("attaches diagnostic trace context to exported logs", async () => {
437409const emitCall = await emitAndCaptureLog({
438-0: '{"subsystem":"diagnostic"}',
439-1: {
440-trace: {
441-traceId: TRACE_ID,
442-spanId: SPAN_ID,
443-traceFlags: "01",
444-},
410+level: "INFO",
411+message: "traceable log",
412+attributes: {
413+subsystem: "diagnostic",
414+},
415+trace: {
416+traceId: TRACE_ID,
417+spanId: SPAN_ID,
418+traceFlags: "01",
445419},
446-2: "traceable log",
447-_meta: { logLevelName: "INFO", date: new Date() },
448420});
449421450422expect(emitCall?.attributes).toMatchObject({
@@ -473,6 +445,99 @@ describe("diagnostics-otel service", () => {
473445});
474446});
475447448+test("bounds plugin-emitted log attributes and omits source paths", async () => {
449+const service = createDiagnosticsOtelService();
450+const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
451+await service.start(ctx);
452+453+const attributes = Object.create(null) as Record<string, string>;
454+attributes.good = "y".repeat(6000);
455+attributes["bad key"] = "drop-me";
456+attributes[PROTO_KEY] = "pollute";
457+attributes["constructor"] = "pollute";
458+attributes["prototype"] = "pollute";
459+attributes["sk-1234567890abcdef1234567890abcdef"] = "secret-key"; // pragma: allowlist secret
460+461+emitDiagnosticEvent({
462+type: "log.record",
463+level: "INFO",
464+message: "x".repeat(6000),
465+ attributes,
466+code: {
467+filepath: "/Users/alice/openclaw/src/private.ts",
468+line: 42,
469+functionName: "handler",
470+location: "/Users/alice/openclaw/src/private.ts:42",
471+},
472+} as Parameters<typeof emitDiagnosticEvent>[0]);
473+await flushDiagnosticEvents();
474+475+const emitCall = logEmit.mock.calls[0]?.[0];
476+expect(emitCall?.body.length).toBeLessThanOrEqual(4200);
477+expect(emitCall?.attributes).toMatchObject({
478+"openclaw.good": expect.stringMatching(/^y+/),
479+"code.lineno": 42,
480+"code.function": "handler",
481+});
482+expect(String(emitCall?.attributes?.["openclaw.good"]).length).toBeLessThanOrEqual(4200);
483+expect(Object.hasOwn(emitCall?.attributes ?? {}, `openclaw.${PROTO_KEY}`)).toBe(false);
484+expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.constructor")).toBe(false);
485+expect(Object.hasOwn(emitCall?.attributes ?? {}, "openclaw.prototype")).toBe(false);
486+expect(
487+Object.hasOwn(
488+emitCall?.attributes ?? {},
489+"openclaw.sk-1234567890abcdef1234567890abcdef", // pragma: allowlist secret
490+),
491+).toBe(false);
492+expect(emitCall?.attributes).toEqual(
493+expect.not.objectContaining({
494+"openclaw.bad key": expect.anything(),
495+"code.filepath": expect.anything(),
496+"openclaw.code.location": expect.anything(),
497+}),
498+);
499+await service.stop?.(ctx);
500+});
501+502+test("rate-limits repeated log export failure reports", async () => {
503+const service = createDiagnosticsOtelService();
504+const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
505+const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_000);
506+logEmit.mockImplementation(() => {
507+throw new Error("export failed");
508+});
509+try {
510+await service.start(ctx);
511+512+emitDiagnosticEvent({
513+type: "log.record",
514+level: "ERROR",
515+message: "first failing log",
516+});
517+emitDiagnosticEvent({
518+type: "log.record",
519+level: "ERROR",
520+message: "second failing log",
521+});
522+await flushDiagnosticEvents();
523+524+expect(ctx.logger.error).toHaveBeenCalledTimes(1);
525+526+nowSpy.mockReturnValue(62_000);
527+emitDiagnosticEvent({
528+type: "log.record",
529+level: "ERROR",
530+message: "third failing log",
531+});
532+await flushDiagnosticEvents();
533+534+expect(ctx.logger.error).toHaveBeenCalledTimes(2);
535+} finally {
536+nowSpy.mockRestore();
537+await service.stop?.(ctx);
538+}
539+});
540+476541test("does not parent diagnostic event spans from plugin-emittable trace context", async () => {
477542const service = createDiagnosticsOtelService();
478543const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。