Files
Jyotisha/docs/tasks/TASK-beta-safety-net-20261014.md
T

12 KiB
Raw Blame History

TASK · 内测安全网:每日备份、探活告警、错误汇总、日志轮转与按 requestId 查日志(2026-10-14)

  • 日期:2026-10-14
  • 来源:产品 10-14 内测体检后拍板「内测跑生产」。体检结论:生产没有定时备份、没有探活告警、没有错误上报、容器日志不轮转,出了事既不知道也查不到(BUG-1306 取证就因日志只在容器控制台而失败)。
  • 执行方:Grok(Windows),分支 codex/beta-safety-net-20261014,只推这个分支。Grok 碰不到生产主机:主机侧的安装与演练写成脚本 + runbook,由产品执行。
  • 验收:Claude(Linux)。
  • BUG 编号:开工时核对最大号顺延(写作时 BUG-1317)。其他已推任务书在 docs/tasks/ 里已预占到 BUG-1346,开工时取 docs/BUG_HISTORY.md 与 docs/tasks/ 已引用编号两者的最大号顺延。
  • 串行:在 TASK-release-path-production-20261014 合入 staging 之后开工(两单都改 deploy/README.md,本单的定时工作流也要沿用那一单的发布 SHA 规则)。与 issue #58(TASK-node-runtime-resources-20261013,Node 内存泄漏 / Postgres 往返 / 内存表)边界:#58 可能改 deploy/docker-compose.server.yml 的环境变量透传;本单改同一文件的 logging 段——开工前看 #58 是否已合入,合入则在其之上改,未合入则在回报里写明需 Claude 合并时解冲突。不碰 #57(校正重算)任何文件。

1. 基线

  • 基线 commit:origin/staging 开工时最新(写作时 48a2fadd;需包含发布通路单的合入提交)。
  • 现有备份能力:.gitea/workflows/create-production-recovery.yml(只 workflow_dispatch)调用 deploy/run-production-recovery.sh:pg_dump 自定义格式 → AES-256-CBC/PBKDF2 加密 → 还原到一次性库校验行数 → 删一次性库 → 上传 Gitea artifact(30 天);主机上 /opt/jyotisha-production/backups 保留加密文件;磁盘使用 ≥70% 拒绝。deploy/backup-staging-postgres.sh 只给 staging 手动用。
  • 编排:deploy/docker-compose.server.yml 三个服务 api、web、caddy(PostgreSQL 另在 deploy/docker-compose.postgres.yml),都有 restart: unless-stopped 与 healthcheck,都没有 logging 段(主机 docker daemon 的默认日志配置未知)。
  • 观测:frontend/src/lib/agent-observability.ts 用 console.info("[agent-observability]", JSON.stringify(event)) 写到 web 容器标准输出;Python API 的错误也只在容器标准输出。

2. 事故实证

缺口 证据 后果
无定时备份 仓库内无 schedule: 触发的工作流、无 cron / systemd timer 文件;生产恢复点最后一次是 08-16(run 1881) 内测开始写入真实用户数据后,任何一次误操作或磁盘故障都可能丢失最多「上次手动备份以来」的全部数据
无探活告警 仓库内无 Uptime Kuma / Prometheus / 告警配置;只有容器 healthcheck(只会重启,不会通知人) 站点挂了只能等用户来说
无错误上报 无 Sentry 类集成;错误只在容器 stdout 用户遇到报错,团队不知道,也无法统计
日志不轮转、留存不可控 compose 无 logging;docker compose logs --tail=100 是唯一查法(deploy/README.md) 磁盘可能被日志写满;或日志在重建容器时丢失;BUG-1306 取证失败
不能按 requestId 查 观测 JSON 里有 requestId,但没有按 requestId 检索的工具或流程 用户报「这一轮不对」时无法还原

3. 根因

上线基础设施按「单人维护、手动发布」设计,备份只作为迁移前的一次性保险;没有任何「持续运行时」的观测与告警。内测引入真实用户后这些从可选变成必需。

