// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/platform-infra-observability.ts. // Moved mechanically from scripts/src/platform-infra-observability.ts:150-369 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 { ApplyOptions, CommonOptions, DiagnoseCodeAgentOptions, MetricsQueryOptions, SearchOptions, TraceOptions } from "./types"; import { apply, plan, status, validate } from "./actions"; import { diagnoseCodeAgent, search, trace } from "./render"; import { renderObservabilityPlan, renderObservabilityPlanFailure } from "./plan-render"; import { metricsQuery } from "./prometheus"; const observabilityOperations = ["plan", "apply", "status", "validate", "trace", "search", "diagnose-code-agent", "metrics-query"] as const; export function observabilityHelp(scope: string | null = null): Record { const scoped = scope === null ? null : observabilityOperationHelp(scope); if (scoped !== null) return scoped; return { command: "platform-infra observability plan|apply|status|validate|trace|search|diagnose-code-agent|metrics-query", output: "默认输出为有界摘要;--full/--raw 显式披露完整结构", configTruth: "config/platform-infra/observability.yaml", spec: "PJ2026-01060501 OTel追踪 draft-2026-06-19-p0", operations: observabilityOperations.map((name) => ({ name, help: `bun scripts/cli.ts platform-infra observability ${name} --help`, })), scopedHelp: "bun scripts/cli.ts platform-infra observability --help", boundary: "Collector + Tempo 是 trace authority;Prometheus 仅承担 metrics,故障和漂移保持非阻塞 warning。", }; } export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (args.some(isHelpToken)) return observabilityHelp(args.find((item) => !isHelpToken(item)) ?? null); if (action === "plan") { const options = parseCommonOptions(args.slice(1)); try { const result = plan(options); return options.full || options.raw ? result : renderObservabilityPlan(result); } catch (error) { if (options.full || options.raw) throw error; return renderObservabilityPlanFailure(error, options.targetId); } } if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1))); if (action === "status") return await status(config, parseCommonOptions(args.slice(1))); if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1))); if (action === "trace") return await trace(config, parseTraceOptions(args.slice(1))); if (action === "search") return await search(config, parseSearchOptions(args.slice(1))); if (action === "diagnose-code-agent") return await diagnoseCodeAgent(config, parseDiagnoseCodeAgentOptions(args.slice(1))); if (action === "metrics-query") return await metricsQuery(config, parseMetricsQueryOptions(args.slice(1))); return { ok: false, error: "unsupported-platform-infra-observability-command", args, help: observabilityHelp() }; } function isHelpToken(value: string): boolean { return value === "help" || value === "--help" || value === "-h"; } function observabilityOperationHelp(scope: string): Record | null { const common = { scope, configTruth: "config/platform-infra/observability.yaml", disclosure: "默认有界;使用 --full 查看完整计划或诊断结构,--raw 仅用于后端/API 解析排障。", }; if (scope === "plan") return { ...common, command: "platform-infra observability plan", usage: ["bun scripts/cli.ts platform-infra observability plan --target [--full|--raw]"], options: ["--target ", "--full", "--raw"], }; if (scope === "apply") return { ...common, command: "platform-infra observability apply", usage: [ "bun scripts/cli.ts platform-infra observability apply --target --dry-run", "bun scripts/cli.ts platform-infra observability apply --target --confirm [--wait]", ], options: ["--target ", "--dry-run", "--confirm", "--wait", "--full", "--raw"], }; if (scope === "status" || scope === "validate") return { ...common, command: `platform-infra observability ${scope}`, usage: [`bun scripts/cli.ts platform-infra observability ${scope} --target [--full|--raw]`], options: ["--target ", "--full", "--raw"], }; if (scope === "trace") return { ...common, command: "platform-infra observability trace", usage: [ "bun scripts/cli.ts platform-infra observability trace --target [--grep ] [--limit 40] [--full|--raw]", "bun scripts/cli.ts platform-infra observability trace --trace-id --target --full", ], options: ["--trace-id <32-hex>", "--target ", "--grep ", "--limit <1..500>", "--full", "--raw"], }; if (scope === "metrics-query") return { ...common, command: "platform-infra observability metrics-query", usage: ["bun scripts/cli.ts platform-infra observability metrics-query --target --query [--full|--raw]"], options: ["--target ", "--query ", "--full", "--raw"], }; if (scope === "search") return { ...common, command: "platform-infra observability search", usage: [ "bun scripts/cli.ts platform-infra observability search --target --grep [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]", "bun scripts/cli.ts platform-infra observability search --target --path --status [--full|--raw]", ], options: ["--grep |--query |--path |--status ", "--target ", "--lookback-minutes ", "--end-at ", "--candidate-limit ", "--limit ", "--full", "--raw"], }; if (scope === "diagnose-code-agent") return { ...common, command: "platform-infra observability diagnose-code-agent", usage: [ "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target --business-trace-id [--full|--raw]", "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target --run-id [--command-id ] [--session-id ] [--runner-job-id ]", ], options: ["--business-trace-id |--trace-id <32-hex>|--run-id |--command-id |--runner-job-id ", "--session-id ", "--target ", "--lookback-minutes ", "--candidate-limit ", "--limit ", "--full", "--raw"], }; return null; } export function parseMetricsQueryOptions(args: string[]): MetricsQueryOptions { const commonArgs: string[] = []; let query: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--query") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--query requires a value"); query = value; index += 1; } else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } return { ...parseCommonOptions(commonArgs), query }; } export function parseCommonOptions(args: string[]): CommonOptions { let targetId: string | null = null; let full = false; let raw = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--target must be a simple target id"); targetId = value; index += 1; } else if (arg === "--full") { full = true; } else if (arg === "--raw") { raw = true; full = true; } else { throw new Error(`unsupported observability option: ${arg}`); } } return { targetId, full, raw }; } export function parseApplyOptions(args: string[]): ApplyOptions { const commonArgs: string[] = []; let confirm = false; let dryRun = false; let wait = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--confirm") confirm = true; else if (arg === "--dry-run") dryRun = true; else if (arg === "--wait") wait = true; else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } if (confirm && dryRun) throw new Error("observability apply accepts only one of --confirm or --dry-run"); return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait }; } export function parseTraceOptions(args: string[]): TraceOptions { const commonArgs: string[] = []; let traceId: string | null = null; let grep: string | null = null; let limit = 40; const assignTraceId = (value: string) => { if (!/^[0-9a-fA-F]{32}$/u.test(value)) throw new Error("trace id must be a 32-character OpenTelemetry trace id encoded as hex"); if (traceId !== null) throw new Error("observability trace accepts exactly one trace id, either positionally or through --trace-id"); traceId = value; }; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--trace-id") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--trace-id requires a value"); assignTraceId(value); index += 1; } else if (arg === "--grep") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--grep requires a value"); grep = value; index += 1; } else if (arg === "--limit") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--limit must be an integer from 1 to 500"); limit = parsed; index += 1; } else if (!arg.startsWith("--")) { assignTraceId(arg); } else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } return { ...parseCommonOptions(commonArgs), traceId, grep, limit }; } export function parseSearchOptions(args: string[]): SearchOptions { const commonArgs: string[] = []; let grep: string | null = null; let query: string | null = null; let path: string | null = null; let status: number | null = null; let limit = 20; let candidateLimit = 80; let lookbackMinutes = 360; let endAt: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--grep") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--grep requires a value"); grep = value; index += 1; } else if (arg === "--query" || arg === "--q") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); query = value; index += 1; } else if (arg === "--path") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--path requires a value"); if (!value.startsWith("/") || value.includes("\n") || value.length > 300) throw new Error("--path must be an absolute HTTP path up to 300 characters"); path = value; index += 1; } else if (arg === "--status") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--status requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 100 || parsed > 599) throw new Error("--status must be an integer HTTP status from 100 to 599"); status = parsed; index += 1; } else if (arg === "--limit") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) throw new Error("--limit must be an integer from 1 to 100"); limit = parsed; index += 1; } else if (arg === "--candidate-limit") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--candidate-limit requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--candidate-limit must be an integer from 1 to 500"); candidateLimit = parsed; index += 1; } else if (arg === "--lookback-minutes" || arg === "--since-minutes") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`); lookbackMinutes = parsed; index += 1; } else if (arg === "--end-at") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--end-at requires an ISO timestamp value"); const parsed = Date.parse(value); if (!Number.isFinite(parsed)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse"); endAt = new Date(parsed).toISOString(); index += 1; } else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } if (grep === null && query === null && path === null && status === null) throw new Error("observability search requires --grep , --query , --path , or --status "); return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes, endAt }; } export function parseDiagnoseCodeAgentOptions(args: string[]): DiagnoseCodeAgentOptions { const commonArgs: string[] = []; let businessTraceId: string | null = null; let traceId: string | null = null; let runId: string | null = null; let commandId: string | null = null; let sessionId: string | null = null; let runnerJobId: string | null = null; let limit = 40; let candidateLimit = 300; let lookbackMinutes = 10080; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--business-trace-id" || arg === "--trace") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^trc_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like trc_`); businessTraceId = value; index += 1; } else if (arg === "--trace-id" || arg === "--otel-trace-id") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^[0-9a-fA-F]{32}$/u.test(value)) throw new Error(`${arg} must be a 32-character OpenTelemetry trace id encoded as hex`); traceId = value.toLowerCase(); index += 1; } else if (arg === "--run-id" || arg === "--run") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^run_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like run_`); runId = value; index += 1; } else if (arg === "--command-id" || arg === "--command") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^cmd_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like cmd_`); commandId = value; index += 1; } else if (arg === "--session-id" || arg === "--session") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^ses_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like ses_`); sessionId = value; index += 1; } else if (arg === "--runner-job-id" || arg === "--runner-job" || arg === "--runnerjob") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^rjob_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like rjob_`); runnerJobId = value; index += 1; } else if (arg === "--limit") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 200) throw new Error("--limit must be an integer from 1 to 200"); limit = parsed; index += 1; } else if (arg === "--candidate-limit") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--candidate-limit requires a value"); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--candidate-limit must be an integer from 1 to 500"); candidateLimit = parsed; index += 1; } else if (arg === "--lookback-minutes" || arg === "--since-minutes") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`); lookbackMinutes = parsed; index += 1; } else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } if (businessTraceId === null && traceId === null && runId === null && commandId === null && runnerJobId === null) { throw new Error("observability diagnose-code-agent requires --business-trace-id , --trace-id , --run-id , --command-id , or --runner-job-id "); } return { ...parseCommonOptions(commonArgs), businessTraceId, traceId, runId, commandId, sessionId, runnerJobId, limit, candidateLimit, lookbackMinutes }; }