// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config 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:370-514 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, OtlpPorts, ServiceConnection } from "./types"; import { assertKnownEnabledTarget, parseStatusEndpoint } from "./actions"; import { apiPathField, arrayOfRecords, asRecord, booleanField, configFile, configLabel, enumField, integerField, kubernetesNameField, numberArrayField, objectField, portField, stringArrayField, stringField } from "./types"; import { configRefGraph, resolveConfigRefString } from "../ops/config-refs"; export function readObservabilityConfig(): ObservabilityConfig { const parsed = Bun.YAML.parse(readFileSync(configFile, "utf8")) as unknown; const root = asRecord(parsed, configLabel); const version = integerField(root, "version", ""); const kind = stringField(root, "kind", ""); if (kind !== "platform-infra-observability") throw new Error(`${configLabel}.kind must be platform-infra-observability`); const metadata = objectField(root, "metadata", ""); const defaults = objectField(root, "defaults", ""); const images = objectField(root, "images", ""); const collector = objectField(root, "collector", ""); const collectorOtlp = objectField(collector, "otlp", "collector"); const traceBackend = objectField(root, "traceBackend", ""); const traceBackendOtlp = objectField(traceBackend, "otlp", "traceBackend"); const traceBackendStorage = objectField(traceBackend, "storage", "traceBackend"); const sampling = objectField(root, "sampling", ""); const instrumentation = objectField(root, "instrumentation", ""); const resourceAttributes = objectField(root, "resourceAttributes", ""); const probes = objectField(root, "probes", ""); const config: ObservabilityConfig = { version, kind, metadata: { id: stringField(metadata, "id", "metadata"), owner: stringField(metadata, "owner", "metadata"), spec: stringField(metadata, "spec", "metadata"), relatedIssues: numberArrayField(metadata, "relatedIssues", "metadata"), }, defaults: { targetId: stringField(defaults, "targetId", "defaults") }, images: { collector: imageSpec(objectField(images, "collector", "images"), "images.collector"), tempo: imageSpec(objectField(images, "tempo", "images"), "images.tempo"), }, targets: arrayOfRecords(root.targets, "targets").map(parseTarget), collector: { deploymentName: kubernetesNameField(collector, "deploymentName", "collector"), serviceName: kubernetesNameField(collector, "serviceName", "collector"), configMapName: kubernetesNameField(collector, "configMapName", "collector"), replicas: integerField(collector, "replicas", "collector"), healthPort: portField(collector, "healthPort", "collector"), otlp: parseOtlpPorts(collectorOtlp, "collector.otlp"), }, traceBackend: { type: enumField(traceBackend, "type", "traceBackend", ["tempo"] as const), deploymentName: kubernetesNameField(traceBackend, "deploymentName", "traceBackend"), serviceName: kubernetesNameField(traceBackend, "serviceName", "traceBackend"), configMapName: kubernetesNameField(traceBackend, "configMapName", "traceBackend"), replicas: integerField(traceBackend, "replicas", "traceBackend"), httpPort: portField(traceBackend, "httpPort", "traceBackend"), otlp: parseOtlpPorts(traceBackendOtlp, "traceBackend.otlp"), storage: { mode: enumField(traceBackendStorage, "mode", "traceBackend.storage", ["emptyDir"] as const), retention: stringField(traceBackendStorage, "retention", "traceBackend.storage"), }, }, sampling: { mode: enumField(sampling, "mode", "sampling", ["parentbased_traceidratio"] as const), ratio: numberField(sampling, "ratio", "sampling"), }, instrumentation: { contextPropagation: stringArrayField(instrumentation, "contextPropagation", "instrumentation"), serviceConnections: arrayOfRecords(instrumentation.serviceConnections, "instrumentation.serviceConnections").map(parseServiceConnection), }, resourceAttributes: { required: stringArrayField(resourceAttributes, "required", "resourceAttributes"), businessCorrelationAttributes: stringArrayField(resourceAttributes, "businessCorrelationAttributes", "resourceAttributes"), }, probes: { readinessPath: apiPathField(probes, "readinessPath", "probes"), traceQueryPathTemplate: stringField(probes, "traceQueryPathTemplate", "probes"), statusEndpoints: arrayOfRecords(probes.statusEndpoints, "probes.statusEndpoints").map(parseStatusEndpoint), }, }; if (config.targets.length === 0) throw new Error(`${configLabel}.targets must not be empty`); assertKnownEnabledTarget(config.targets, config.defaults.targetId, "defaults.targetId"); if (config.collector.replicas < 0 || config.traceBackend.replicas < 0) throw new Error(`${configLabel} replicas must be >= 0`); if (config.sampling.ratio < 0 || config.sampling.ratio > 1) throw new Error(`${configLabel}.sampling.ratio must be between 0 and 1`); if (!config.probes.traceQueryPathTemplate.includes("{{traceId}}")) throw new Error(`${configLabel}.probes.traceQueryPathTemplate must include {{traceId}}`); return config; } export function imageSpec(record: Record, path: string): ImageSpec { const image = { repository: stringField(record, "repository", path), tag: stringField(record, "tag", path), pullPolicy: enumField(record, "pullPolicy", path, ["Always", "IfNotPresent", "Never"] as const), }; if (!/^[A-Za-z0-9._/:@-]+$/u.test(`${image.repository}:${image.tag}`)) throw new Error(`${configLabel}.${path} must render a valid image reference`); return image; } export function parseTarget(record: Record, index: number): ObservabilityTarget { const path = `targets[${index}]`; return { id: stringField(record, "id", path), route: stringField(record, "route", path), namespace: kubernetesNameField(record, "namespace", path), role: enumField(record, "role", path, ["active", "standby"] as const), enabled: booleanField(record, "enabled", path), createNamespace: booleanField(record, "createNamespace", path), }; } export function parseOtlpPorts(record: Record, path: string): OtlpPorts { return { grpcPort: portField(record, "grpcPort", path), httpPort: portField(record, "httpPort", path), }; } export function parseServiceConnection(record: Record, index: number): ServiceConnection { const path = `instrumentation.serviceConnections[${index}]`; const refs = record.configRefs === undefined ? null : objectField(record, "configRefs", path); const configRefs = refs === null ? {} : { targetNode: stringField(refs, "targetNode", `${path}.configRefs`), lane: stringField(refs, "lane", `${path}.configRefs`), namespace: stringField(refs, "namespace", `${path}.configRefs`), }; return { serviceName: stringField(record, "serviceName", path), owningRepo: stringField(record, "owningRepo", path), targetNode: refs === null ? stringField(record, "targetNode", path) : resolveConfigRefString(configRefs.targetNode, `${path}.configRefs.targetNode`), lane: refs === null ? stringField(record, "lane", path) : resolveConfigRefString(configRefs.lane, `${path}.configRefs.lane`), namespace: refs === null ? kubernetesNameField(record, "namespace", path) : kubernetesNameValue(resolveConfigRefString(configRefs.namespace, `${path}.configRefs.namespace`), `${path}.configRefs.namespace`), configRefs, configRefGraph: refs === null ? [] : configRefGraph([ { id: `${path}.targetNode`, ref: configRefs.targetNode }, { id: `${path}.lane`, ref: configRefs.lane }, { id: `${path}.namespace`, ref: configRefs.namespace }, ]), requiredSpans: stringArrayField(record, "requiredSpans", path), }; } function kubernetesNameValue(value: string, path: string): string { if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${configLabel}.${path} must resolve to a Kubernetes name`); return value; }