feat: add yaml egress proxy benchmark
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import { runCommand } from "./command";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { egressBenchmarkCompactResult, egressBenchmarkDryRun, egressBenchmarkStartScript, egressBenchmarkStatusScript, type EgressBenchmarkSpec } from "./egress-proxy-benchmark";
|
||||
import { readSub2ApiConfig } from "./platform-infra/config";
|
||||
import { resolveTarget } from "./platform-infra/manifest";
|
||||
|
||||
type BenchmarkAction = "benchmark" | "benchmark-status" | "benchmark-logs";
|
||||
|
||||
interface BenchmarkOptions {
|
||||
action: BenchmarkAction;
|
||||
targetId: string;
|
||||
profile: "no-mirror";
|
||||
confirm: boolean;
|
||||
dryRun: boolean;
|
||||
samples: number;
|
||||
sampleTimeoutSeconds: number;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export async function runPlatformInfraEgressProxyCommand(_config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const options = parseBenchmarkOptions(args);
|
||||
const spec = platformBenchmarkSpec(options);
|
||||
if (options.action === "benchmark-status" || options.action === "benchmark-logs") return benchmarkStatus(spec, options);
|
||||
if (options.dryRun) {
|
||||
return renderBenchmark({
|
||||
ok: true,
|
||||
command: "platform-infra egress-proxy benchmark",
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
plan: egressBenchmarkDryRun(spec),
|
||||
next: { confirm: `bun scripts/cli.ts platform-infra egress-proxy benchmark --target ${options.targetId} --profile ${options.profile} --confirm` },
|
||||
});
|
||||
}
|
||||
const result = runTrans(spec.route, egressBenchmarkStartScript(spec), options.timeoutSeconds);
|
||||
const parsed = parseJson(result.stdout);
|
||||
return renderBenchmark({
|
||||
ok: result.exitCode === 0,
|
||||
command: "platform-infra egress-proxy benchmark",
|
||||
mode: "async-job",
|
||||
mutation: result.exitCode === 0,
|
||||
target: options.targetId,
|
||||
profile: options.profile,
|
||||
start: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) },
|
||||
result: egressBenchmarkCompactResult(result),
|
||||
});
|
||||
}
|
||||
|
||||
function benchmarkStatus(spec: EgressBenchmarkSpec, options: BenchmarkOptions): RenderedCliResult {
|
||||
const result = runTrans(spec.route, egressBenchmarkStatusScript(spec, options.tailLines), options.timeoutSeconds);
|
||||
const parsed = parseJson(result.stdout);
|
||||
const status = record(record(parsed).status);
|
||||
const state = text(status.state, "unknown");
|
||||
return renderBenchmark({
|
||||
ok: result.exitCode === 0 && (options.action === "benchmark-logs" || state !== "failed"),
|
||||
command: `platform-infra egress-proxy ${options.action}`,
|
||||
mode: "status",
|
||||
mutation: false,
|
||||
target: options.targetId,
|
||||
profile: options.profile,
|
||||
job: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) },
|
||||
result: egressBenchmarkCompactResult(result),
|
||||
});
|
||||
}
|
||||
|
||||
function platformBenchmarkSpec(options: BenchmarkOptions): EgressBenchmarkSpec {
|
||||
const sub2api = readSub2ApiConfig();
|
||||
const target = resolveTarget(sub2api, options.targetId);
|
||||
const proxy = target.egressProxy;
|
||||
if (proxy === null || !proxy.enabled) throw new Error(`target ${target.id} has no enabled egressProxy`);
|
||||
return {
|
||||
scope: "platform-infra",
|
||||
targetId: target.id,
|
||||
route: target.route,
|
||||
namespace: target.namespace,
|
||||
serviceName: proxy.serviceName,
|
||||
port: proxy.listenPort,
|
||||
noProxy: proxy.noProxy,
|
||||
sourceType: proxy.sourceType,
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourceConfigRef: proxy.sourceConfigRef,
|
||||
sourceFingerprint: proxy.sourceFingerprint,
|
||||
profile: options.profile,
|
||||
samples: options.samples,
|
||||
sampleTimeoutSeconds: options.sampleTimeoutSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
function parseBenchmarkOptions(args: string[]): BenchmarkOptions {
|
||||
const actionRaw = args[0] ?? "benchmark";
|
||||
if (actionRaw !== "benchmark" && actionRaw !== "benchmark-status" && actionRaw !== "benchmark-logs") {
|
||||
throw new Error("platform-infra egress-proxy usage: benchmark|benchmark-status|benchmark-logs --target D601|D518 --profile no-mirror [--dry-run|--confirm]");
|
||||
}
|
||||
const action = actionRaw;
|
||||
const rest = args.slice(1);
|
||||
const targetId = option(rest, "--target") ?? "D601";
|
||||
const profileRaw = option(rest, "--profile") ?? "no-mirror";
|
||||
if (profileRaw !== "no-mirror") throw new Error("--profile currently supports no-mirror");
|
||||
const confirm = rest.includes("--confirm");
|
||||
const dryRun = rest.includes("--dry-run") || !confirm;
|
||||
if (confirm && rest.includes("--dry-run")) throw new Error("benchmark accepts only one of --confirm or --dry-run");
|
||||
return {
|
||||
action,
|
||||
targetId,
|
||||
profile: profileRaw,
|
||||
confirm,
|
||||
dryRun: action === "benchmark" ? dryRun : false,
|
||||
samples: positiveIntOption(rest, "--samples", 5, 20),
|
||||
sampleTimeoutSeconds: positiveIntOption(rest, "--sample-timeout-seconds", 240, 900),
|
||||
timeoutSeconds: positiveIntOption(rest, "--timeout-seconds", 30, 60),
|
||||
tailLines: positiveIntOption(rest, "--tail", 40, 200),
|
||||
};
|
||||
}
|
||||
|
||||
function runTrans(route: string, script: string, timeoutSeconds: number) {
|
||||
return runCommand(["/root/.local/bin/trans", route, "sh", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function option(args: string[], name: string): string | null {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveIntOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
const raw = option(args, name);
|
||||
if (raw === null) return defaultValue;
|
||||
const value = Number.parseInt(raw, 10);
|
||||
if (!Number.isInteger(value) || value < 1 || value > maxValue) throw new Error(`${name} must be an integer from 1 to ${maxValue}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseJson(text: string): unknown {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length === 0) return null;
|
||||
try { return JSON.parse(trimmed); } catch {
|
||||
const start = trimmed.indexOf("{");
|
||||
const end = trimmed.lastIndexOf("}");
|
||||
if (start >= 0 && end > start) {
|
||||
try { return JSON.parse(trimmed.slice(start, end + 1)); } catch {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderBenchmark(result: Record<string, unknown>): RenderedCliResult {
|
||||
const start = record(result.start);
|
||||
const job = record(result.job);
|
||||
const status = record(job.status);
|
||||
const plan = record(result.plan);
|
||||
const next = record(result.next);
|
||||
const rows = Array.isArray(status.rows) ? status.rows.map(record) : [];
|
||||
const target = text(result.target ?? plan.target);
|
||||
const profile = text(result.profile ?? plan.profile, "no-mirror");
|
||||
const statusText = text(status.state, result.ok === false ? "failed" : "ok");
|
||||
const logTail = typeof job.logTail === "string" ? job.logTail.trimEnd() : "";
|
||||
const lines = [
|
||||
`platform-infra egress-proxy ${result.mode === "dry-run" ? "benchmark dry-run" : "benchmark"}`,
|
||||
"",
|
||||
...table(["TARGET", "PROFILE", "MODE", "STATUS"], [[target, profile, text(result.mode), statusText]]),
|
||||
"",
|
||||
rows.length === 0 ? "RESULTS\n-" : [
|
||||
"RESULTS",
|
||||
...table(["TEST", "SUCCESS", "SAMPLES", "P50", "P95", "FAILURES"], rows.map((row) => [
|
||||
text(row.test),
|
||||
text(row.success),
|
||||
text(row.samples),
|
||||
text(row.p50Ms),
|
||||
text(row.p95Ms),
|
||||
JSON.stringify(row.failureFamilies ?? {}),
|
||||
])),
|
||||
].join("\n"),
|
||||
...(logTail.length === 0 ? [] : ["", "LOG TAIL", logTail]),
|
||||
"",
|
||||
"NEXT",
|
||||
` ${text(next.confirm, "")}`,
|
||||
` ${text(start.statusCommand, `bun scripts/cli.ts platform-infra egress-proxy benchmark-status --target ${target} --profile ${profile}`)}`,
|
||||
` ${text(start.logsCommand, `bun scripts/cli.ts platform-infra egress-proxy benchmark-logs --target ${target} --profile ${profile}`)}`,
|
||||
"",
|
||||
"Disclosure: default output is a bounded benchmark table; Secret/proxy source values are redacted.",
|
||||
].filter((line) => line !== " ");
|
||||
return { ok: result.ok !== false, command: text(result.command, "platform-infra egress-proxy benchmark"), renderedText: lines.join("\n"), contentType: "text/plain" };
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: string[][]): string[] {
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
||||
const render = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
|
||||
return [render(headers), ...rows.map(render)];
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function text(value: unknown, fallback = "-"): string {
|
||||
if (value === undefined || value === null || value === "") return fallback;
|
||||
return String(value);
|
||||
}
|
||||
Reference in New Issue
Block a user