fix(diagnostics): gate traceparent propagation on trusted metadata

This commit is contained in:
Vincent Koc
2026-04-25 12:55:30 -07:00
parent b64bfc5d9a
commit 89c52988c5
3 changed files with 57 additions and 2 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
- Diagnostics/OTEL: keep model-usage span GenAI provider attributes aligned with the existing semantic-convention opt-in policy, using legacy `gen_ai.system` unless latest experimental GenAI conventions are enabled. Thanks @vincentkoc.
- Diagnostics/OTEL: keep `gen_ai.request.model` present on GenAI token usage metrics with a bounded `unknown` fallback when model usage events do not include a model. Thanks @vincentkoc.
- Docs/OTEL: document the GenAI token and model-call duration metrics, model-usage span attributes, and `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` provider-attribute behavior. Thanks @vincentkoc.
- Diagnostics/trace: add an internal traceparent propagation helper that only formats trusted dispatcher metadata, keeping plugin-emitted diagnostic traces out of outbound propagation by default. Thanks @vincentkoc.
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.

View File

@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
emitDiagnosticEvent,
emitTrustedDiagnosticEvent,
formatDiagnosticTraceparentForPropagation,
isDiagnosticsEnabled,
onInternalDiagnosticEvent,
onDiagnosticEvent,
@@ -147,6 +148,32 @@ describe("diagnostic-events", () => {
]);
});
it("formats traceparent for propagation only from dispatcher-trusted metadata", () => {
const trace = createDiagnosticTraceContext({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: "00f067aa0ba902b7",
traceFlags: "01",
});
const traceparents: Array<string | undefined> = [];
onInternalDiagnosticEvent((event, metadata) => {
traceparents.push(formatDiagnosticTraceparentForPropagation(event, metadata));
});
emitDiagnosticEvent({
type: "message.queued",
source: "plugin",
trace,
});
emitTrustedDiagnosticEvent({
type: "model.usage",
usage: { total: 1 },
trace,
});
expect(traceparents).toEqual([undefined, `00-${trace.traceId}-${trace.spanId}-01`]);
expect(formatDiagnosticTraceparentForPropagation({ trace }, { trusted: true })).toBeUndefined();
});
it("shares diagnostic state across duplicate module instances", async () => {
const events: string[] = [];
onDiagnosticEvent((event) => {

View File

@@ -1,5 +1,8 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { DiagnosticTraceContext } from "./diagnostic-trace-context.js";
import {
formatDiagnosticTraceparent,
type DiagnosticTraceContext,
} from "./diagnostic-trace-context.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
export type DiagnosticSessionState = "idle" | "processing" | "waiting";
@@ -413,6 +416,7 @@ type DiagnosticEventsGlobalState = {
const MAX_ASYNC_DIAGNOSTIC_EVENTS = 10_000;
const DIAGNOSTIC_EVENTS_STATE_KEY = Symbol.for("openclaw.diagnosticEvents.state.v1");
const dispatchedTrustedDiagnosticMetadata = new WeakSet<object>();
const ASYNC_DIAGNOSTIC_EVENT_TYPES = new Set<DiagnosticEventPayload["type"]>([
"tool.execution.started",
"tool.execution.completed",
@@ -500,7 +504,10 @@ function dispatchDiagnosticEvent(
try {
for (const listener of state.listeners) {
try {
listener(cloneDiagnosticEventForListener(enriched), Object.freeze({ ...metadata }));
listener(
cloneDiagnosticEventForListener(enriched),
createDiagnosticMetadataForListener(metadata),
);
} catch (err) {
const errorMessage =
err instanceof Error
@@ -519,6 +526,16 @@ function dispatchDiagnosticEvent(
}
}
function createDiagnosticMetadataForListener(
metadata: DiagnosticEventMetadata,
): DiagnosticEventMetadata {
const listenerMetadata = Object.freeze({ ...metadata });
if (listenerMetadata.trusted) {
dispatchedTrustedDiagnosticMetadata.add(listenerMetadata);
}
return listenerMetadata;
}
function cloneDiagnosticEventForListener(event: DiagnosticEventPayload): DiagnosticEventPayload {
return deepFreezeDiagnosticValue(structuredClone(event)) as DiagnosticEventPayload;
}
@@ -623,6 +640,16 @@ export function onDiagnosticEvent(listener: (evt: DiagnosticEventPayload) => voi
});
}
export function formatDiagnosticTraceparentForPropagation(
event: { trace?: DiagnosticTraceContext },
metadata: DiagnosticEventMetadata,
): string | undefined {
if (!metadata.trusted || !dispatchedTrustedDiagnosticMetadata.has(metadata)) {
return undefined;
}
return formatDiagnosticTraceparent(event.trace);
}
export function resetDiagnosticEventsForTest(): void {
const state = getDiagnosticEventsState();
state.enabled = true;