4. 决策记录(产品 2026-10-14 授权)

  1. 内测跑生产,本单是内测开放前的必需项。
  2. 授权新增定时工作流(.gitea/workflows/ 下新增文件,属于 10-14 对 workflow 的授权范围):每日生产备份。新增文件不得改动已有四个生产工作流的语义(那是发布通路单的范围)。
  3. 红线(产品 10-14):错误上报不得把用户资料、出生资料、对话内容、邮箱、Cookie、JWT 发给任何第三方。 因此本单不接入托管 Sentry 类服务;错误汇总在自己的主机上完成,外发的告警只含「哪个服务、什么错误码、多少次、requestId」,不含正文。
  4. 告警渠道由产品提供(见 §10);本单实现「Webhook 地址从环境 / 主机配置读取」,默认按企业微信 / 钉钉 / 飞书群机器人 Webhook 格式(三者都只要一个 URL),没配置时只写本地日志不外发。
  5. 主机侧安装全部写成脚本 + runbook,由产品执行;Grok 不 SSH、不 dispatch 生产工作流。

5. 硬红线

  1. 备份文件始终加密;不打印、不提交加密密钥、.env.production*、数据库口令;告警消息与日志汇总不含任何用户内容(只含计数、错误码、路由、requestId、时间)。
  2. 不改现有 create-production-recovery.yml / run-production-recovery.sh 的加密、还原校验、锁、磁盘检查逻辑;定时备份复用它们,不另写一套 pg_dump。
  3. 定时备份不要求 main == staging == 发布 SHA(备份不该被发布状态卡住),但仍只从 main 运行、只用 main 上的脚本。
  4. 日志轮转只改 logging 段,不改服务的镜像、环境、端口、healthcheck。
  5. 不新增对外开放的端口;探活从外部访问公网 https://jyotisha.chat/api/health,不暴露 5200 / 3000。
  6. 不写真实用户资料;测试用虚构数据。
  7. Grok 不 push main、不推 staging、不 dispatch 生产工作流。

6. 任务分解

项 内容 验收标准
T1 每日备份工作流 新增 .gitea/workflows/backup-production-daily.yml:schedule 每天北京时间 04:00 + workflow_dispatch;从 main 取 deploy/run-production-recovery.sh 执行(复用恢复点工作流的 SSH、锁、产物上传步骤,能抽成共用脚本就抽);artifact 保留天数可配,默认 14。主机侧本地加密文件保留策略:新增清理步骤只删 backups/ 下由本工作流生成、超过 N 天(默认 7)的 *.dump.enc 与对应 verify 文件,按文件名前缀严格匹配,绝不删其他文件 合同测试断言:有 schedule、只从 main 运行、调用同一个 recovery 脚本、清理只匹配本工作流前缀、不打印密钥;清理逻辑写成独立 shell 函数并有 bats 风格或 Python 子进程测试(虚构目录:只删过期的本前缀文件,其他文件不动)
T2 恢复演练 runbook docs/operations/production-restore-drill-20261014.md:从某一天的备份 artifact 还原到一次性库(不碰 jyotisha 主库)→ 核对表行数 → 删除;以及真正整库还原的步骤(停写、还原、核对、恢复服务)与预计停机时间。演练结果模板 docs/testing/production-restore-drill-20261014.md 步骤可照做;演练由产品在内测开放前执行一次,结果回填模板
T3 探活与告警 deploy/monitor/healthcheck-alert.sh:每分钟 GET https://jyotisha.chat/api/health(超时 10s),连续 3 次失败或 status != ok 或 deployment.gitCommit 与上次不同且非发布窗口时发告警;恢复时发「已恢复」;同时检查主机磁盘 ≥80% 告警。Webhook URL 从 /opt/jyotisha-production/.monitor.env(0600)读取。runbook:在哪台机器装(建议 Gitea runner 所在机器,不在生产主机本身——主机挂了它也就挂了;磁盘检查例外,需在生产主机上单独跑)、用 cron 还是 systemd timer。不在 Gitea Actions 里每分钟跑 job(runner 负担大、间隔不可靠) Shell 测试:用本地桩服务模拟 200 / 500 / 超时 / 字段缺失;连续 3 次才告警、恢复只发一次、无 Webhook 时只写本地日志;告警正文只含允许字段
T4 错误汇总(不外发正文) deploy/monitor/error-digest.sh:每 5 分钟读最近 5 分钟的 web / api 容器日志(docker compose logs --since),按「服务 + 错误类别 / 状态码 / agent 观测里的 errorCode」计数;超过阈值(默认 5 分钟内同类 ≥10 次,或任何 5xx ≥20 次)发告警,消息里附最多 5 个 requestId;每天 09:00 发一份前一天汇总。需要时在 web / api 的错误日志里补齐结构化字段(requestId、route、errorCode),不加用户内容 用虚构日志样本测试分类与阈值;断言告警文本不含邮箱、出生日期、会话文本(用含这些内容的样本日志验证会被丢弃)
T5 日志轮转与留存 deploy/docker-compose.server.yml 与 docker-compose.postgres.yml 每个服务加 logging: { driver: json-file, options: { max-size: "20m", max-file: "10" } }(约每服务 200 MB 上限);runbook 说明主机 daemon 级默认值如何确认 合同测试:每个服务都有 logging 段且值一致;compose config 能解析(本地 docker compose -f ... config 跑一次)
T6 按 requestId 查日志 deploy/find-request-log.sh <requestId> [--since 24h]:在 web / api / caddy 日志里检索并按时间排序输出;只读。文档写进 deploy/README.md 运维段 用虚构日志样本测试;拒绝非 UUID / 非约定格式的参数(防命令注入)
T7 文档与登记 deploy/README.md 新增「运维与告警」段:备份在哪、保留多久、怎么还原、告警发到哪、怎么查一轮请求;BLOCKED.md 登记需产品在主机上执行的安装步骤(解除后划掉) Claude 逐条对照脚本

