From 399dcc289ff9b839d699fcc83330788dc08576e8 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 20:44:49 +0000 Subject: [PATCH] fix: bound observability trace output --- .../platform-infra-observability/options.ts | 2 +- .../platform-infra-observability/render.ts | 120 +++++++++++++++++- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/scripts/src/platform-infra-observability/options.ts b/scripts/src/platform-infra-observability/options.ts index da64d767..0e0376cb 100644 --- a/scripts/src/platform-infra-observability/options.ts +++ b/scripts/src/platform-infra-observability/options.ts @@ -26,7 +26,7 @@ import { diagnoseCodeAgent, search, trace } from "./render"; export function observabilityHelp(): Record { return { command: "platform-infra observability plan|apply|status|validate|trace|search|diagnose-code-agent", - output: "text/table by default for search and diagnose-code-agent; use --full or --raw for structured JSON", + output: "text/table by default; trace --full expands bounded diagnostic tables, --raw returns backend JSON", configTruth: "config/platform-infra/observability.yaml", spec: "PJ2026-01060501 OTel追踪 draft-2026-06-19-p0", usage: [ diff --git a/scripts/src/platform-infra-observability/render.ts b/scripts/src/platform-infra-observability/render.ts index 8765051b..b6221f99 100644 --- a/scripts/src/platform-infra-observability/render.ts +++ b/scripts/src/platform-infra-observability/render.ts @@ -36,7 +36,7 @@ export async function trace(config: UniDeskConfig, options: TraceOptions): Promi const result = await capture(config, target.route, ["sh"], traceScript(observability, target, tracePath, options)); const parsed = parseJsonOutput(result.stdout); const ok = result.exitCode === 0 && parsed?.ok === true; - if (!options.full && !options.raw) { + if (!options.raw) { return renderTraceTable({ ok, target, @@ -208,9 +208,14 @@ export function renderTraceTable(input: { const spanSource = input.result.grep === null || input.result.grep === undefined ? [...asArray(input.result.errorSpans), ...asArray(input.result.spans)] : asArray(input.result.spans); + const rowLimit = input.options.full + ? Math.max(10, Math.min(input.options.limit, 24)) + : 10; + const allSpans = [...asArray(input.result.errorSpans), ...asArray(input.result.spans)] + .map((span) => asPlainRecord(span) ?? {}); const spanRows = uniqueSpanRecords(spanSource) .sort((left, right) => traceSpanImportanceScore(right) - traceSpanImportanceScore(left)) - .slice(0, 10) + .slice(0, rowLimit) .map((span) => { const attrs = asPlainRecord(span.attributes); const durationMs = traceSpanDurationMs(span, attrs); @@ -225,9 +230,13 @@ export function renderTraceTable(input: { spanColumnAttr(attrs, ["workbench.ui.resource_request_wait_ms"]), spanColumnAttr(attrs, ["workbench.ui.resource_response_transfer_ms"]), shortenEnd(spanColumnAttr(attrs, ["workbench.ui.resource_next_hop_protocol"]), 14), - spanDetail(span, 220), + spanDetail(span, input.options.full ? 150 : 220), ]; }); + const runnerRows = traceRunnerRows(allSpans); + const readRows = traceReadWindowRows(allSpans); + const turnRows = traceTurnStatusRows(allSpans); + const projectionRows = traceProjectionRows(allSpans); const countRows = asArray(input.result.spanNameCounts).slice(0, 8).map((item) => { const row = asPlainRecord(item) ?? {}; return [shortenEnd(textValue(row.name), 64), textValue(row.count)]; @@ -255,6 +264,18 @@ export function renderTraceTable(input: { "Key spans:", formatTable(["NAME", "SERVICE", "ROUTE", "STATUS", "DUR_MS", "RES_MS", "FETCH_REQ", "REQ_WAIT", "RESP_XFER", "PROTO", "DETAIL"], spanRows.length > 0 ? spanRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]), "", + "Runner terminal/errors:", + formatTable(["NAME", "SERVICE", "TERMINAL", "FAILURE", "RETRY", "ROUTE", "HTTP", "MESSAGE"], runnerRows.length > 0 ? runnerRows : [["-", "-", "-", "-", "-", "-", "-", "-"]]), + "", + "Trace read windows:", + formatTable(["TRACE", "SESSION", "SINCE", "LIMIT", "RETURNED", "RANGE", "TOTAL", "MORE", "FULL", "LAST"], readRows.length > 0 ? readRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]), + "", + "Turn status reads:", + formatTable(["TRACE", "SESSION", "TURN", "STATUS", "PHASE", "ROUTE", "HTTP"], turnRows.length > 0 ? turnRows : [["-", "-", "-", "-", "-", "-", "-"]]), + "", + "Projection writes:", + formatTable(["NAME", "TRACE", "SESSION", "TURN", "STATUS", "EVENT", "PROJECTED", "SOURCE", "MESSAGE"], projectionRows.length > 0 ? projectionRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-"]]), + "", "Span name counts:", formatTable(["NAME", "COUNT"], countRows.length > 0 ? countRows : [["-", "-"]]), "", @@ -275,7 +296,7 @@ export function renderTraceTable(input: { lines.push(` ${buildTraceCommand(input.target, input.options, true)}`); } lines.push("", "Disclosure:"); - lines.push(" default view is a bounded table; use --full for structured span summary JSON or --raw for backend response."); + lines.push(" trace output is a bounded table; --full expands row counts and diagnostic tables, --raw returns backend JSON."); return { ok: input.ok, command: "platform-infra observability trace", @@ -284,6 +305,97 @@ export function renderTraceTable(input: { }; } +function traceRunnerRows(spans: Record[]): string[][] { + const rows = spans.filter((span) => { + const name = textValue(span.name); + const attrs = asPlainRecord(span.attributes); + return name.startsWith("runner_terminal") + || name.startsWith("runner_error") + || name === "command_result" + || (name.startsWith("codex_stdio") && spanColumnAttr(attrs, ["failureKind", "terminalStatus"]) !== "-"); + }).map((span) => { + const attrs = asPlainRecord(span.attributes); + return [ + shortenEnd(textValue(span.name), 34), + shortenEnd(textValue(span.service), 20), + shortenEnd(spanColumnAttr(attrs, ["terminalStatus"]), 12), + shortenEnd(spanColumnAttr(attrs, ["failureKind"]), 24), + shortenEnd(spanColumnAttr(attrs, ["willRetry"]), 8), + shortenEnd(spanColumnAttr(attrs, ["http.route"]), 34), + shortenEnd(spanColumnAttr(attrs, ["http.status_code", "http.response.status_code"]), 8), + shortenEnd(spanColumnAttr(attrs, ["errorSummary", "message", "error.message", "exception.message", "statusMessage"]), 80), + ]; + }); + return dedupeRows(rows).slice(0, 12); +} + +function traceReadWindowRows(spans: Record[]): string[][] { + const rows = spans.filter((span) => textValue(span.name) === "trace_events_read").map((span) => { + const attrs = asPlainRecord(span.attributes); + const fromSeq = spanColumnAttr(attrs, ["fromSeq"]); + const toSeq = spanColumnAttr(attrs, ["toSeq"]); + return [ + shortenMiddle(spanColumnAttr(attrs, ["traceId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["sessionId"]), 18), + shortenEnd(spanColumnAttr(attrs, ["sinceSeq", "afterSeq"]), 8), + shortenEnd(spanColumnAttr(attrs, ["limit"]), 8), + shortenEnd(spanColumnAttr(attrs, ["returnedEvents", "rawEventCount"]), 8), + fromSeq === "-" && toSeq === "-" ? "-" : `${fromSeq}..${toSeq}`, + shortenEnd(spanColumnAttr(attrs, ["totalEvents"]), 8), + shortenEnd(spanColumnAttr(attrs, ["hasMore"]), 6), + shortenEnd(spanColumnAttr(attrs, ["fullTraceLoaded"]), 6), + shortenEnd(spanColumnAttr(attrs, ["traceLastSeq", "maxSeq", "endSeq"]), 8), + ]; + }); + return dedupeRows(rows).slice(0, 12); +} + +function traceTurnStatusRows(spans: Record[]): string[][] { + const rows = spans.filter((span) => textValue(span.name) === "turn_status_read").map((span) => { + const attrs = asPlainRecord(span.attributes); + return [ + shortenMiddle(spanColumnAttr(attrs, ["traceId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["sessionId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["turnId"]), 18), + shortenEnd(spanColumnAttr(attrs, ["status", "terminalStatus"]), 14), + shortenEnd(spanColumnAttr(attrs, ["phase"]), 14), + shortenEnd(spanColumnAttr(attrs, ["http.route"]), 34), + shortenEnd(spanColumnAttr(attrs, ["http.status_code", "http.response.status_code"]), 8), + ]; + }); + return dedupeRows(rows).slice(0, 12); +} + +function traceProjectionRows(spans: Record[]): string[][] { + const rows = spans.filter((span) => textValue(span.name).includes("projection")).map((span) => { + const attrs = asPlainRecord(span.attributes); + return [ + shortenEnd(textValue(span.name), 34), + shortenMiddle(spanColumnAttr(attrs, ["traceId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["sessionId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["turnId"]), 18), + shortenEnd(spanColumnAttr(attrs, ["status", "terminalStatus", "phase"]), 14), + shortenEnd(spanColumnAttr(attrs, ["eventType", "type"]), 18), + shortenEnd(spanColumnAttr(attrs, ["projectedSeq"]), 10), + shortenEnd(spanColumnAttr(attrs, ["sourceSeq", "sourceEventId"]), 18), + shortenMiddle(spanColumnAttr(attrs, ["messageId", "failureKind", "errorSummary"]), 24), + ]; + }); + return dedupeRows(rows).slice(0, 12); +} + +function dedupeRows(rows: string[][]): string[][] { + const output: string[][] = []; + const seen = new Set(); + for (const row of rows) { + const key = JSON.stringify(row); + if (seen.has(key)) continue; + seen.add(key); + output.push(row); + } + return output; +} + export function renderSearchTable(input: { ok: boolean; target: ObservabilityTarget;