Files
pikasTech-unidesk/scripts/src/platform-infra-observability/options.ts
T
2026-07-13 20:58:00 +02:00

404 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, unknown> {
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 <operation> --help",
boundary: "Collector + Tempo 是 trace authorityPrometheus 仅承担 metrics,故障和漂移保持非阻塞 warning。",
};
}
export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | 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<string, unknown> | 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 <NODE> [--full|--raw]"],
options: ["--target <NODE>", "--full", "--raw"],
};
if (scope === "apply") return {
...common,
command: "platform-infra observability apply",
usage: [
"bun scripts/cli.ts platform-infra observability apply --target <NODE> --dry-run",
"bun scripts/cli.ts platform-infra observability apply --target <NODE> --confirm [--wait]",
],
options: ["--target <NODE>", "--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 <NODE> [--full|--raw]`],
options: ["--target <NODE>", "--full", "--raw"],
};
if (scope === "trace") return {
...common,
command: "platform-infra observability trace",
usage: [
"bun scripts/cli.ts platform-infra observability trace <otel-trace-id> --target <NODE> [--grep <text>] [--limit 40] [--full|--raw]",
"bun scripts/cli.ts platform-infra observability trace --trace-id <otel-trace-id> --target <NODE> --full",
],
options: ["--trace-id <32-hex>", "--target <NODE>", "--grep <text>", "--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 <NODE> --query <promql> [--full|--raw]"],
options: ["--target <NODE>", "--query <promql>", "--full", "--raw"],
};
if (scope === "search") return {
...common,
command: "platform-infra observability search",
usage: [
"bun scripts/cli.ts platform-infra observability search --target <NODE> --grep <text> [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]",
"bun scripts/cli.ts platform-infra observability search --target <NODE> --path </route> --status <code> [--full|--raw]",
],
options: ["--grep <text>|--query <TraceQL>|--path </route>|--status <code>", "--target <NODE>", "--lookback-minutes <N>", "--end-at <ISO>", "--candidate-limit <N>", "--limit <N>", "--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 <NODE> --business-trace-id <trc_...> [--full|--raw]",
"bun scripts/cli.ts platform-infra observability diagnose-code-agent --target <NODE> --run-id <run_...> [--command-id <cmd_...>] [--session-id <ses_...>] [--runner-job-id <rjob_...>]",
],
options: ["--business-trace-id <trc_...>|--trace-id <32-hex>|--run-id <run_...>|--command-id <cmd_...>|--runner-job-id <rjob_...>", "--session-id <ses_...>", "--target <NODE>", "--lookback-minutes <N>", "--candidate-limit <N>", "--limit <N>", "--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 <text>, --query <tempo-query>, --path <route>, or --status <code>");
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_<id>`);
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_<id>`);
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_<id>`);
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_<id>`);
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_<id>`);
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 <trc_...>, --trace-id <otelTraceId>, --run-id <run_...>, --command-id <cmd_...>, or --runner-job-id <rjob_...>");
}
return { ...parseCommonOptions(commonArgs), businessTraceId, traceId, runId, commandId, sessionId, runnerJobId, limit, candidateLimit, lookbackMinutes };
}