// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. trace-script module for scripts/src/platform-infra-observability.ts. // Moved mechanically from scripts/src/platform-infra-observability.ts:1575-1813 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 { ObservabilityConfig, ObservabilityTarget } from "./types"; import { tempoService } from "./search-script"; import { imageReference, indent } from "./summary"; export function asArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } export function limitArray(value: unknown, limit: number): unknown[] { return asArray(value).slice(0, limit); } export 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"); } export 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} `; } export 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: - {} `; } export 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)} `; } export 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} strategy: type: RollingUpdate rollingUpdate: maxSurge: 0 maxUnavailable: 1 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} `; } export 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 `; } export 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)} `; } export 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} strategy: type: RollingUpdate rollingUpdate: maxSurge: 0 maxUnavailable: 1 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: {} `; }