// 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 { startJob } from "./jobs"; import { compactCapture, compactUnknown, createYamlFieldReader, numberField, parseJsonOutput, redactSensitiveUnknown, shQuote, capture, } from "./platform-infra-ops-library"; const configFile = rootPath("config", "platform-infra", "observability.yaml"); const configLabel = "config/platform-infra/observability.yaml"; const fieldManager = "unidesk-platform-observability"; const { asRecord, objectField, arrayOfRecords, stringField, integerField, booleanField, stringArrayField, numberArrayField, enumField, kubernetesNameField, portField, apiPathField, } = createYamlFieldReader(configLabel); interface ObservabilityConfig { version: number; kind: "platform-infra-observability"; metadata: { id: string; owner: string; spec: string; relatedIssues: number[] }; defaults: { targetId: string }; images: { collector: ImageSpec; tempo: ImageSpec; }; targets: ObservabilityTarget[]; collector: { deploymentName: string; serviceName: string; configMapName: string; replicas: number; healthPort: number; otlp: OtlpPorts; }; traceBackend: { type: "tempo"; deploymentName: string; serviceName: string; configMapName: string; replicas: number; httpPort: number; otlp: OtlpPorts; storage: { mode: "emptyDir"; retention: string }; }; sampling: { mode: "parentbased_traceidratio"; ratio: number }; instrumentation: { contextPropagation: string[]; serviceConnections: ServiceConnection[]; }; resourceAttributes: { required: string[]; businessCorrelationAttributes: string[]; }; probes: { readinessPath: string; traceQueryPathTemplate: string; statusEndpoints: StatusEndpoint[]; }; } interface ImageSpec { repository: string; tag: string; pullPolicy: "Always" | "IfNotPresent" | "Never"; } interface ObservabilityTarget { id: string; route: string; namespace: string; role: "active" | "standby"; enabled: boolean; createNamespace: boolean; } interface OtlpPorts { grpcPort: number; httpPort: number; } interface ServiceConnection { serviceName: string; owningRepo: string; targetNode: string; lane: string; namespace: string; requiredSpans: string[]; } interface StatusEndpoint { name: string; service: string; portName: string; path: string; } interface CommonOptions { targetId: string | null; full: boolean; raw: boolean; } interface ApplyOptions extends CommonOptions { confirm: boolean; dryRun: boolean; wait: boolean; } interface TraceOptions extends CommonOptions { traceId: string | null; } export function observabilityHelp(): Record { return { command: "platform-infra observability plan|apply|status|validate|trace", output: "json", configTruth: "config/platform-infra/observability.yaml", spec: "PJ2026-01060501 OTel追踪 draft-2026-06-19-p0", usage: [ "bun scripts/cli.ts platform-infra observability plan --target D601", "bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run", "bun scripts/cli.ts platform-infra observability apply --target D601 --confirm", "bun scripts/cli.ts platform-infra observability status --target D601 [--full|--raw]", "bun scripts/cli.ts platform-infra observability validate --target D601 [--full|--raw]", "bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id [--full|--raw]", ], boundary: "Prometheus remains the metrics source; this command owns only platform-infra OTel Collector, trace backend readiness, and trace lookup.", }; } export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise> { const [action = "plan"] = args; if (action === "plan") return plan(parseCommonOptions(args.slice(1))); if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1))); if (action === "status") return await status(config, parseCommonOptions(args.slice(1))); if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1))); if (action === "trace") return await trace(config, parseTraceOptions(args.slice(1))); return { ok: false, error: "unsupported-platform-infra-observability-command", args, help: observabilityHelp() }; } function parseCommonOptions(args: string[]): CommonOptions { let targetId: string | null = null; let full = false; let raw = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--target must be a simple target id"); targetId = value; index += 1; } else if (arg === "--full") { full = true; } else if (arg === "--raw") { raw = true; full = true; } else { throw new Error(`unsupported observability option: ${arg}`); } } return { targetId, full, raw }; } function parseApplyOptions(args: string[]): ApplyOptions { const commonArgs: string[] = []; let confirm = false; let dryRun = false; let wait = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--confirm") confirm = true; else if (arg === "--dry-run") dryRun = true; else if (arg === "--wait") wait = true; else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } if (confirm && dryRun) throw new Error("observability apply accepts only one of --confirm or --dry-run"); return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait }; } function parseTraceOptions(args: string[]): TraceOptions { const commonArgs: string[] = []; let traceId: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--trace-id") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--trace-id requires a value"); if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error("--trace-id has an unsupported format"); traceId = value; index += 1; } else { commonArgs.push(arg); if (arg === "--target") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } return { ...parseCommonOptions(commonArgs), traceId }; } 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; } 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; } 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), }; } function parseOtlpPorts(record: Record, path: string): OtlpPorts { return { grpcPort: portField(record, "grpcPort", path), httpPort: portField(record, "httpPort", path), }; } function parseServiceConnection(record: Record, index: number): ServiceConnection { const path = `instrumentation.serviceConnections[${index}]`; return { serviceName: stringField(record, "serviceName", path), owningRepo: stringField(record, "owningRepo", path), targetNode: stringField(record, "targetNode", path), lane: stringField(record, "lane", path), namespace: kubernetesNameField(record, "namespace", path), requiredSpans: stringArrayField(record, "requiredSpans", path), }; } function parseStatusEndpoint(record: Record, index: number): StatusEndpoint { const path = `probes.statusEndpoints[${index}]`; return { name: stringField(record, "name", path), service: kubernetesNameField(record, "service", path), portName: stringField(record, "portName", path), path: apiPathField(record, "path", path), }; } function assertKnownEnabledTarget(targets: ObservabilityTarget[], targetId: string, path: string): void { const target = targets.find((item) => item.id.toLowerCase() === targetId.toLowerCase()); if (target === undefined) throw new Error(`${configLabel}.${path} references unknown target ${targetId}; known targets: ${targets.map((item) => item.id).join(", ")}`); if (!target.enabled) throw new Error(`${configLabel}.${path} references disabled target ${target.id}`); } function resolveTarget(observability: ObservabilityConfig, targetId: string | null): ObservabilityTarget { const resolved = targetId ?? observability.defaults.targetId; const target = observability.targets.find((item) => item.id.toLowerCase() === resolved.toLowerCase()); if (target === undefined) throw new Error(`unknown observability target ${resolved}; known targets: ${observability.targets.map((item) => item.id).join(", ")}`); if (!target.enabled) throw new Error(`observability target ${target.id} is disabled in ${configLabel}`); return target; } function plan(options: CommonOptions): Record { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const yaml = renderManifest(observability, target); const policy = policyChecks(yaml, target); return { ok: policy.every((check) => check.ok), action: "platform-infra-observability-plan", mutation: false, config: configSummary(observability, target), renderPlan: { target: targetSummary(target), objects: manifestObjectSummary(yaml), otlp: { collectorGrpcEndpoint: `${observability.collector.serviceName}.${target.namespace}.svc.cluster.local:${observability.collector.otlp.grpcPort}`, collectorHttpEndpoint: `http://${observability.collector.serviceName}.${target.namespace}.svc.cluster.local:${observability.collector.otlp.httpPort}`, backendGrpcEndpoint: `${observability.traceBackend.serviceName}.${target.namespace}.svc.cluster.local:${observability.traceBackend.otlp.grpcPort}`, }, instrumentation: observability.instrumentation.serviceConnections, resourceAttributes: observability.resourceAttributes, }, policy, next: { dryRun: `bun scripts/cli.ts platform-infra observability apply --target ${target.id} --dry-run`, apply: `bun scripts/cli.ts platform-infra observability apply --target ${target.id} --confirm`, status: `bun scripts/cli.ts platform-infra observability status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra observability validate --target ${target.id}`, trace: `bun scripts/cli.ts platform-infra observability trace --target ${target.id} --trace-id `, }, }; } async function apply(config: UniDeskConfig, options: ApplyOptions): Promise> { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const yaml = renderManifest(observability, target); const policy = policyChecks(yaml, target); if (!policy.every((check) => check.ok)) return { ok: false, action: "platform-infra-observability-apply", mode: "policy-blocked", policy }; if (options.confirm && !options.wait) { const job = startJob( `platform_infra_observability_apply_${target.id.toLowerCase()}`, ["bun", "scripts/cli.ts", "platform-infra", "observability", "apply", "--target", target.id, "--confirm", "--wait"], `Apply ${target.id} platform-infra OTel Collector and trace backend through the controlled UniDesk CLI`, ); return { ok: true, action: "platform-infra-observability-apply", mode: "async-job", mutation: true, target: targetSummary(target), job, statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, next: { status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, rollout: `bun scripts/cli.ts platform-infra observability status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra observability validate --target ${target.id}`, }, }; } const result = await capture(config, target.route, ["sh"], applyScript({ yaml, target, dryRun: options.dryRun, wait: options.wait, collectorDeploymentName: observability.collector.deploymentName, backendDeploymentName: observability.traceBackend.deploymentName, })); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-observability-apply", mode: options.dryRun ? "dry-run" : "confirmed", mutation: !options.dryRun, target: targetSummary(target), policy, remote: parsed ?? compactCapture(result, { full: true }), }; } async function status(config: UniDeskConfig, options: CommonOptions): Promise> { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const result = await capture(config, target.route, ["sh"], statusScript(observability, target, options.full)); const parsed = parseJsonOutput(result.stdout); const summary = parsed === null ? null : statusSummary(parsed); return { ok: result.exitCode === 0 && summary?.ready === true, action: "platform-infra-observability-status", mutation: false, target: targetSummary(target), summary, remote: options.raw ? parsed : compactStatus(parsed, options.full) ?? compactCapture(result, { full: true }), next: { plan: `bun scripts/cli.ts platform-infra observability plan --target ${target.id}`, apply: `bun scripts/cli.ts platform-infra observability apply --target ${target.id} --confirm`, validate: `bun scripts/cli.ts platform-infra observability validate --target ${target.id}`, }, }; } async function validate(config: UniDeskConfig, options: CommonOptions): Promise> { const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const result = await capture(config, target.route, ["sh"], statusScript(observability, target, options.full)); const parsed = parseJsonOutput(result.stdout); const summary = parsed === null ? null : statusSummary(parsed); const ready = summary?.ready === true; return { ok: result.exitCode === 0 && ready, action: "platform-infra-observability-validate", mutation: false, target: targetSummary(target), summary, validation: { readiness: ready ? "passed" : "failed", testTrace: "not-generated-by-this-stage", traceQuery: ready ? `bun scripts/cli.ts platform-infra observability trace --target ${target.id} --trace-id ` : "blocked-until-runtime-ready", metricsBoundary: "Prometheus/RUM remains outside this trace readiness check.", }, remote: options.raw ? parsed : compactStatus(parsed, options.full) ?? compactCapture(result, { full: true }), }; } async function trace(config: UniDeskConfig, options: TraceOptions): Promise> { if (options.traceId === null) throw new Error("observability trace requires --trace-id "); const observability = readObservabilityConfig(); const target = resolveTarget(observability, options.targetId); const tracePath = observability.probes.traceQueryPathTemplate.replaceAll("{{traceId}}", encodeURIComponent(options.traceId)); const result = await capture(config, target.route, ["sh"], traceScript(observability, target, tracePath)); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-observability-trace", mutation: false, target: targetSummary(target), traceId: options.traceId, query: { backend: observability.traceBackend.type, service: observability.traceBackend.serviceName, path: tracePath, }, result: options.raw ? redactSensitiveUnknown(parsed) : compactUnknown(redactSensitiveUnknown(parsed)), }; } function renderManifest(observability: ObservabilityConfig, target: ObservabilityTarget): string { const collectorImage = imageReference(observability.images.collector); const tempoImage = imageReference(observability.images.tempo); return [ target.createNamespace ? namespaceManifest(target) : "", allowAllNetworkPolicy(target), collectorConfigMap(observability, target), collectorDeployment(observability, target, collectorImage), collectorService(observability, target), tempoConfigMap(observability, target), tempoDeployment(observability, target, tempoImage), tempoService(observability, target), ].filter((item) => item.trim().length > 0).join("\n---\n"); } function namespaceManifest(target: ObservabilityTarget): string { return `apiVersion: v1 kind: Namespace metadata: name: ${target.namespace} labels: app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} `; } function allowAllNetworkPolicy(target: ObservabilityTarget): string { return `apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-all namespace: ${target.namespace} labels: app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - {} egress: - {} `; } function collectorConfigMap(observability: ObservabilityConfig, target: ObservabilityTarget): string { const config = `receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:${observability.collector.otlp.grpcPort} http: endpoint: 0.0.0.0:${observability.collector.otlp.httpPort} processors: batch: {} exporters: otlp/tempo: endpoint: ${observability.traceBackend.serviceName}.${target.namespace}.svc.cluster.local:${observability.traceBackend.otlp.grpcPort} tls: insecure: true extensions: health_check: endpoint: 0.0.0.0:${observability.collector.healthPort} service: extensions: [health_check] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/tempo] `; return `apiVersion: v1 kind: ConfigMap metadata: name: ${observability.collector.configMapName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk annotations: unidesk.ai/spec: "${observability.metadata.spec}" data: collector.yaml: | ${indent(config, 4)} `; } function collectorDeployment(observability: ObservabilityConfig, target: ObservabilityTarget, image: string): string { return `apiVersion: apps/v1 kind: Deployment metadata: name: ${observability.collector.deploymentName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: replicas: ${observability.collector.replicas} selector: matchLabels: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing template: metadata: labels: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing app.kubernetes.io/part-of: platform-infra annotations: unidesk.ai/spec: "${observability.metadata.spec}" spec: containers: - name: collector image: ${image} imagePullPolicy: ${observability.images.collector.pullPolicy} args: - --config=/etc/otelcol/collector.yaml ports: - name: otlp-grpc containerPort: ${observability.collector.otlp.grpcPort} - name: otlp-http containerPort: ${observability.collector.otlp.httpPort} - name: health containerPort: ${observability.collector.healthPort} readinessProbe: httpGet: path: / port: health volumeMounts: - name: config mountPath: /etc/otelcol/collector.yaml subPath: collector.yaml readOnly: true volumes: - name: config configMap: name: ${observability.collector.configMapName} `; } function collectorService(observability: ObservabilityConfig, target: ObservabilityTarget): string { return `apiVersion: v1 kind: Service metadata: name: ${observability.collector.serviceName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: type: ClusterIP selector: app.kubernetes.io/name: ${observability.collector.deploymentName} app.kubernetes.io/component: tracing ports: - name: otlp-grpc port: ${observability.collector.otlp.grpcPort} targetPort: otlp-grpc - name: otlp-http port: ${observability.collector.otlp.httpPort} targetPort: otlp-http - name: health port: ${observability.collector.healthPort} targetPort: health `; } function tempoConfigMap(observability: ObservabilityConfig, _target: ObservabilityTarget): string { const config = `server: http_listen_port: ${observability.traceBackend.httpPort} distributor: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:${observability.traceBackend.otlp.grpcPort} http: endpoint: 0.0.0.0:${observability.traceBackend.otlp.httpPort} ingester: trace_idle_period: 10s max_block_duration: 5m compactor: compaction: block_retention: ${observability.traceBackend.storage.retention} storage: trace: backend: local wal: path: /var/tempo/wal local: path: /var/tempo/traces `; return `apiVersion: v1 kind: ConfigMap metadata: name: ${observability.traceBackend.configMapName} namespace: ${_target.namespace} labels: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk annotations: unidesk.ai/spec: "${observability.metadata.spec}" data: tempo.yaml: | ${indent(config, 4)} `; } function tempoDeployment(observability: ObservabilityConfig, target: ObservabilityTarget, image: string): string { return `apiVersion: apps/v1 kind: Deployment metadata: name: ${observability.traceBackend.deploymentName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: replicas: ${observability.traceBackend.replicas} selector: matchLabels: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend template: metadata: labels: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend app.kubernetes.io/part-of: platform-infra annotations: unidesk.ai/spec: "${observability.metadata.spec}" spec: containers: - name: tempo image: ${image} imagePullPolicy: ${observability.images.tempo.pullPolicy} args: - -config.file=/etc/tempo/tempo.yaml ports: - name: http containerPort: ${observability.traceBackend.httpPort} - name: otlp-grpc containerPort: ${observability.traceBackend.otlp.grpcPort} - name: otlp-http containerPort: ${observability.traceBackend.otlp.httpPort} readinessProbe: httpGet: path: ${observability.probes.readinessPath} port: http volumeMounts: - name: config mountPath: /etc/tempo/tempo.yaml subPath: tempo.yaml readOnly: true - name: data mountPath: /var/tempo volumes: - name: config configMap: name: ${observability.traceBackend.configMapName} - name: data emptyDir: {} `; } function tempoService(observability: ObservabilityConfig, target: ObservabilityTarget): string { return `apiVersion: v1 kind: Service metadata: name: ${observability.traceBackend.serviceName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: type: ClusterIP selector: app.kubernetes.io/name: ${observability.traceBackend.deploymentName} app.kubernetes.io/component: trace-backend ports: - name: http port: ${observability.traceBackend.httpPort} targetPort: http - name: otlp-grpc port: ${observability.traceBackend.otlp.grpcPort} targetPort: otlp-grpc - name: otlp-http port: ${observability.traceBackend.otlp.httpPort} targetPort: otlp-http `; } function applyScript(params: { yaml: string; target: ObservabilityTarget; dryRun: boolean; wait: boolean; collectorDeploymentName: string; backendDeploymentName: string; }): string { const encoded = Buffer.from(params.yaml, "utf8").toString("base64"); const dryRunArg = params.dryRun ? "--dry-run=server" : ""; const wait = params.dryRun || !params.wait ? "wait_disposition=skipped" : [ `kubectl -n ${shQuote(params.target.namespace)} rollout status deployment/${shQuote(params.collectorDeploymentName)} --timeout=180s >"$tmp/collector-rollout.out" 2>"$tmp/collector-rollout.err"`, "collector_rollout_rc=$?", `kubectl -n ${shQuote(params.target.namespace)} rollout status deployment/${shQuote(params.backendDeploymentName)} --timeout=180s >"$tmp/backend-rollout.out" 2>"$tmp/backend-rollout.err"`, "backend_rollout_rc=$?", "wait_disposition=executed", ].join("\n"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/platform-infra-observability.yaml" printf '%s' '${encoded}' | base64 -d > "$manifest" kubectl apply --server-side --field-manager=${fieldManager} ${dryRunArg} -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err" apply_rc=$? collector_rollout_rc=0 backend_rollout_rc=0 ${wait} python3 - "$apply_rc" "$collector_rollout_rc" "$backend_rollout_rc" "$wait_disposition" "$tmp/apply.out" "$tmp/apply.err" "$tmp/collector-rollout.out" "$tmp/collector-rollout.err" "$tmp/backend-rollout.out" "$tmp/backend-rollout.err" <<'PY' import json, os, sys def text(path, limit=6000): try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] except FileNotFoundError: return "" apply_rc = int(sys.argv[1]) collector_rc = int(sys.argv[2]) backend_rc = int(sys.argv[3]) payload = { "ok": apply_rc == 0 and collector_rc == 0 and backend_rc == 0, "target": "${params.target.id}", "namespace": "${params.target.namespace}", "dryRun": ${params.dryRun ? "True" : "False"}, "apply": {"exitCode": apply_rc, "stdout": text(sys.argv[5]), "stderr": text(sys.argv[6])}, "rollout": { "disposition": sys.argv[4], "collector": {"exitCode": collector_rc, "stdout": text(sys.argv[7]), "stderr": text(sys.argv[8])}, "backend": {"exitCode": backend_rc, "stdout": text(sys.argv[9]), "stderr": text(sys.argv[10])}, }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function statusScript(observability: ObservabilityConfig, target: ObservabilityTarget, full: boolean): string { const endpointsJson = JSON.stringify(observability.probes.statusEndpoints); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT capture_json() { name="$1" shift "$@" >"$tmp/$name.json" 2>"$tmp/$name.err" echo $? >"$tmp/$name.rc" } capture_raw() { name="$1" shift "$@" >"$tmp/$name.out" 2>"$tmp/$name.err" echo $? >"$tmp/$name.rc" } capture_json namespace kubectl get namespace ${shQuote(target.namespace)} -o json capture_json deployments kubectl -n ${shQuote(target.namespace)} get deployment ${shQuote(observability.collector.deploymentName)} ${shQuote(observability.traceBackend.deploymentName)} -o json capture_json services kubectl -n ${shQuote(target.namespace)} get service ${shQuote(observability.collector.serviceName)} ${shQuote(observability.traceBackend.serviceName)} -o json capture_json pods kubectl -n ${shQuote(target.namespace)} get pods -l ${shQuote(`app.kubernetes.io/name in (${observability.collector.deploymentName},${observability.traceBackend.deploymentName})`)} -o json python3 - "$tmp" '${endpointsJson.replaceAll("'", "'\"'\"'")}' <<'PY' import json, subprocess, sys tmp = sys.argv[1] endpoints = json.loads(sys.argv[2]) namespace = "${target.namespace}" def read(path, binary=False, limit=8000): try: mode = "rb" if binary else "r" with open(path, mode, encoding=None if binary else "utf-8", errors=None if binary else "replace") as fh: data = fh.read() if binary: data = data.decode("utf-8", errors="replace") return data[-limit:] except FileNotFoundError: return "" def rc(name): try: return int(read(f"{tmp}/{name}.rc").strip() or "1") except ValueError: return 1 def parsed_json(name): raw = read(f"{tmp}/{name}.json", limit=1000000) try: return json.loads(raw) if raw else None except Exception: return None def compact_section(name): return { "exitCode": rc(name), "stderrTail": read(f"{tmp}/{name}.err", limit=2000), "json": parsed_json(name), } probe_results = [] for ep in endpoints: path = ep.get("path") or "/" if not path.startswith("/"): path = "/" + path proxy_path = f"/api/v1/namespaces/{namespace}/services/http:{ep['service']}:{ep['portName']}/proxy{path}" proc = subprocess.run(["kubectl", "get", "--raw", proxy_path], text=True, capture_output=True, timeout=20) probe_results.append({ "name": ep["name"], "service": ep["service"], "portName": ep["portName"], "path": path, "exitCode": proc.returncode, "stdoutTail": proc.stdout[-1000:], "stderrTail": proc.stderr[-1000:], "ok": proc.returncode == 0, }) payload = { "ok": rc("namespace") == 0 and rc("deployments") == 0 and rc("services") == 0 and all(item["ok"] for item in probe_results), "target": "${target.id}", "namespace": namespace, "sections": { "namespace": compact_section("namespace"), "deployments": compact_section("deployments"), "services": compact_section("services"), "pods": compact_section("pods"), }, "probes": probe_results, } print(json.dumps(payload, ensure_ascii=False, indent=2 if ${full ? "True" : "False"} else None)) sys.exit(0 if payload["ok"] else 1) PY `; } function traceScript(observability: ObservabilityConfig, target: ObservabilityTarget, tracePath: string): string { const proxyPath = `/api/v1/namespaces/${target.namespace}/services/http:${observability.traceBackend.serviceName}:http/proxy${tracePath}`; return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT kubectl get --raw ${shQuote(proxyPath)} >"$tmp/trace.out" 2>"$tmp/trace.err" rc=$? python3 - "$rc" "$tmp/trace.out" "$tmp/trace.err" <<'PY' import json, sys def text(path, limit=12000): try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] except FileNotFoundError: return "" body = text(sys.argv[2], 200000) try: parsed = json.loads(body) if body else None except Exception: parsed = body[-12000:] payload = { "ok": int(sys.argv[1]) == 0, "path": "${tracePath}", "proxyPath": "${proxyPath}", "body": parsed, "stderrTail": text(sys.argv[3], 4000), } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } 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, }; } function targetSummary(target: ObservabilityTarget): Record { return { id: target.id, route: target.route, namespace: target.namespace, role: target.role, createNamespace: target.createNamespace, }; } 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}.` }, ]; } 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 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)), probes: probes.map((item) => ({ name: item.name, ok: item.ok === true, service: item.service, path: item.path, stderrTail: item.ok === true ? "" : item.stderrTail, })), }; } function compactStatus(payload: Record | null, full: boolean): Record | null { if (payload === null) return null; if (full) return redactSensitiveUnknown(payload) as Record; return statusSummary(payload); } function sectionJson(sections: Record, name: string): unknown { const section = asRecord(sections[name], `sections.${name}`); return section.json; } 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)); } 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, }; } function podSummary(item: Record): Record { const status = item.status as Record | undefined; return { name: metadataName(item), phase: status?.phase ?? null, }; } function metadataName(item: Record): string | null { const metadata = item.metadata as Record | undefined; return typeof metadata?.name === "string" ? metadata.name : null; } 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 }; }); } function imageReference(image: ImageSpec): string { return `${image.repository}:${image.tag}`; } function indent(value: string, spaces: number): string { const prefix = " ".repeat(spaces); return value.split("\n").map((line) => `${prefix}${line}`).join("\n"); }