feat(diagnostics): surface provider request id hashes

This commit is contained in:
Vincent Koc
2026-04-25 10:38:31 -07:00
parent 2e0ae56b1a
commit 44114328b4
8 changed files with 218 additions and 8 deletions

View File

@@ -5,12 +5,14 @@ const telemetryState = vi.hoisted(() => {
const histograms = new Map<string, { record: ReturnType<typeof vi.fn> }>();
const spans: Array<{
name: string;
addEvent: ReturnType<typeof vi.fn>;
end: ReturnType<typeof vi.fn>;
setStatus: ReturnType<typeof vi.fn>;
}> = [];
const tracer = {
startSpan: vi.fn((name: string, _opts?: unknown, _ctx?: unknown) => {
const span = {
addEvent: vi.fn(),
end: vi.fn(),
setStatus: vi.fn(),
};
@@ -945,6 +947,48 @@ describe("diagnostics-otel service", () => {
await service.stop?.(ctx);
});
test("records upstream request id hashes as model call span events only", async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "model.call.error",
runId: "run-1",
callId: "call-1",
provider: "openai",
model: "gpt-5.4",
api: "openai-responses",
durationMs: 40,
errorCategory: "ProviderError",
upstreamRequestIdHash: "sha256:123456abcdef",
});
await flushDiagnosticEvents();
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.model.call",
);
expect(modelCall?.[1]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.upstreamRequestIdHash": expect.anything(),
}),
startTime: expect.any(Number),
});
const span = telemetryState.spans.find((candidate) => candidate.name === "openclaw.model.call");
expect(span?.addEvent).toHaveBeenCalledWith("openclaw.provider.request", {
"openclaw.upstreamRequestIdHash": "sha256:123456abcdef",
});
expect(
telemetryState.histograms.get("openclaw.model_call.duration_ms")?.record,
).toHaveBeenCalledWith(
40,
expect.not.objectContaining({
"openclaw.upstreamRequestIdHash": expect.anything(),
}),
);
await service.stop?.(ctx);
});
test("parents trusted diagnostic lifecycle spans from explicit parent ids", async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });

View File

@@ -184,6 +184,22 @@ function assignGenAiModelCallAttrs(
attrs["gen_ai.operation.name"] = genAiOperationName(evt.api);
}
function addUpstreamRequestIdSpanEvent(
span: { addEvent?: (name: string, attributes?: Record<string, string>) => void },
upstreamRequestIdHash: string | undefined,
): void {
if (!upstreamRequestIdHash) {
return;
}
const boundedHash = lowCardinalityAttr(upstreamRequestIdHash);
if (boundedHash === "unknown") {
return;
}
span.addEvent?.("openclaw.provider.request", {
"openclaw.upstreamRequestIdHash": boundedHash,
});
}
function clampOtelLogText(value: string, maxChars: number): string {
return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value;
}
@@ -1148,6 +1164,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
parentContext: contextForTrustedDiagnosticSpanParent(evt, metadata),
endTimeMs: evt.ts,
});
addUpstreamRequestIdSpanEvent(span, evt.upstreamRequestIdHash);
span.end(evt.ts);
};
@@ -1184,6 +1201,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
parentContext: contextForTrustedDiagnosticSpanParent(evt, metadata),
endTimeMs: evt.ts,
});
addUpstreamRequestIdSpanEvent(span, evt.upstreamRequestIdHash);
span.setStatus({
code: SpanStatusCode.ERROR,
message: redactSensitiveText(evt.errorCategory),