fix: expose workbench projection failures in otel (#1824)

This commit is contained in:
Lyon
2026-06-21 13:44:22 +08:00
committed by GitHub
parent 426048bca4
commit a5b47d5d8d
3 changed files with 51 additions and 2 deletions
+5 -1
View File
@@ -1095,7 +1095,11 @@ async function handleWorkbenchTurnSnapshot(response, url, options, actor, rawTur
}
const readModel = createWorkbenchReadModel(options, actor);
const result = await readModel.queryFacts({ traceId, limit: MAX_PAGE_LIMIT });
if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
if (result.error) {
const body = workbenchProjectionStoreError(result.error);
recordWorkbenchTurnReadMetric(options, url, 503, body, startedAt);
return sendJson(response, 503, body);
}
const session = visibleFactSessions(result.facts, actor).find((item) => item.lastTraceId === traceId) ?? visibleFactSessions(result.facts, actor)[0] ?? null;
const turn = factTurnForTrace(result.facts, traceId, turnId);
const found = Boolean(session || turn);
+35
View File
@@ -3,6 +3,7 @@
* 职责: Workbench durable facts store adapter session/message/turn/trace projection facts GET AgentRun facts
*/
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { emitCodeAgentOtelSpan } from "./otel-trace.ts";
import { safeConversationId, safeSessionId, safeTraceId } from "./server-http-utils.ts";
import { durableTraceStatus, normalizeWorkbenchStatus, TERMINAL_STATUSES, traceTerminalEvidence } from "./workbench-turn-projection.ts";
@@ -46,6 +47,7 @@ export function createWorkbenchFactsStore(options = {}, actor = null) {
retryExhausted: finalAttempt && retryable
});
if (finalAttempt) {
emitWorkbenchFactsQueryFailureOtel(params, annotatedError);
logWorkbenchFactsQueryFailure(options.logger ?? console, params, annotatedError);
return {
facts: emptyWorkbenchFacts(),
@@ -208,6 +210,39 @@ function logWorkbenchFactsQueryFailure(logger, params = {}, error = null) {
} catch {}
}
function emitWorkbenchFactsQueryFailureOtel(params = {}, error = null) {
const data = objectValue(error?.data) ?? {};
const families = Array.isArray(params.families) ? params.families : [];
const traceId = safeTraceId(params.traceId) ?? "trc_unassigned";
void emitCodeAgentOtelSpan("workbench.projection_store.query.failed", traceId, process.env, {
status: "error",
error,
attributes: {
"workbench.projection_store.error": true,
"workbench.projection_store.retryable": data.retryable === true,
"workbench.projection_store.transient": data.transient === true,
"workbench.projection_store.retry_attempt": otelNonNegativeIntegerOrNull(data.retryAttempt ?? data.retryAttempts),
"workbench.projection_store.retry_max": otelNonNegativeIntegerOrNull(data.retryMax),
"workbench.projection_store.retry_exhausted": data.retryExhausted === true,
"workbench.projection_store.retry_after_ms": otelNonNegativeIntegerOrNull(data.retryAfterMs),
"workbench.facts.family_count": families.length,
"workbench.facts.families": families.join(","),
"workbench.facts.has_session_id": Boolean(params.sessionId),
"workbench.facts.has_trace_id": Boolean(params.traceId),
"workbench.facts.has_owner_user_id": Boolean(params.ownerUserId),
"error.code": error?.code ?? null,
"error.category": "projection_store_unavailable",
"error.layer": "runtime-store",
valuesRedacted: true
}
}).catch(() => undefined);
}
function otelNonNegativeIntegerOrNull(value) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : null;
}
async function durableTraceSnapshot(runtimeStore, traceId) {
if (!runtimeStore || typeof runtimeStore.queryAgentTraceEvents !== "function") return null;
const safeId = safeTraceId(traceId);
@@ -307,7 +307,17 @@ function formatDuration(ms: number): string {
<template>
<section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
<LoadingState v-if="workbench.sessionDetailLoading" class="conversation-detail-loading" label="加载中" />
<article v-for="message in visibleMessages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
<article
v-for="message in visibleMessages"
:key="message.id"
class="message-card"
:data-role="message.role"
:data-status="message.status"
:data-session-id="message.sessionId || undefined"
:data-message-id="message.id || undefined"
:data-trace-id="message.traceId || message.runnerTrace?.traceId || undefined"
:data-turn-id="message.turnId || message.traceId || message.runnerTrace?.traceId || undefined"
>
<header v-if="message.role !== 'user'">
<div class="message-header-main">
<strong>{{ message.title }}</strong>