Files
pikasTech-unidesk/scripts/src/platform-infra-observability/manifest.ts
T
2026-06-26 22:15:12 +00:00

312 lines
13 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. manifest module for scripts/src/platform-infra-observability.ts.
// Moved mechanically from scripts/src/platform-infra-observability.ts:983-1299 for #903.
// SPEC: PJ2026-01060501 OTel追踪 draft-2026-06-19-p0.
// Responsibility: YAML-first platform-infra OpenTelemetry tracing control commands.
import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import {
compactCapture,
createYamlFieldReader,
numberField,
parseJsonOutput,
redactSensitiveUnknown,
shQuote,
capture,
} from "../platform-infra-ops-library";
import type { DiagnoseCodeAgentOptions, ObservabilityTarget, SearchOptions, TraceOptions } from "./types";
import { asPlainRecord, compactSearchTrace } from "./apply-status-scripts";
import { trace } from "./render";
import { asArray } from "./trace-script";
export function traceSpanDurationMs(span: Record<string, unknown>, attrs: Record<string, unknown> | null): number {
const topLevel = numberFromUnknown(span.durationMs);
if (Number.isFinite(topLevel)) return topLevel;
if (attrs === null) return Number.NaN;
for (const key of ["workbench.ui.value_ms", "workbench.ui.resource_duration_ms", "durationMs"]) {
const value = numberFromUnknown(attrs[key]);
if (Number.isFinite(value)) return value;
}
return Number.NaN;
}
export function traceSpanImportanceScore(span: Record<string, unknown>): number {
const attrs = asPlainRecord(span.attributes);
const statusCode = textValue(span.statusCode);
const name = textValue(span.name).toLowerCase();
const hasFailure = spanColumnAttr(attrs, ["failureKind", "errorSummary", "error.message", "exception.message"]) !== "-";
const terminalStatus = spanColumnAttr(attrs, ["terminalStatus"]);
const problemScore = statusCode === "STATUS_CODE_ERROR" || statusCode === "2" || name.includes("error") || hasFailure || terminalStatus === "failed" || terminalStatus === "blocked" ? 1_000_000_000 : 0;
const durationMs = traceSpanDurationMs(span, attrs);
const durationScore = Number.isFinite(durationMs) ? durationMs : 0;
return problemScore + durationScore;
}
export function searchTraceStatus(trace: Record<string, unknown>): string {
if (trace.queryOk === false) return "query-bad";
if (trace.parseOk === false) return "parse-bad";
const errorCount = numberFromUnknown(trace.errorSpanCount);
if (errorCount > 0) return "error";
const matchedCount = numberFromUnknown(trace.matchedSpanCount);
if (matchedCount > 0 || trace.rawMatched === true) return "matched";
return "ok";
}
export function uniqueSpanRecords(values: unknown[]): Record<string, unknown>[] {
const rows: Record<string, unknown>[] = [];
const seen = new Set<string>();
for (const value of values) {
const span = asPlainRecord(value) ?? {};
const key = `${textValue(span.name)}\n${textValue(span.service)}\n${spanDetail(span, 200)}`;
if (seen.has(key)) continue;
seen.add(key);
rows.push(span);
}
return rows;
}
export function spanColumnAttr(attrs: Record<string, unknown> | null, keys: string[]): string {
if (attrs === null) return "-";
for (const key of keys) {
const value = attrs[key];
const text = textValue(value);
if (text !== "-") return text;
}
return "-";
}
export function spanDetail(span: Record<string, unknown>, maxLength: number): string {
const attrs = asPlainRecord(span.attributes);
const parts = [
valuePart("durationMs", span.durationMs),
attrPart(attrs, "failureKind"),
attrPart(attrs, "errorSummary"),
attrPart(attrs, "error.message"),
attrPart(attrs, "exception.message"),
attrPart(attrs, "terminalStatus"),
attrPart(attrs, "status"),
attrPart(attrs, "toolName"),
attrPart(attrs, "exitCode"),
attrPart(attrs, "outputSummary"),
attrPart(attrs, "outputBytes"),
attrPart(attrs, "durationMs"),
attrPart(attrs, "cwd"),
attrPart(attrs, "command"),
attrPart(attrs, "waitingFor"),
attrPart(attrs, "lastEventLabel"),
attrPart(attrs, "idleMs"),
attrPart(attrs, "idleSeconds"),
attrPart(attrs, "commandFingerprint"),
attrPart(attrs, "http.route"),
attrPart(attrs, "http.response.status_code"),
attrPart(attrs, "http.response.status_class"),
attrPart(attrs, "workbench.ui.event_type"),
attrPart(attrs, "workbench.ui.scope"),
attrPart(attrs, "workbench.ui.state"),
attrPart(attrs, "workbench.ui.reason"),
attrPart(attrs, "workbench.ui.outcome"),
attrPart(attrs, "workbench.ui.value_ms"),
attrPart(attrs, "workbench.ui.activity_idle_ms"),
attrPart(attrs, "workbench.ui.activity_waiting_for"),
attrPart(attrs, "workbench.ui.activity_last_event_label"),
attrPart(attrs, "workbench.ui.resource_duration_ms"),
attrPart(attrs, "workbench.ui.resource_fetch_to_request_ms"),
attrPart(attrs, "workbench.ui.resource_request_wait_ms"),
attrPart(attrs, "workbench.ui.resource_response_transfer_ms"),
attrPart(attrs, "workbench.ui.resource_next_hop_protocol"),
attrPart(attrs, "workbench.ui.resource_server_timing"),
attrPart(attrs, "hwlab.http.stage"),
attrPart(attrs, "hwlab.http.phase"),
attrPart(attrs, "hwlab.http.phase.outcome"),
attrPart(attrs, "hwlab.live_builds.service_id"),
attrPart(attrs, "hwlab.live_builds.service_kind"),
attrPart(attrs, "hwlab.live_builds.external"),
attrPart(attrs, "hwlab.live_builds.deploy_manifest_status"),
attrPart(attrs, "hwlab.live_builds.artifact_catalog_status"),
attrPart(attrs, "db.system"),
attrPart(attrs, "db.operation.name"),
attrPart(attrs, "db.sql.table"),
attrPart(attrs, "db.query.arg_count"),
attrPart(attrs, "db.index.expected"),
attrPart(attrs, "db.pool.max_open"),
attrPart(attrs, "db.pool.open_connections"),
attrPart(attrs, "db.pool.in_use"),
attrPart(attrs, "db.pool.idle"),
attrPart(attrs, "db.pool.wait_count"),
attrPart(attrs, "db.pool.wait_duration_ms"),
attrPart(attrs, "error.code"),
attrPart(attrs, "error.category"),
attrPart(attrs, "error.layer"),
].filter((item) => item !== null) as string[];
if (parts.length === 0) return "-";
return shortenEnd(parts.join(" "), maxLength);
}
export function valuePart(key: string, value: unknown): string | null {
if (value === null || value === undefined || value === "") return null;
return `${key}=${textValue(value)}`;
}
export function attrPart(attrs: Record<string, unknown> | null, key: string): string | null {
if (attrs === null) return null;
const value = attrs[key];
if (value === null || value === undefined || value === "") return null;
return `${key}=${textValue(value)}`;
}
export function httpTableRows(http: Record<string, unknown> | null): string[][] {
if (http === null) return [];
const problemRows = asArray(http.problemCounts).map((item) => {
const row = asPlainRecord(item) ?? {};
return [
textValue(row.method),
shortenEnd(textValue(row.route ?? row.path), 44),
textValue(row.status ?? row.statusCode),
textValue(row.count),
];
});
const statusRows = asArray(http.statusCounts).map((item) => {
const row = asPlainRecord(item) ?? {};
return [
textValue(row.method),
shortenEnd(textValue(row.route ?? row.path), 44),
textValue(row.status ?? row.statusCode),
textValue(row.count),
];
});
return [...problemRows, ...statusRows].slice(0, 8);
}
export function buildTraceCommand(target: ObservabilityTarget, options: TraceOptions, full: boolean): string {
const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "trace", "--target", target.id];
if (options.traceId !== null) parts.push("--trace-id", options.traceId);
if (options.grep !== null) parts.push("--grep", options.grep);
parts.push("--limit", String(options.limit));
if (full) parts.push("--full");
return parts.map(cliArg).join(" ");
}
export function buildSearchCommand(target: ObservabilityTarget, options: SearchOptions, full: boolean): string {
const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "search", "--target", target.id];
if (options.grep !== null) parts.push("--grep", options.grep);
if (options.query !== null) parts.push("--query", options.query);
if (options.path !== null) parts.push("--path", options.path);
if (options.status !== null) parts.push("--status", String(options.status));
if (options.endAt !== null) parts.push("--end-at", options.endAt);
parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
if (full) parts.push("--full");
return parts.map(cliArg).join(" ");
}
export function buildDiagnoseCommand(target: ObservabilityTarget, options: DiagnoseCodeAgentOptions, full: boolean): string {
const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "diagnose-code-agent", "--target", target.id];
if (options.businessTraceId !== null) parts.push("--business-trace-id", options.businessTraceId);
if (options.traceId !== null) parts.push("--trace-id", options.traceId);
if (options.runId !== null) parts.push("--run-id", options.runId);
if (options.commandId !== null) parts.push("--command-id", options.commandId);
if (options.runnerJobId !== null) parts.push("--runner-job-id", options.runnerJobId);
parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
if (full) parts.push("--full");
return parts.map(cliArg).join(" ");
}
export function cliArg(value: string): string {
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : shQuote(value);
}
export function formatTable(headers: string[], rows: string[][]): string {
const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? ""));
const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length)));
return [
headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(),
...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()),
].join("\n");
}
export function textValue(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return JSON.stringify(value);
}
export function numberValue(value: unknown): string {
const number = numberFromUnknown(value);
return Number.isFinite(number) ? String(number) : "-";
}
export function numberFromUnknown(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return Number.NaN;
}
export function isoFromUnixNano(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
try {
const text = String(value);
if (!/^\d+$/u.test(text)) return "-";
const millis = Number(BigInt(text) / 1000000n);
if (!Number.isFinite(millis) || millis <= 0) return "-";
return new Date(millis).toISOString().replace(/\.000Z$/u, "Z");
} catch {
return "-";
}
}
export function joinValues(value: unknown, maxLength: number): string {
const joined = asArray(value).map(textValue).filter((item) => item !== "-").join(",");
return shortenEnd(joined.length > 0 ? joined : textValue(value), maxLength);
}
export function shortenEnd(value: string, maxLength: number): string {
if (value.length <= maxLength) return value;
if (maxLength <= 1) return value.slice(0, maxLength);
return `${value.slice(0, maxLength - 1)}~`;
}
export function shortenMiddle(value: string, maxLength: number): string {
if (value.length <= maxLength) return value;
if (maxLength <= 3) return value.slice(0, maxLength);
const left = Math.ceil((maxLength - 1) / 2);
const right = Math.floor((maxLength - 1) / 2);
return `${value.slice(0, left)}~${value.slice(value.length - right)}`;
}
export function compactSearchResult(value: unknown): Record<string, unknown> {
const source = asPlainRecord(value) ?? {};
const traces = asArray(source.traces).map(compactSearchTrace);
return {
ok: source.ok === true,
searchPath: source.searchPath ?? null,
grep: source.grep ?? null,
tempoQuery: source.tempoQuery ?? null,
pathFilter: source.pathFilter ?? null,
statusFilter: source.statusFilter ?? null,
limit: source.limit ?? null,
candidateLimit: source.candidateLimit ?? null,
searchParseOk: source.searchParseOk ?? null,
candidateTraceCount: source.candidateTraceCount ?? null,
scannedTraceCount: source.scannedTraceCount ?? null,
matchedTraceCount: source.matchedTraceCount ?? null,
scanStopped: source.scanStopped ?? null,
matchingActive: source.matchingActive ?? null,
traces,
truncated: source.truncated ?? null,
next: source.next ?? null,
searchStderrTail: source.searchStderrTail ?? null,
disclosure: {
defaultView: "compact trace rows only; span arrays are omitted to keep stdout below config/unidesk-cli.yaml output.maxStdoutBytes",
expand: "rerun with --full or query a specific trace id with platform-infra observability trace",
},
};
}