7. 让步顺序

T1、T3、T5 不可让(内测开放的最低线:有备份、挂了有人知道、磁盘不会被日志写满)。T2 runbook 不可让,演练执行可在开放前最后一天完成。T4 可让为「内测第一周补上」,但 T6 不可让(至少能查)。

8. 验收(执行方自测 + Claude 复验)

  • Shell:bash -n;shellcheck(有则跑);上表各项测试。
  • 合同测试:放在 frontend/tests/staging-backend-workflows.test.ts 同类文件或新增 tests/test_beta_safety_net.py。
  • 前端改动(若在 web 错误日志补字段):tsc --noEmit 0 错;npm run lint 0 error;npm test 失败名单与基线逐名一致。
  • Python:快速门 scripts/run_quality_gate.py --profile quick。
  • 隐私:tests/test_repo_privacy_markers.py;新增测试样本只用虚构数据。
  • 文档:docs/BUG_HISTORY.md(无备份 / 无告警 / 日志不轮转各记一条或合并一条,状态写清「代码已交,主机安装待产品」)、CHANGELOG.md(运维变化,用户不可见可写一句)、docs/tasks/PROGRESS-beta-safety-net-20261014.md、docs/tasks/README.md 状态。

9. 开工前置

git fetch origin --prune
git status -sb
# 确认发布通路单已合入:git log origin/staging --oneline | grep -i release-path
git worktree add -b codex/beta-safety-net-20261014 .worktrees/beta-safety-net-20261014 origin/staging

10. 需产品提供 / 拍板

  1. 告警发到哪:建议企业微信 / 钉钉 / 飞书任选一个群,建一个群机器人,把 Webhook 地址交给你自己保管、写进主机 .monitor.env(不要发到聊天里)。不选的话只写本地日志,等于没有告警。
  2. 探活装在哪台机器:建议 Gitea runner 所在机器(与生产主机分开);磁盘检查另装在生产主机。
  3. 备份保留:默认 Gitea artifact 14 天 + 主机本地 7 天;有更长要求(如 30 天)告诉 Claude,注意 Gitea 服务器磁盘。
  4. 恢复演练时间:内测开放前执行一次,约 30 分钟,不影响线上(还原到一次性库)。
  5. 每日汇总发送时间:默认 09:00。

11. 产品答复(2026-10-14,覆盖上文对应建议值)

  1. 告警渠道:飞书群机器人(Webhook 地址由产品在主机 / runner 的环境里配置,不进仓库、不进日志)。T3 按飞书机器人消息格式实现(msg_type: text,可选签名校验)。
  2. 其余按任务书建议默认值。