






















@@ -5,11 +5,20 @@ const registerLogTransportMock = vi.hoisted(() => vi.fn());
55const telemetryState = vi.hoisted(() => {
66const counters = new Map<string, { add: ReturnType<typeof vi.fn> }>();
77const histograms = new Map<string, { record: ReturnType<typeof vi.fn> }>();
8+const spans: Array<{
9+name: string;
10+end: ReturnType<typeof vi.fn>;
11+setStatus: ReturnType<typeof vi.fn>;
12+}> = [];
813const tracer = {
9-startSpan: vi.fn((_name: string, _opts?: unknown, _ctx?: unknown) => ({
10-end: vi.fn(),
11-setStatus: vi.fn(),
12-})),
14+startSpan: vi.fn((name: string, _opts?: unknown, _ctx?: unknown) => {
15+const span = {
16+end: vi.fn(),
17+setStatus: vi.fn(),
18+};
19+spans.push({ name, ...span });
20+return span;
21+}),
1322setSpanContext: vi.fn((_ctx: unknown, spanContext: unknown) => ({ spanContext })),
1423};
1524const meter = {
@@ -24,7 +33,7 @@ const telemetryState = vi.hoisted(() => {
2433return histogram;
2534}),
2635};
27-return { counters, histograms, tracer, meter };
36+return { counters, histograms, spans, tracer, meter };
2837});
29383039const sdkStart = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
@@ -121,6 +130,8 @@ const OTEL_TEST_ENDPOINT = "http://otel-collector:4318";
121130const OTEL_TEST_PROTOCOL = "http/protobuf";
122131const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
123132const SPAN_ID = "00f067aa0ba902b7";
133+const CHILD_SPAN_ID = "1111111111111111";
134+const GRANDCHILD_SPAN_ID = "2222222222222222";
124135125136function createLogger() {
126137return {
@@ -189,10 +200,15 @@ async function emitAndCaptureLog(logObj: Record<string, unknown>) {
189200return emitCall;
190201}
191202203+function flushDiagnosticEvents() {
204+return new Promise<void>((resolve) => setImmediate(resolve));
205+}
206+192207describe("diagnostics-otel service", () => {
193208beforeEach(() => {
194209telemetryState.counters.clear();
195210telemetryState.histograms.clear();
211+telemetryState.spans.length = 0;
196212telemetryState.tracer.startSpan.mockClear();
197213telemetryState.tracer.setSpanContext.mockClear();
198214telemetryState.meter.createCounter.mockClear();
@@ -432,10 +448,14 @@ describe("diagnostics-otel service", () => {
432448});
433449434450expect(emitCall?.attributes).toMatchObject({
435-"openclaw.traceId": TRACE_ID,
436-"openclaw.spanId": SPAN_ID,
437451"openclaw.traceFlags": "01",
438452});
453+expect(emitCall?.attributes).toEqual(
454+expect.not.objectContaining({
455+"openclaw.traceId": expect.anything(),
456+"openclaw.spanId": expect.anything(),
457+}),
458+);
439459expect(telemetryState.tracer.setSpanContext).toHaveBeenCalledWith(
440460expect.anything(),
441461expect.objectContaining({
@@ -453,7 +473,7 @@ describe("diagnostics-otel service", () => {
453473});
454474});
455475456-test("parents diagnostic event spans from trace context", async () => {
476+test("does not parent diagnostic event spans from plugin-emittable trace context", async () => {
457477const service = createDiagnosticsOtelService();
458478const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
459479await service.start(ctx);
@@ -474,14 +494,167 @@ describe("diagnostics-otel service", () => {
474494const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
475495(call) => call[0] === "openclaw.model.usage",
476496);
477-expect(modelUsageCall?.[2]).toEqual({
478-spanContext: expect.objectContaining({
497+expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
498+expect(modelUsageCall?.[2]).toBeUndefined();
499+await service.stop?.(ctx);
500+});
501+502+test("exports run, model call, and tool execution lifecycle spans", async () => {
503+const service = createDiagnosticsOtelService();
504+const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
505+await service.start(ctx);
506+507+emitDiagnosticEvent({
508+type: "run.completed",
509+runId: "run-1",
510+sessionKey: "session-key",
511+provider: "openai",
512+model: "gpt-5.4",
513+channel: "webchat",
514+outcome: "completed",
515+durationMs: 100,
516+trace: {
479517traceId: TRACE_ID,
480518spanId: SPAN_ID,
481-traceFlags: 1,
482-isRemote: true,
519+traceFlags: "01",
520+},
521+});
522+emitDiagnosticEvent({
523+type: "model.call.completed",
524+runId: "run-1",
525+callId: "call-1",
526+provider: "openai",
527+model: "gpt-5.4",
528+api: "completions",
529+transport: "http",
530+durationMs: 80,
531+trace: {
532+traceId: TRACE_ID,
533+spanId: CHILD_SPAN_ID,
534+parentSpanId: SPAN_ID,
535+traceFlags: "01",
536+},
537+});
538+emitDiagnosticEvent({
539+type: "tool.execution.error",
540+runId: "run-1",
541+toolName: "read",
542+toolCallId: "tool-1",
543+paramsSummary: { kind: "object" },
544+durationMs: 20,
545+errorCategory: "TypeError",
546+errorCode: "429",
547+trace: {
548+traceId: TRACE_ID,
549+spanId: GRANDCHILD_SPAN_ID,
550+parentSpanId: CHILD_SPAN_ID,
551+traceFlags: "01",
552+},
553+});
554+await flushDiagnosticEvents();
555+556+const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0]);
557+expect(spanNames).toEqual(
558+expect.arrayContaining(["openclaw.run", "openclaw.model.call", "openclaw.tool.execution"]),
559+);
560+561+const runCall = telemetryState.tracer.startSpan.mock.calls.find(
562+(call) => call[0] === "openclaw.run",
563+);
564+expect(runCall?.[1]).toMatchObject({
565+attributes: {
566+"openclaw.outcome": "completed",
567+"openclaw.provider": "openai",
568+"openclaw.model": "gpt-5.4",
569+"openclaw.channel": "webchat",
570+},
571+startTime: expect.any(Number),
572+});
573+expect(runCall?.[1]).toEqual({
574+attributes: expect.not.objectContaining({
575+"gen_ai.system": expect.anything(),
576+"gen_ai.request.model": expect.anything(),
577+"openclaw.runId": expect.anything(),
578+"openclaw.sessionKey": expect.anything(),
579+"openclaw.traceId": expect.anything(),
483580}),
581+startTime: expect.any(Number),
484582});
583+584+const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
585+(call) => call[0] === "openclaw.model.call",
586+);
587+expect(modelCall?.[1]).toMatchObject({
588+attributes: {
589+"gen_ai.system": "openai",
590+"gen_ai.request.model": "gpt-5.4",
591+"gen_ai.operation.name": "text_completion",
592+},
593+});
594+expect(modelCall?.[1]).toEqual({
595+attributes: expect.not.objectContaining({
596+"openclaw.callId": expect.anything(),
597+"openclaw.runId": expect.anything(),
598+"openclaw.sessionKey": expect.anything(),
599+}),
600+startTime: expect.any(Number),
601+});
602+expect(modelCall?.[2]).toBeUndefined();
603+604+const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
605+(call) => call[0] === "openclaw.tool.execution",
606+);
607+expect(toolCall?.[1]).toMatchObject({
608+attributes: {
609+"openclaw.toolName": "read",
610+"openclaw.errorCategory": "TypeError",
611+"openclaw.errorCode": "429",
612+"openclaw.tool.params.kind": "object",
613+"gen_ai.tool.name": "read",
614+},
615+});
616+expect(toolCall?.[1]).toEqual({
617+attributes: expect.not.objectContaining({
618+"openclaw.toolCallId": expect.anything(),
619+"openclaw.runId": expect.anything(),
620+"openclaw.sessionKey": expect.anything(),
621+}),
622+startTime: expect.any(Number),
623+});
624+expect(toolCall?.[2]).toBeUndefined();
625+626+expect(
627+telemetryState.histograms.get("openclaw.model_call.duration_ms")?.record,
628+).toHaveBeenCalledWith(
629+80,
630+expect.objectContaining({
631+"openclaw.provider": "openai",
632+"openclaw.model": "gpt-5.4",
633+}),
634+);
635+expect(telemetryState.histograms.get("openclaw.run.duration_ms")?.record).toHaveBeenCalledWith(
636+100,
637+expect.not.objectContaining({
638+"openclaw.runId": expect.anything(),
639+}),
640+);
641+expect(
642+telemetryState.histograms.get("openclaw.tool.execution.duration_ms")?.record,
643+).toHaveBeenCalledWith(
644+20,
645+expect.not.objectContaining({
646+"openclaw.errorCode": expect.anything(),
647+"openclaw.runId": expect.anything(),
648+}),
649+);
650+651+const toolSpan = telemetryState.spans.find((span) => span.name === "openclaw.tool.execution");
652+expect(toolSpan?.setStatus).toHaveBeenCalledWith({
653+code: 2,
654+message: "TypeError",
655+});
656+expect(toolSpan?.end).toHaveBeenCalledWith(expect.any(Number));
657+expect(telemetryState.tracer.setSpanContext).not.toHaveBeenCalled();
485658await service.stop?.(ctx);
486659});
487660此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。