Files
pikasTech-unidesk/scripts/src/platform-infra-observability/plan-render.ts
T
2026-07-11 22:25:56 +02:00

147 lines
6.2 KiB
TypeScript

import type { RenderedCliResult } from "../output";
import { formatTable, shortenEnd, textValue } from "./manifest";
import { configLabel } from "./types";
export function renderObservabilityPlan(result: Record<string, unknown>): RenderedCliResult {
const config = record(result.config);
const target = record(config.target);
const images = record(config.images);
const collector = record(config.collector);
const traceBackend = record(config.traceBackend);
const renderPlan = record(result.renderPlan);
const otlp = record(renderPlan.otlp);
const connections = records(config.serviceConnections);
const objects = records(renderPlan.objects);
const policy = records(result.policy);
const configRefs = connections.flatMap((connection) => records(connection.configRefs));
const presentConfigRefs = configRefs.filter((item) => item.present === true);
const missingConfigRefs = configRefs.filter((item) => item.present !== true);
const failedPolicy = policy.filter((item) => item.ok === false);
const serviceNames = new Set(connections.map((item) => textValue(item.serviceName)).filter((item) => item !== "-"));
const nodes = new Set(connections.map((item) => textValue(record(item.resolved).targetNode)).filter((item) => item !== "-"));
const connectionRows = connections.map((item) => {
const resolved = record(item.resolved);
const refs = records(item.configRefs);
const present = refs.filter((ref) => ref.present === true).length;
return [
shortenEnd(textValue(item.serviceName), 28),
textValue(resolved.targetNode),
textValue(resolved.lane),
textValue(resolved.namespace),
refs.length === 0 ? "inline" : `${present}/${refs.length}`,
String(values(item.requiredSpans).length),
];
});
const objectRows = objects.map((item) => [textValue(item.kind), textValue(item.name)]);
const lines = [
`platform-infra observability plan (${result.ok === false ? "not-ok" : "ok"})`,
"",
`target=${textValue(target.id)} route=${textValue(target.route)} namespace=${textValue(target.namespace)} role=${textValue(target.role)}`,
`collector=${textValue(collector.serviceName)} replicas=${textValue(collector.replicas)} image=${textValue(images.collector)}`,
`tempo=${textValue(traceBackend.serviceName)} replicas=${textValue(traceBackend.replicas)} retention=${textValue(record(traceBackend.storage).retention)} image=${textValue(images.traceBackend)}`,
`serviceConnections=${connections.length} services=${serviceNames.size} nodes=${nodes.size} configRefs=${presentConfigRefs.length}/${configRefs.length} missing=${missingConfigRefs.length} objects=${objects.length}`,
"",
"Service connections:",
formatTable(
["SERVICE", "NODE", "LANE", "NAMESPACE", "CONFIG_REFS", "SPANS"],
connectionRows.length > 0 ? connectionRows : [["-", "-", "-", "-", "-", "-"]],
),
"",
"Render objects:",
formatTable(["KIND", "NAME"], objectRows.length > 0 ? objectRows : [["-", "-"]]),
];
if (missingConfigRefs.length > 0) {
lines.push(
"",
"Missing configRefs:",
formatTable(
["ID", "FILE", "PATH"],
missingConfigRefs.map((item) => [
shortenEnd(textValue(item.id), 48),
shortenEnd(textValue(item.file), 48),
shortenEnd(textValue(item.path), 64),
]),
),
);
}
if (failedPolicy.length > 0) {
lines.push(
"",
"Failed policy:",
formatTable(["NAME", "DETAIL"], failedPolicy.map((item) => [textValue(item.name), shortenEnd(textValue(item.detail), 100)])),
);
}
lines.push(
"",
"Endpoints:",
` collector-grpc: ${textValue(otlp.collectorGrpcEndpoint)}`,
` collector-http: ${textValue(otlp.collectorHttpEndpoint)}`,
` tempo-grpc: ${textValue(otlp.backendGrpcEndpoint)}`,
"",
"Next:",
` bun scripts/cli.ts platform-infra observability plan --target ${textValue(target.id)} --full`,
` bun scripts/cli.ts platform-infra observability plan --target ${textValue(target.id)} --raw`,
` bun scripts/cli.ts platform-infra observability status --target ${textValue(target.id)}`,
"",
"Disclosure:",
" 默认视图只展示计划摘要;--full/--raw 显式披露完整 render plan。",
);
return {
ok: result.ok !== false,
command: "platform-infra observability plan",
contentType: "text/plain",
renderedText: lines.join("\n"),
};
}
export function renderObservabilityPlanFailure(error: unknown, targetId: string | null): RenderedCliResult {
const message = error instanceof Error ? error.message : String(error);
const configRef = extractConfigRef(message);
const sourcePath = configRef?.split("#")[0] ?? configLabel;
const inspectPattern = configRef?.split("#")[1]?.split(/[.[]/u)[0] ?? "instrumentation";
const target = targetId ?? "YAML-default";
const targetArg = targetId === null ? "" : ` --target ${targetId}`;
const lines = [
"platform-infra observability plan (not-ok)",
"",
`target=${target}`,
`config=${configLabel}`,
`configRef=${configRef ?? "unresolved"}`,
`reason=${shortenEnd(message.replace(/\s+/gu, " ").trim(), 500)}`,
"",
"Next:",
` rg -n ${quoteArg(inspectPattern)} ${quoteArg(sourcePath)}`,
` bun scripts/cli.ts platform-infra observability plan${targetArg} --full`,
"",
"Disclosure:",
" 配置或 configRef 失败会直接显示来源与检查入口,不依赖 stdout dump。",
];
return {
ok: false,
command: "platform-infra observability plan",
contentType: "text/plain",
renderedText: lines.join("\n"),
};
}
function extractConfigRef(message: string): string | null {
const match = message.match(/config\/[A-Za-z0-9_./-]+[.]yaml(?:#[A-Za-z0-9_.\-[\]]+)?/u);
return match?.[0] ?? null;
}
function quoteArg(value: string): string {
return /^[A-Za-z0-9_./:[\]-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function records(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.map(record) : [];
}
function values(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}