fix: bound observability trace output

This commit is contained in:
Codex
2026-06-26 20:44:49 +00:00
parent 618aa5240e
commit 399dcc289f
2 changed files with 117 additions and 5 deletions
@@ -26,7 +26,7 @@ import { diagnoseCodeAgent, search, trace } from "./render";
export function observabilityHelp(): Record<string, unknown> {
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: [
@@ -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, unknown>[]): 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, unknown>[]): 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, unknown>[]): 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, unknown>[]): 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<string>();
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;