// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. summary module for scripts/src/platform-infra-observability.ts. // SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/platform-infra-observability.ts:3031-4400 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 { ImageSpec, ObservabilityConfig, ObservabilityTarget } from "./types"; import { status } from "./actions"; import { asRecord, configLabel } from "./types"; export function configSummary(observability: ObservabilityConfig, target: ObservabilityTarget): Record { return { configPath: configLabel, spec: observability.metadata.spec, target: targetSummary(target), images: { collector: imageReference(observability.images.collector), traceBackend: imageReference(observability.images.tempo), }, collector: observability.collector, traceBackend: observability.traceBackend, sampling: observability.sampling, serviceConnections: observability.instrumentation.serviceConnections.map((item) => ({ serviceName: item.serviceName, owningRepo: item.owningRepo, resolved: { targetNode: item.targetNode, lane: item.lane, namespace: item.namespace, }, configRefs: item.configRefGraph, requiredSpans: item.requiredSpans, })), }; } export function targetSummary(target: ObservabilityTarget): Record { return { id: target.id, route: target.route, namespace: target.namespace, role: target.role, createNamespace: target.createNamespace, }; } export function policyChecks(yaml: string, target: ObservabilityTarget): Array> { return [ { name: "yaml-source-of-truth", ok: true, detail: "All concrete images, routes, namespace, ports, retention and sampling values are read from config/platform-infra/observability.yaml." }, { name: "clusterip-only", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Collector and trace backend stay ClusterIP-only." }, { name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: "No public ingress is rendered for the first tracing backend." }, { name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use host network." }, { name: "allow-all-network-policy", ok: yaml.includes("kind: NetworkPolicy") && yaml.includes("name: allow-all") && yaml.includes(`namespace: ${target.namespace}`), detail: `NetworkPolicy/allow-all is rendered in ${target.namespace}.` }, ]; } export function statusSummary(payload: Record): Record { const sections = asRecord(payload.sections, "status.sections"); const deployments = objectList(sectionJson(sections, "deployments")); const services = objectList(sectionJson(sections, "services")); const pods = objectList(sectionJson(sections, "pods")); const events = objectList(sectionJson(sections, "events")); const podEvents = latestPodEvents(events); const probes = Array.isArray(payload.probes) ? payload.probes as Array> : []; const readyDeployments = deployments.map((item) => deploymentSummary(item)); return { ready: payload.ok === true, namespace: payload.namespace, deployments: readyDeployments, services: services.map((item) => metadataName(item)), pods: pods.map((item) => podSummary(item, podEvents)), probes: probes.map((item) => ({ name: item.name, ok: item.ok === true, service: item.service, path: item.path, stderrTail: item.ok === true ? "" : item.stderrTail, })), }; } export function compactStatus(payload: Record | null, full: boolean): Record | null { if (payload === null) return null; if (full) return redactSensitiveUnknown(payload) as Record; return statusSummary(payload); } export function sectionJson(sections: Record, name: string): unknown { const section = asRecord(sections[name], `sections.${name}`); return section.json; } export function objectList(value: unknown): Record[] { if (typeof value !== "object" || value === null) return []; const items = (value as Record).items; if (!Array.isArray(items)) return []; return items.filter((item): item is Record => typeof item === "object" && item !== null && !Array.isArray(item)); } export function deploymentSummary(item: Record): Record { const spec = item.spec as Record | undefined; const status = item.status as Record | undefined; const replicas = typeof spec?.replicas === "number" ? spec.replicas : null; const available = typeof status?.availableReplicas === "number" ? status.availableReplicas : 0; return { name: metadataName(item), replicas, availableReplicas: available, ready: replicas !== null && available >= replicas, }; } export function podSummary(item: Record, podEvents: Map>): Record { const status = item.status as Record | undefined; const waiting = firstWaitingContainer(status); const scheduled = conditionSummary(status, "PodScheduled"); const ready = conditionSummary(status, "Ready"); const name = metadataName(item); const showEvent = status?.phase !== "Running" || waiting !== null || scheduled !== null || ready !== null; const latestEvent = showEvent && name !== null ? podEvents.get(name) ?? null : null; return { name, phase: status?.phase ?? null, reason: waiting?.reason ?? scheduled?.reason ?? ready?.reason ?? null, message: waiting?.message ?? scheduled?.message ?? ready?.message ?? null, latestEvent: latestEvent === null ? null : { reason: stringValue(latestEvent.reason), message: stringValue(latestEvent.message), type: stringValue(latestEvent.type), count: typeof latestEvent.count === "number" ? latestEvent.count : null, timestamp: eventTimestamp(latestEvent), }, }; } export function latestPodEvents(events: Record[]): Map> { const byPod = new Map>(); for (const item of events) { const involved = item.involvedObject; if (typeof involved !== "object" || involved === null || Array.isArray(involved)) continue; const podName = (involved as Record).name; const kind = (involved as Record).kind; if (kind !== "Pod" || typeof podName !== "string") continue; const current = byPod.get(podName); if (current === undefined || eventTimestamp(item) > eventTimestamp(current)) byPod.set(podName, item); } return byPod; } export function eventTimestamp(item: Record): string | null { const candidate = stringValue(item.eventTime) ?? stringValue(item.lastTimestamp) ?? stringValue(item.deprecatedLastTimestamp) ?? (typeof item.metadata === "object" && item.metadata !== null && !Array.isArray(item.metadata) ? stringValue((item.metadata as Record).creationTimestamp) : null); return candidate; } export function stringValue(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } export function firstWaitingContainer(status: Record | undefined): { reason: string | null; message: string | null } | null { const statuses = Array.isArray(status?.containerStatuses) ? status.containerStatuses : []; for (const item of statuses) { if (typeof item !== "object" || item === null || Array.isArray(item)) continue; const state = (item as Record).state; if (typeof state !== "object" || state === null || Array.isArray(state)) continue; const waiting = (state as Record).waiting; if (typeof waiting !== "object" || waiting === null || Array.isArray(waiting)) continue; const record = waiting as Record; return { reason: typeof record.reason === "string" ? record.reason : null, message: typeof record.message === "string" ? record.message : null, }; } return null; } export function conditionSummary(status: Record | undefined, type: string): { reason: string | null; message: string | null } | null { const conditions = Array.isArray(status?.conditions) ? status.conditions : []; for (const item of conditions) { if (typeof item !== "object" || item === null || Array.isArray(item)) continue; const record = item as Record; if (record.type !== type || record.status === "True") continue; return { reason: typeof record.reason === "string" ? record.reason : null, message: typeof record.message === "string" ? record.message : null, }; } return null; } export function metadataName(item: Record): string | null { const metadata = item.metadata as Record | undefined; return typeof metadata?.name === "string" ? metadata.name : null; } export function manifestObjectSummary(yaml: string): Array> { const docs = yaml.split(/^---$/mu); return docs.map((doc) => { const kind = doc.match(/^\s*kind:\s*(.+)$/mu)?.[1]?.trim() ?? "unknown"; const name = doc.match(/^\s*name:\s*(.+)$/mu)?.[1]?.trim() ?? "unknown"; return { kind, name }; }); } export function imageReference(image: ImageSpec): string { return `${image.repository}:${image.tag}`; } export function indent(value: string, spaces: number): string { const prefix = " ".repeat(spaces); return value.split("\n").map((line) => `${prefix}${line}`).join("\n"); }