import type { RenderedCliResult } from "../output"; import { formatTable, shortenEnd, textValue } from "./manifest"; import { configLabel } from "./types"; export function renderObservabilityPlan(result: Record): 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 metricsBackend = record(config.metricsBackend); const renderPlan = record(result.renderPlan); const metricsPlan = record(renderPlan.metrics); 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)}`, `prometheus=${textValue(metricsPlan.disposition)} service=${textValue(metricsBackend.serviceName)} targets=${values(metricsPlan.targetIds).join(",")} replicas=${textValue(metricsBackend.replicas)} retention=${textValue(record(metricsBackend.storage).retention)} image=${textValue(images.metricsBackend)}`, `scrapeSelector=${textValue(record(record(metricsBackend.discovery).labelSelector).key)}=${textValue(record(record(metricsBackend.discovery).labelSelector).value)} namespaces=${values(record(metricsBackend.discovery).namespaces).length === 0 ? "all" : values(record(metricsBackend.discovery).namespaces).join(",")} queryBudget=${textValue(record(metricsBackend.query).timeoutSeconds)}s/${textValue(record(metricsBackend.query).maxSeries)}series/${textValue(record(metricsBackend.query).maxResponseBytes)}bytes`, `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)}`, ` prometheus-http: ${textValue(otlp.prometheusHttpEndpoint)}`, "", "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 { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function records(value: unknown): Record[] { return Array.isArray(value) ? value.map(record) : []; } function values(value: unknown): unknown[] { return Array.isArray(value) ? value : []; }