19 KiB
UniDesk Observability Reference
UniDesk 的可观测性优先级高于静默成功。CLI、服务日志、Docker 日志和数据库状态都必须能通过短命令查询。
Capability Boundary
可观测性是定位、验收和排障手段,不是功能完成标准。Trace、日志、诊断文案、fallback 标记、只读原因、降级提示和 issue 进展只能说明系统在哪里失败、为什么失败、修复后如何验证;它们不能把缺失能力包装成已完成,也不能把未修复功能包装成可接受状态。
当证据表明某条用户路径需要的能力不存在、没有真正接入、没有复用预期资源、没有返回最终业务结果,或仍停留在 mock、fixture、fallback、只读替代和提示文案时,正确动作是补齐能力或修复功能本身。允许先补最小观测来定位根因,但完成条件必须回到原始用户入口的真实行为闭环,并证明能力已经可用。
因此,CLI、Web、trace 和 issue 评论中的状态字段必须避免把“已观测到缺口”“已显示失败原因”“已提供诊断入口”表达成“问题已解决”。如果当前阶段只能增加可见性,输出应明确标记为诊断进展或阻塞定位;不得关闭对应功能 issue,不得把观测增强当作验收通过。
Zero Implicit Fallback
运行时异常、投影写入异常、序列化异常、ReferenceError/TypeError、不可达上游、provider 不可用、trace 同步失败和状态机非法转移不得被隐式 fallback 吞掉。任何 catch 后继续返回旧值、空值、默认值、legacy 查询、二次来源仲裁或“看起来成功”的行为,都必须改成以下两类之一:
hard failure: 当前用户路径必须失败,并把 trace_id、错误类型、错误消息、阶段和可重试性透传到 CLI/Web/API 诊断面。observed degraded: 只有业务明确允许降级时才可继续,但必须写入 OTel span event/error、结构化日志、诊断字段和用户可见降级提示;提示必须说明缺失的真实能力,不能把降级输出伪装成正常结果。
禁止用下游 repair、多来源投票、旧缓存补洞、空数组/空对象、默认标题、默认耗时、默认 final response、legacy session store 或浏览器端再计算来掩盖上游异常。尤其是 Workbench/AgentRun/WebProbe 链路中,如果 durable projection、message facts、timing facts、terminal event 或 final response 持久化失败,必须让上游写路径失败并产生 OTel 证据,而不是让前端继续用 fallback 渲染。
每次发现隐式 fallback,都应优先修上游 source of truth:先定位第一个吞错点,删除吞错 fallback,补 OTel/error 透传,再复测原入口。只有确认上游事实已经正确产生后,才允许清理前端展示或 CLI 表格。不得通过增加更多采样器判断、前端兜底字段或 analyzer 自动仲裁来“修好”业务结果。
Web/Workbench trace、Web 哨兵和 web-probe observe 的人工判定入口以 $unidesk-webdev 为准:先用采样器保存的 artifact 渲染 turn-summary 和 trace-frame CLI 视图,再解释 analyzer finding。自动判别器、聚合计数或额外截图保存源不能压过同一采样帧的 CLI trace 视图;若二者冲突,应登记 analyzer/tooling 精度问题或上游投影问题,而不是用 fallback 视图修业务结论。
CLI Logs
异步 job 的 stdout 和 stderr 位于 .state/jobs/。job list 默认只返回最新 50 条摘要,并为已知异步工作流返回轻量 progress.summary;job status 会返回结构化 progress 与有限尾部,避免输出爆炸,同时保留完整日志文件路径便于继续排查。实现必须只读取日志尾部字节,不得先把完整 job 日志读入 CLI 内存;长时命令的阶段、关键对象名和下一步查询命令应优先沉淀到 progress,不能要求调用者先阅读完整日志才能知道是否卡在提交、构建、发布或观测阶段。
Service Logs
服务日志位于 logs/{YYYYMMDD}/,每次 server start 都生成新的本地时间戳前缀。新写入的 UniDesk JSONL 日志必须按小时切片:logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_{service}.jsonl,一天一个目录,禁止长期追加到单个巨大 JSONL。所有 UniDesk Bun 服务(frontend、provider-gateway、Code Queue、project-manager、baidu-netdisk 以及后续新增 Bun 服务)必须复用 src/components/shared/src/rotating-jsonl.ts 中的 createHourlyJsonlWriter;Rust backend-core 必须提供等价的 hourly rotation and retention behavior in src/components/backend-core/src/logger.rs。LOG_FILE 只作为推导 logs 根目录、启动前缀和 service 后缀的 base path,不得长期追加到单个文件。database 通过 PostgreSQL logging collector 写入同一日期目录。
日志保留默认按日志族限制为 512MiB:服务写入或 Code Queue 导出日志时必须扫描同一 service 后缀的历史文件,超过上限后自动删除最旧切片;当前活跃切片不能被保留清理删除。全局上限由 UNIDESK_LOG_RETENTION_BYTES 控制,服务级上限使用 UNIDESK_<SERVICE>_LOG_MAX_BYTES(如 UNIDESK_FRONTEND_LOG_MAX_BYTES、UNIDESK_PROVIDER_GATEWAY_LOG_MAX_BYTES),历史兼容变量只允许作为过渡入口。主 server Compose 服务必须启用 Docker json-file 轮转,默认 UNIDESK_DOCKER_LOG_MAX_SIZE=20m、UNIDESK_DOCKER_LOG_MAX_FILE=3;该配置在服务重建或重建容器后生效。Codex app-server 的 logs_*.sqlite 仅作为 Codex 上游运行时的短暂缓冲,Code Queue 必须周期性导出为同样按小时切片的 codex-app-server JSONL,并删除/压缩已导出的 SQLite 行,避免 logs_2.sqlite 成为长期大文件。
主 server 应安装 bun scripts/cli.ts gc policy install 渲染的低风险防膨胀策略:systemd journal 上限 512MiB,并启用每日 unidesk-gc.timer 执行文件日志与 allowlisted /tmp 低风险 GC。该 timer 不主动 vacuum journal,不触碰数据库、Docker image/volume 或 Baidu staging;输出固定写入 .state/gc/last-run.json 和 .state/gc/last-run.stderr,不得把全量候选 JSON 打进 systemd journal;数据库 trace 留存仍必须由 gc db-trace 显式维护,不得加入默认 timer。
OA Event Flow 的高频 trace 统计不得把每个 trace-stats-updated 投影事件长期写入 oa_events;持久化真相是 oa_trace_stats 与 oa_trace_steps,SSE/API 发布时可以返回短暂投影通知用于实时 UI 刷新。需要历史回收时只通过 gc db-trace plan|run 做显式维护窗口操作,禁止把数据库 VACUUM FULL 或 trace 表大规模删除接入默认 timer。
新增或迁移服务的长期规范:Dockerfile 必须把 src/components/shared 复制到与仓库相同的相对路径,TypeScript 配置必须能解析 shared 引用,Compose 必须传入 LOG_FILE 和 UNIDESK_LOG_RETENTION_BYTES;如果服务需要在内存中暴露 /logs,可以继续维护有限 recentLogs,但落盘只能通过统一 hourly writer。业务归档日志(例如 Code Queue task output archive)可以保留 append-only 文件,但不得复用 UniDesk service JSONL 命名族,也不得替代 /logs 的结构化服务日志。
bun scripts/cli.ts check 必须包含日志轮转门禁:核心 Bun 服务源码不得直接向 LOG_FILE append,且必须引用统一 hourly writer;Rust backend-core logger must expose equivalent rotation/pruning markers for the same check. 新增服务如果进入主 Compose,也要纳入该门禁的 checked file 列表。
Log Access
bun scripts/cli.ts server logs 同时读取文件日志和 Docker logs 尾部。文件日志是服务崩溃时的第一现场,Docker logs 是容器启动失败和 stdout/stderr 的辅助来源。默认输出必须包含 tail 字节数、是否截断和完整文件路径;扩大读取范围只能通过显式 --tail-bytes N,且 CLI 会对单次 tail 设置硬上限。
Diagnostic Output Limits
所有诊断型 CLI 输出必须优先摘要化、尾部化或分页化,禁止默认倾倒大 JSON、全量日志、全量 trace 或 .state/logs 宽泛搜索结果。当前硬限额入口包括:server logs 默认 3000 bytes tail、job list 默认 50 条、job status 默认 12000 bytes tail、codex task/trace/output 默认分页与文本预览、microservice proxy 默认 body 预览且 --raw 仍受硬限额保护。确实需要完整响应时必须显式使用对应的 --full、--full-text、--tail-bytes 或 --limit 参数,并在验收记录中说明为什么需要扩大输出。
面向人工的“显式字段选择”也不能把常用长集合默认展开成 stdout dump。gh issue view/read --json comments 应返回 comment metadata、body 长度/SHA 和短 preview,完整正文通过显式 full/raw 或 trans gh: drill-down 读取;node-scoped hwlab nodes control-plane status --full 是运行面 workload/bridge/SecretRef 的有界 drill-down,完整 JSON 只属于显式 --raw。Secret/API key 输出只能披露对象名、key 名、presence、fingerprint 或 valuesPrinted=false。
本地或远端 AGENTS.md、CLAUDE.md 或同类 agent 入口文档超过 10 KiB、超过 YAML dump 阈值,或被 CLI/SSH/trans 读取时触发自动 dump,不能只把 dump 文件路径当成继续工作的正常入口。该现象表示入口文档已经过长,必须按 docs-spec 把入口文件拆成短索引:只保留 P0 规则摘要、关键命令入口和指向权威文档的链接;具体流程、背景、判定标准和长篇约束迁入对应 skill 的 SKILL.md 或 docs/reference/ 长期参考。拆分后入口文档、skill 和长期参考必须互相交叉引用,避免同一规则在多个位置重复展开或产生第二真相。
CLI 写 stdout/stderr 遇到下游 pipe 关闭的 EPIPE 必须安静退出,不能打印 Bun stack trace。常见验证命令是 set -o pipefail; bun scripts/cli.ts server status | head -1,应只看到第一行 JSON 而无额外错误噪声。
Task Liveness
backend-core 必须把 queued、dispatched、running 视为待处理任务,并通过 TASK_PENDING_TIMEOUT_MS 对长时间没有 provider 终态回报的任务做超时处理。超时任务转为 failed,result 中保留 timeout、previousStatus 和 previousResult 摘要,避免 态势总览 的待处理数量长期卡住且无法解释。
Performance Metrics
backend-core 必须提供 /api/performance,返回滚动窗口内的 HTTP 组件请求统计、最近失败请求、内部操作统计、最近慢操作、进程内存、PGDATA 用量和 Code Queue PostgreSQL 存储摘要。组件统计必须包含请求数、失败数、失败率、平均延迟和 P95,内部操作统计必须包含服务名、操作名、次数、平均延迟和 P95;失败和慢操作记录必须保留时间、状态、耗时、路径或细节,避免只给汇总数字而无法定位。
frontend Bun server 必须提供同源 /api/frontend-performance,记录 webui 静态资源、登录/session、API 代理和 frontend->core 代理操作耗时。浏览器中的 运行总览 / 性能面板 必须把 frontend 与 backend-core 指标合并展示为 Bwebui 曲线、组件汇总、最近失败请求、内部操作汇总和最近慢操作;完整性能 JSON 只能通过显式 查看原始JSON 打开。
Node Resource Status
节点 CPU、内存、磁盘和进程资源指标的实时真相来自 provider-gateway 上报的 system_status,Docker 资源摘要来自 docker_status;backend-core 负责落库、历史采样和 /api/nodes/system-status 聚合。排查资源面板不同步时,必须同时对照 provider-gateway 日志里的 system_status_sent / docker_status_sent、backend-core 日志里的 provider_system_status / provider_docker_status、以及 backend-core 内部 API,而不能只看前端旧值或浏览器缓存。
provider 上报的 collectedAt 在 backend-core 入库前必须解析为 typed timestamp。Rust backend-core 使用 PostgreSQL 参数时不得把 RFC3339 字符串传给 $n::timestamptz 这类 SQL cast;tokio-postgres 会按目标 PostgreSQL 类型序列化参数,字符串参数可能在序列化阶段失败,导致 provider 已发送但数据库仍停在旧采样。等价实现应先解析为 DateTime<Utc> 或目标驱动支持的时间类型,再传入 SQL 参数。
/api/nodes/system-status 必须区分当前指标和最后已知指标。超过 backend stale window 的采样不得继续作为 current 冒充实时状态;接口和前端应暴露 stale、currentCollectedAt 和 lastKnown,并在指标过期时显示过期状态。运维验证优先使用 backend-core 内部只读入口:docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/api/nodes/system-status?limit=5'。外部 frontend 同源 API 可能受登录 session 保护,不应把未登录请求的 401 当成资源同步失败。
Low-Memory Diagnostics
主 server 是低资源、低抖动控制面,排查内存时必须先区分共享内存、容器 cgroup 占用和进程私有占用。PostgreSQL 后端进程的 RSS 会重复显示 shared_buffers 等共享映射,不能把多个 postgres 进程 RSS 简单相加当成真实内存消耗;优先看 docker stats unidesk-database、cgroup memory、/proc/<pid>/smaps_rollup 的 PSS/USS、pg_stat_activity 连接数和 pg_settings 中的 shared_buffers/work_mem。
如果 PostgreSQL 容器总占用和 PSS 并不异常,不应优先通过压缩 shared_buffers 解决主 server OOM。更高优先级是识别非核心、交互式和开发型进程,例如 web terminal、长驻 agent session、一次性日志调查或大输出 CLI,把它们迁移到 D601、增加 TTL/硬上限,或通过 server logs、job status、microservice proxy 的默认输出限额减少瞬时内存尖峰。只有在连接池、真实 cgroup 占用和慢查询证据都指向 PostgreSQL 时,才调整 PostgreSQL 内存参数。
性能优化必须先用这些指标锁定慢操作名称、路径、耗时和代理层级,再改后端查询或前后端通信策略;不得只凭主观体感改 UI。Code Queue 这类控制面页面出现 core_proxy、GET /api/microservices/code-queue/proxy/api/tasks/overview、POST /api/microservices/code-queue/proxy/api/tasks/<id>/read 等超过 1s 的慢操作时,应保留优化前后的性能面板证据,并同时记录 live API 耗时、容器内存、/health 存储摘要和是否仍通过 PostgreSQL/append-only archive 重建历史数据。短 TTL cache、warmup 或页面内存缓存只能作为重复请求抖动保护,性能证据必须证明数据库索引/聚合、分页和渐进式披露本身已把核心路径降到目标内,不能用长缓存遮蔽慢 SQL 或全量 JSON 物化。
当最近失败请求集中出现 frontend core_proxy 502/503/504,路径为 /api/microservices/code-queue/proxy/... 的 overview、trace 或 summary,且 k3s/k8s Pod 仍在运行时,必须先运行 bun scripts/cli.ts microservice diagnostics code-queue,区分 provider-gateway online、WebSocket HTTP tunnel、k3sctl-adapter、Kubernetes API service proxy 和目标 Service 五段状态。provider tunnel 类失败必须记录响应 body/headers 中的 requestId、stage、failureReason、x-unidesk-request-id 和 x-unidesk-tunnel-error;如需主动验证错误结构,运行 bun scripts/cli.ts microservice tunnel-self-test code-queue,该自测应返回预期失败但 ok=true 的诊断结果。随后再继续判断“Kubernetes API service proxy 不可达”“Code Queue 进程不可达”和“Code Queue event loop 被热路径同步工作饿死”。如果 debug health 或 provider-gateway egress health 显示 providerGatewayEgressProxyActiveTunnels 持续偏高、pendingTunnels 非零或 oldestTunnelAgeMs 长时间增长,应先按 provider-gateway egress tunnel 生命周期排障,确认 egress_tcp_open、connect timeout、idle cleanup 与 core socket close 清理是否生效。排障顺序是同时查看 /api/frontend-performance、/api/performance、k3sctl-adapter /api/control-plane、Kubernetes Pod /live、/health、overview/trace-step curl、kubectl top pod 或 Docker stats、容器 RestartCount/OOMKilled 和 Code Queue 日志;如果 Pod 内 /health 也超时,应优先检查实时 output 发布、archive 读取、transcript 构建、统计计算、启动维护、历史 OA backfill 和远程 Provider 准备/SSH 子进程是否阻塞 event loop,而不是先调整 frontend 渲染或代理超时。Code Queue 默认不得在启动时自动执行历史 OA backfill 或通知表索引维护;显式 backfill 必须作为运维动作记录,并在运行期间并发证明 /live、/health 与 /api/tasks/overview 仍快速返回。涉及 D601 等远程 Provider 时,还要检查 runCodeQueueSsh/开发容器准备是否仍存在同步子进程、无 timeout 的 SSH、无上限 stdout/stderr 或 stale TUN 重建等待;修复后必须在远程准备探针运行期间并发证明 Pod /health 与 /api/tasks/overview 仍快速返回。
Code Queue task 明明产出最终回复却反复 retry_wait 时,应优先用任务详情里的 latest attempt 字段核查 terminalStatus、transportClosedBeforeTerminal、appServerExitCode、finalResponseChars、judge.raw._safetyOverride 和 attempt output。OpenCode 远程任务中,opencode completed status=completed exit=0 加当前 attempt 非空 assistant 输出应对应 terminalStatus=completed、transportClosedBeforeTerminal=false;如果因为缺少 step_finish 事件仍触发 _safetyOverride=terminal_not_completed,说明协议终态归一化有回归。相反,当前 attempt 没有最终 assistant response 时即使 tool/read/bash 证据完整,也必须 retry,不能用旧 task.finalResponse 或 reasoning/tool evidence 代替可见最终回复。
Code Queue Liveness
Code Queue 的“任务是否卡死”不能由单一控制面字段判断。排障必须同时看 PostgreSQL 中的 running/judging 任务、D601 scheduler 本地 active run/active slot/active queue、scheduler-owned heartbeat、Trace/OA 持久化进度和 OA publisher pending/lastError。master code-queue-mgr 的 postgres-control-plane 视图只证明数据库行存在;当它显示 activeRunSlotCount=0 但 D601 heartbeat 仍新鲜时,正确结论是 control-plane/execution-plane 分裂,diagnostics 应显示 split-brain 或 degraded,不能宣称任务未执行或卡死。诊断输出中的 effectiveLiveness=live、splitBrainLive=true 和 recommendedAction=continue-supervision 表示这是 heartbeat 新鲜的观测分裂,应继续监督;effectiveLiveness=at-risk 或 recommendedAction=investigate-heartbeat-risk 表示存在 expired/missing/stale heartbeat 风险,需要优先人工确认。
Trace/OA 长时间没有新 seq 但 scheduler heartbeat 正常时,应归类为 trace gap 或 publisher degraded,不得自动 retry。只有 scheduler 本地没有 active run,且对应 owner heartbeat 已过期时,才允许进入 stale recovery candidate;缺失 heartbeat 只能触发 degraded 诊断和人工确认。任何恢复入口都必须由 scheduler 执行,使用条件更新和审计事件区分 user interrupt、admin stale recovery 与 service restart recovery;禁止直接修改 production PostgreSQL 任务状态来“修复” active run。