Files
pikasTech-unidesk/scripts/src/platform-infra-observability.ts
T
2026-06-19 13:49:39 +00:00

1125 lines
42 KiB
TypeScript

// 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<string, unknown> {
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 <traceId> [--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<Record<string, unknown>> {
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, path: string): OtlpPorts {
return {
grpcPort: portField(record, "grpcPort", path),
httpPort: portField(record, "httpPort", path),
};
}
function parseServiceConnection(record: Record<string, unknown>, 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<string, unknown>, 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<string, unknown> {
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 <traceId>`,
},
};
}
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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 <traceId>` : "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<Record<string, unknown>> {
if (options.traceId === null) throw new Error("observability trace requires --trace-id <traceId>");
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<string, unknown> {
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<string, unknown> {
return {
id: target.id,
route: target.route,
namespace: target.namespace,
role: target.role,
createNamespace: target.createNamespace,
};
}
function policyChecks(yaml: string, target: ObservabilityTarget): Array<Record<string, unknown>> {
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<string, unknown>): Record<string, unknown> {
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<Record<string, unknown>> : [];
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<string, unknown> | null, full: boolean): Record<string, unknown> | null {
if (payload === null) return null;
if (full) return redactSensitiveUnknown(payload) as Record<string, unknown>;
return statusSummary(payload);
}
function sectionJson(sections: Record<string, unknown>, name: string): unknown {
const section = asRecord(sections[name], `sections.${name}`);
return section.json;
}
function objectList(value: unknown): Record<string, unknown>[] {
if (typeof value !== "object" || value === null) return [];
const items = (value as Record<string, unknown>).items;
if (!Array.isArray(items)) return [];
return items.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item));
}
function deploymentSummary(item: Record<string, unknown>): Record<string, unknown> {
const spec = item.spec as Record<string, unknown> | undefined;
const status = item.status as Record<string, unknown> | 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<string, unknown>): Record<string, unknown> {
const status = item.status as Record<string, unknown> | undefined;
return {
name: metadataName(item),
phase: status?.phase ?? null,
};
}
function metadataName(item: Record<string, unknown>): string | null {
const metadata = item.metadata as Record<string, unknown> | undefined;
return typeof metadata?.name === "string" ? metadata.name : null;
}
function manifestObjectSummary(yaml: string): Array<Record<string, string>> {
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");
}