# 生产监控、告警与日志留存证据

## 结论

2026-07-17 已在生产运行时 `/root/younis-ai-lab-runtime` 完成以下发布门禁：

- Node.js `v22.23.1` 下的健康基线检查；
- 独立 HTTPS 接收端的 `readiness:firing -> readiness:resolved -> backup:firing -> backup:resolved` 四步真实投递；
- request id 只保存 SHA-256 前 16 位，不保存原值；
- `monitor.env`、状态、事件账本、drill 报告和维护报告均为 `0600 root:root`；
- systemd 每分钟 timer、30 天 journald、30 轮宝塔 Nginx logrotate 与 90 天监控/部署记录清理；
- app、migrate、PostgreSQL、CMS、CMS migrate、CMS PostgreSQL 六个容器全部使用 `journald` logging driver；
- 日志驱动切换前分别创建博客与 CMS PostgreSQL gzip 备份，并通过 `gzip -t`；
- 维护窗口中实际发生的 readiness/app/PostgreSQL firing 均完成真实 resolved 投递，最终所有检查为 `ok` 且 `notifiedStatus=ok`；
- 首页、`/ready`、博客和 CMS 登录页公网 smoke 成功。

本次接收端通过独立主机上的 Cloudflare Quick Tunnel 完成 release drill。它证明 HTTPS 投递、receipt header、失败重试和恢复链路，但 Quick Tunnel 没有长期可用性承诺；长期生产运行仍须换成独立、可轮换、具备持久运行保障的命名 endpoint，且不得降低 receipt header 门禁。

## 交互与存储

```mermaid
flowchart LR
    A["systemd timer"] --> B["monitor-runtime.mjs"]
    B --> C["readiness / containers / disk / 5xx / backup checks"]
    B --> D["HTTPS alert receiver"]
    D -->|"2xx + request id header"| B
    B -->|"SHA-256 前 16 位"| E["events-YYYY-MM-DD.jsonl"]
    B --> F["current.json"]
    G["verify-monitor-drill.mjs"] --> E
    G --> F
    G --> H["drills/latest.json"]
```

| 数据 | 生产位置 | 权限或留存 |
| --- | --- | --- |
| webhook 配置 | `/root/younis-ai-lab-runtime/monitor.env` | `0600 root:root`，不进入 Git、镜像、unit 或证据输出 |
| 当前状态 | `state/monitor/current.json` | `0600` |
| transition 账本 | `state/monitor/events-2026-07-17.jsonl` | `0600`，90 天 |
| drill 报告 | `state/monitor/drills/latest.json` | `0600` |
| 最终维护恢复报告 | `state/monitor/drills/monitor-restore-20260717T160731Z.txt` | `0600` |
| journald 策略 | `/etc/systemd/journald.conf.d/younis-ai-lab.conf` | 30 天、最多 512 MiB |
| Nginx 轮转策略 | `/etc/logrotate.d/younis-ai-lab-nginx` | daily、30 轮、压缩 |

## 四步告警演练

生产事件按以下顺序完成，所有 `webhookDelivery` 均为 `delivered`：

| transition | UTC 时间 | request id SHA-256 前 16 位 |
| --- | --- | --- |
| `readiness:firing` | `2026-07-17T06:37:06.897Z` | `878a40d62398f0eb` |
| `readiness:resolved` | `2026-07-17T06:37:08.803Z` | `0fa8b92647c73fd8` |
| `backup:firing` | `2026-07-17T06:37:10.444Z` | `74af6ddbd39bde80` |
| `backup:resolved` | `2026-07-17T06:37:12.231Z` | `eed7cdea018704a9` |

机器验收结果：

```text
verifiedAt=2026-07-17T06:37:14.512Z
eventCount=4
state mode=0600
events mode=0600
state sha256=2c8c9679e96043b681623baf7538d3fda975fa368a011434e05fe58120f4c14e
events sha256=e692dde7dc25c5838651cbf0f50862ca2a112fced67200e1917dc9fdbcd767b1
drill report sha256=de243a94721ee49f7cec7bd2b9e91229f587b86d64532c01c7d8f5e21fedbf29
```

## 真实维护恢复

切换既有容器的 logging driver 时，维护窗口暴露了 split-compose 的同 project 行为：对 CMS compose 使用 `--remove-orphans` 会把博客 service 当作 orphan 删除。外部首页与 `/ready` 短暂返回 `502`；恢复流程立即停止 monitor timer，以同一 `0.1.8` 镜像和既有 volume 重新创建博客 app、migrate 与 PostgreSQL，外部 `/ready` 和首页恢复 `200`。

该次真实故障产生三条 firing，并在恢复后完成对应 resolved：

| check | firing receipt hash | resolved receipt hash |
| --- | --- | --- |
| `readiness` | `32b3ac7ff45c5b7b` | `a977fc9e014c5f5c` |
| `container_app` | `34a7859cfe3faedd` | `50bdf61215509096` |
| `container_postgres` | `978bde5fc80b5e99` | `d8f57895b6165b1e` |

最终权威状态：

```text
all_checks_notified=ok
service_container_count=6
app journald
migrate journald
postgres journald
cms journald
cms-migrate journald
cms-postgres journald
current.json sha256=26423bd550110520625b214024f2c760f81168169a9be51cce58f39bfc498ac3
events-2026-07-17.jsonl sha256=324fddb31e4f9e876a52611930da2ce7c86dce21fc072264ea5139f604b82409
latest.json sha256=de243a94721ee49f7cec7bd2b9e91229f587b86d64532c01c7d8f5e21fedbf29
monitor-restore report sha256=5cb307f92000b8bf56c010409262557b5e970bf72d22a8e687abef5c71afdaf4
```

维护备份均为 `0600 root:root`，最新一组为：

```text
backups/pre-logging-recreate-20260717T065530Z-blog.sql.gz  bytes=3464
backups/pre-logging-recreate-20260717T065530Z-cms.sql.gz   bytes=16064
```

## 运行规则

1. 博客 `compose.yml` 与 CMS `cms-compose.yml` 当前共享默认 Compose project；分别执行 `up -d` 时不得使用 `--remove-orphans`。
2. logging driver 只在容器创建时确定；`docker restart` 不能完成切换。
3. oneshot monitor service 成功后会回到 `inactive/dead`；验收应检查 `Result=success` 与 `ExecMainStatus=0`，不能把 `is-active=inactive` 当作失败。
4. `verify-monitor-drill.mjs` 用于刚完成的四步 drill。后续真实 transition 会合法更新 `current.json` receipt；不得在维护 transition 之后拿旧 drill receipt 再断言当前 receipt 未变化。
5. webhook URL、原始 request id、数据库密码、Cookie、token 和 MFA 数据不得写入本文、任务日志或 Git。

## 本地门禁

与本次生产证据对应的代码门禁：

```text
npm run runtime:install:test  passed
npm run monitor:test          passed
npm run editorial:check       passed for 65 posts
npm run preflight             passed
```
