docs: P3 backup+observability spec + implementation plan (design decisions D1/D2/D3 defaulted after unanswered consult)
This commit is contained in:
@@ -55,3 +55,4 @@
|
|||||||
| P5 收藏/评分 | `docs/specs/2026-09-30-favorites-ratings-design.md` | `docs/plans/2026-09-30-favorites-ratings.md`(已执行,2026-09-30) |
|
| P5 收藏/评分 | `docs/specs/2026-09-30-favorites-ratings-design.md` | `docs/plans/2026-09-30-favorites-ratings.md`(已执行,2026-09-30) |
|
||||||
| 创作者中心 `/creator`(维护者直接需求) | `docs/specs/2026-09-30-creator-center-design.md` | `docs/plans/2026-09-30-creator-center.md`(已执行,2026-09-30) |
|
| 创作者中心 `/creator`(维护者直接需求) | `docs/specs/2026-09-30-creator-center-design.md` | `docs/plans/2026-09-30-creator-center.md`(已执行,2026-09-30) |
|
||||||
| P2 CI+测试基线 | `docs/specs/2026-09-30-ci-test-baseline-design.md` | `docs/plans/2026-09-30-ci-test-baseline.md`(已执行,2026-09-30) |
|
| P2 CI+测试基线 | `docs/specs/2026-09-30-ci-test-baseline-design.md` | `docs/plans/2026-09-30-ci-test-baseline.md`(已执行,2026-09-30) |
|
||||||
|
| P3 备份+可观测 | `docs/specs/2026-09-30-backup-observability-design.md` | `docs/plans/2026-09-30-backup-observability.md` |
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# P3 备份 + 可观测 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** crearte-server 0.12.0(slog 结构化日志 + 请求日志 + 手写 `/metrics` + 限流权衡记录)与 crearte-deploy 0.6.0(`backup.sh` / `restore-drill.sh` / BACKUP-RUNBOOK / CI dry-run),端到端验证后合主。前端零改动。
|
||||||
|
|
||||||
|
**Architecture:** server 侧新增 `internal/observability` 包(mutex Registry + gin 中间件 + exposition 函数),config 增 `LOG_FORMAT`/`LOG_LEVEL`,26 处 `log.Printf` 机械迁移到 slog;deploy 侧结构零改动,备份靠 `compose exec/run` 一次性容器(db 镜像自带 pg_dump、minio 镜像自带 mc)。见 spec `docs/specs/2026-09-30-backup-observability-design.md`(§2 三个选型 D1/D2/D3、§4 脚本七步、§5 验收四条)。
|
||||||
|
|
||||||
|
**Tech Stack:** Go 1.24(gin 1.11 / pgx 5.8 / stdlib log-slog;**禁新增第三方依赖**)、bash + docker compose + mc、GitHub Actions(deploy 仓 validate.yml 已有 compose config 步骤可仿)。
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 工作目录:`crearte-monorepo/crearte-server/src`(Go 源码根)与 `crearte-monorepo/crearte-deploy`。
|
||||||
|
- 分支:server 与 deploy 各从 `master` 切 `feat/backup-observability`;不碰 crearte。**master 上禁直接提交**(两仓 AGENTS.md);合回一律 `--no-ff`。
|
||||||
|
- **Go 命令一律走容器**(AGENTS.md 本地约定,proxy.golang.org 不可达):
|
||||||
|
`docker run --rm -v <repo>/crearte-server:/work -v crearte_gomod:/go/pkg/mod -v crearte_gocache:/gocache -e GOCACHE=/gocache -e GOPROXY=https://goproxy.cn,direct -e GOSUMDB=sum.golang.google.cn -w /work/src golang:1.24-alpine sh -c "<cmd>"`(`<repo>`=/root/.openclaw/workspace/coder/crearte-monorepo;首次冷 1–3 分钟用 background+poll)。
|
||||||
|
- slog 迁移**机械映射**(spec §3.1):`log.Printf("X: k=%s e=%v", …)` → `slog.Error("X", "k", …, "error", err)`;原短语/键名不变;`error=` 一律 `*error` 值入 attr;不改任何调用语义。
|
||||||
|
- `/metrics` 手写 exposition(D1):文本格式 v0.0.4,`# HELP`/`# TYPE` 行齐全,指标名与 label 集合严格按 spec §3.2;`/healthz` 请求既不记日志也不计指标。
|
||||||
|
- 备份脚本:只删 `^\d{8}T\d{6}Z$` 形态目录;`restore-drill.sh` 只连 `db-test`(127.0.0.1:5434→内部 5432,debug profile),绝不指向 prod;凭据从 `.env` 读,不落命令行。
|
||||||
|
- CHANGELOG:server `docs/CHANGELOG.md` 追加 `## [0.12.0] - 2026-09-30`(顶版 0.11.1 之上)、deploy 追加 `## [0.6.0] - 2026-09-30`(顶版 0.5.1 之上);英文行+中文行连续,条目间空行。
|
||||||
|
- 完成定义:Task 4 六腿全绿(容器四连 / TEST_DATABASE_URL 集成层 / observability 新单测 / 实栈 prod 冒烟抓 /metrics+JSON 日志 / backup+restore-drill 真跑 / full-loop e2e 不回归),Task 5 合主推送。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: server — slog 基座(config 解析 + 26 处迁移 + 请求日志中间件)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/internal/config/config.go`(+ `LogFormat`/`LogLevel` 字段与解析)
|
||||||
|
- Create: `src/internal/observability/logging.go`(slog 初始化 helper + `RequestLogger` 中间件)
|
||||||
|
- Create: `src/internal/observability/logging_test.go`
|
||||||
|
- Modify: `src/internal/api/router.go`(挂 `RequestLogger`;`gin.Recovery` 保留)
|
||||||
|
- Modify: `src/cmd/serve.go`(启动时初始化 slog;启动行转 slog)
|
||||||
|
- Modify: handler/service/api 其余 10 文件(机械迁移清单:`internal/handler/{games,admin,uploads,submissions,auth,bundlekey,reactions}.go`、`internal/service/{content,cleanup}.go`、`internal/api/{admin,auth}.go`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `observability.InitLogging(format, level string) error`;`observability.RequestLogger() gin.HandlerFunc`;`Config.LogFormat` / `Config.LogLevel`
|
||||||
|
- 测试基建约定:新单测写临时文件 + `slog.New(slog.NewTextHandler(file, …))` 式注入,或直接断言 `InitLogging` 后 `slog.Default()` 输出——用 `httptest`+gin engine 打请求断言行内容。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败的单测** `observability/logging_test.go`:
|
||||||
|
1. `InitLogging("json","warn")` 后以 `slog.Info("x")` 不输出、`slog.Warn` 输出且 `jq` 可解析(用 `bytes.Buffer` 接管 handler 输出断言,别起子进程)。
|
||||||
|
2. `InitLogging("bogus",…)` / `InitLogging(…,"bogus")` 返回 error。
|
||||||
|
3. `RequestLogger`:gin engine(含 `/healthz` 与一个业务路由 + 一个不存在路径)经 httptest 打四类请求 → 断言:业务请求有 `msg=http_request method=… route=/api/games… status=… duration_ms=… ip=…` 行;`/healthz` 无行;未匹配路由 `route=nomatch`;`status>=500` 级别为 ERROR(`level=ERROR`/`"level":"ERROR"`)。
|
||||||
|
Run(容器): `go test ./internal/observability/ -run 'Logging|RequestLogger' -v` → Expected: FAIL(包不存在 → `go vet` 或 build 错误即红)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 实现** `config.go` 增 `LogFormat`/`LogLevel`(`LOG_FORMAT` 默认 `text`、`LOG_LEVEL` 默认 `info`,非法值 `fmt.Errorf` 报错——仿 `parseAllowedOrigins` 的返回 err 风格,`Load()` 里接线)。
|
||||||
|
- [ ] **Step 3: 实现** `observability/logging.go`:`InitLogging` 组装 `slog.NewTextHandler`/`NewJSONHandler(os.Stderr, &HandlerOptions{Level:…})` 并 `slog.SetDefault`;`RequestLogger` 按 Interfaces 断言实现(`c.FullPath()` 空→`nomatch`;`path=="/healthz"` 直接 `c.Next()` 不记录)。
|
||||||
|
- [ ] **Step 4: router.go** 在 CORS 之后 `engine.Use(observability.RequestLogger())`(Recovery 之前挂,保证 panic 路径也有行;`gin.Recovery()` 不动)。
|
||||||
|
- [ ] **Step 5: 迁移 26 处 log.Printf**(`grep -rn 'log\.' src --include='*.go' | grep -v _test` 清零 stdlib `log` import;serve.go 启动行 → `slog.Info("crearte-server listening", "port", cfg.Port, "storage", storageEnabled)`)。
|
||||||
|
- [ ] **Step 6: 容器四连** `gofmt -l . && go vet ./... && go build ./... && go test ./...` → 全绿;Step 1 新测试 PASS。
|
||||||
|
- [ ] **Step 7: 提交** `git add -A && git commit -m "feat(observability): slog structured logging + per-request log middleware (LOG_FORMAT/LOG_LEVEL)"`(server 仓 feat/backup-observability)。
|
||||||
|
|
||||||
|
### Task 2: server — /metrics + 权衡记录 + CHANGELOG
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/internal/observability/metrics.go` + `metrics_test.go`
|
||||||
|
- Modify: `src/internal/observability/logging.go`(计时点并入指标记录——或独立 `Metrics` 中间件,二选一,实现者定,测试覆盖为准)
|
||||||
|
- Modify: `src/internal/api/router.go`(`RouteMetrics = "/metrics"` 注册;`Deps` 增 `Metrics http.Handler` 或 `Pool *pgxpool.Pool` 可选字段——**注意现有 router_test 构造 Deps 的地方要兼容零值**)
|
||||||
|
- Modify: `docs/README.md`(「已知权衡:限流单实例」节)、`docs/CHANGELOG.md`(0.12.0)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `observability.NewRegistry()`;`registry.Observe(method, route string, status int, seconds float64)`;`registry.Expose() string`(或 `WriteTo(io.Writer)`);`GET /metrics` 文本。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败的单测** `metrics_test.go`:httptest 经真 router(无池 Deps)打 `GET /api/games`×2、`POST /login` 404×1 → `GET /metrics` 断言:`# TYPE crearte_http_requests_total counter` 行、`crearte_http_requests_total{method="GET",route="/api/games",status="200"} 2`、`nomatch` 桶有行、`_duration_seconds_sum`+`_count` 成对、`go_goroutines` 存在、pgpool 族**不出现**(nil pool 省略)。再断言 `/metrics` 自身与 `/healthz` 不计入(先取计数、抓一轮 scrape、计数不变)。
|
||||||
|
- [ ] **Step 2: 实现** metrics.go(`sync.Mutex` + map key `method|route|status`; Exposition 按指标名字典序输出,label 值本场景集合安全无需转义但注明);router 注册 `/metrics`(Content-Type: `text/plain; version=0.0.4`);pgxpool gauge 从 `Deps.Pool` 可选注入(serve.go 传池;测试传 nil)。
|
||||||
|
- [ ] **Step 3: 容器四连** 同 Task 1 Step 6 → 全绿。
|
||||||
|
- [ ] **Step 4: README 权衡节**(spec §3.3 三点:N× 限额/重启清零/扩缩前置条件)+ CHANGELOG 0.12.0(Added:metrics+请求日志;Changed:slog 迁移与 LOG_FORMAT/LOG_LEVEL;英文行+中文行)。
|
||||||
|
- [ ] **Step 5: 提交** `git commit -m "feat(observability): hand-rolled /metrics exposition with http+go+pgpool families; document single-instance rate-limit tradeoff"`。
|
||||||
|
|
||||||
|
### Task 3: deploy — 备份脚本 + 恢复演练 + runbook + CI(0.6.0)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/backup.sh`、`scripts/restore-drill.sh`、`docs/BACKUP-RUNBOOK.md`
|
||||||
|
- Modify: `.env.example`(BACKUP_DIR/BACKUP_KEEP_DAYS 注释+键)、`README.md`(prod 节链 runbook)、`.github/workflows/validate.yml`(追加 dry-run 步骤)、`docs/CHANGELOG.md`(0.6.0)
|
||||||
|
- 分支:deploy 仓 `feat/backup-observability`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: compose prod 服务名 `db-prod`/`minio-prod`、`.env` 变量 `POSTGRES_PASSWORD`/`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`/`MINIO_BUCKET`、debug profile `db-test`(宿主 127.0.0.1:${PG_TEST_PORT:-5432})
|
||||||
|
- Produces: `backup.sh [--dry-run]`(退出码 0/非 0;stdout 计划/结果)、`restore-drill.sh`(PASS/FAIL 字面输出);产物布局 `$BACKUP_DIR/prod/<UTC 时间戳>/{database.dump,objects/,manifest.txt}` + `prod/latest` 软链。
|
||||||
|
|
||||||
|
- [ ] **Step 1: backup.sh**(bash `set -euo pipefail`,逐步实现 spec §4 七步;`compose()` 封装 `docker compose -f docker-compose.yml --profile prod`;dump 用 `exec -T db-prod pg_dump -U crearte -d crearte -Fc` 重定向到目标文件;mirror 用 `run --no-deps --rm -T minio-prod mc alias set local ... && mc mirror --overwrite --remove local/$MINIO_BUCKET $TARGET`——**注意 minio 镜像无 sh 之外的 coreutils 假设,先 `docker run --rm --entrypoint sh pgsty/minio:latest -c 'mc version'` 探明再写**;manifest 逐表 count 用循环 `psql -At -c "SELECT tablename FROM pg_tables WHERE schemaname='public'"` 拼表名到单条 `SELECT count(*) FROM x UNION ALL …`;修剪 `ls -1 | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | head -n -$KEEP_DAYS` 删除;`latest` 用 `ln -sfn`)。
|
||||||
|
- [ ] **Step 2: dry-run 腿** `--dry-run`:`set -a; . ./.env; set +a` 校验四变量非空 + 打印将执行的命令序列(不碰 docker)。本机先手工验证两态(缺 env 文件→红;.env 齐全→绿)。
|
||||||
|
- [ ] **Step 3: restore-drill.sh**:读 `latest/manifest.txt` → `compose --profile debug` 起 db-test(`docker compose up -d db-test` 若未跑)→ `run --rm` 一次性 postgres:17-alpine 客户端 `pg_restore -h db-test -U crearte -d crearte --clean --if-exists`(drop/recreate 仅限该库)→ 重算逐表行数与 manifest diff → `RESTORE-DRILL: PASS|FAIL` 末行输出。
|
||||||
|
- [ ] **Step 4: runbook**(定时约定 crontab 一行 + systemd timer 备选;两条恢复路径;`mc mirror` 反推对象恢复;3-2-1 与 rclone 可选钩子;已知风险节——同盘、非快照一致性、manifest 漂移,抄 spec §4 末段展开成操作语言)。
|
||||||
|
- [ ] **Step 5: validate.yml 追加步骤**:现有 compose config 步骤后 `cp .env.example .env && printf 'POSTGRES_PASSWORD=…\nMINIO_ROOT_USER=…\nMINIO_ROOT_PASSWORD=…\n' >> .env && bash scripts/backup.sh --dry-run`(CI 无 docker daemon 场景 dry-run 不得触碰 docker 子命令——Step 1 实现顺序保证校验先于 docker 检查)。
|
||||||
|
- [ ] **Step 6: 本机绿** `bash -n scripts/*.sh`、`cd .. && docker compose --profile prod config -q`、workflow 语法对照现有 validate.yml;CHANGELOG 0.6.0 双语;README 链接。
|
||||||
|
- [ ] **Step 7: 提交** `git commit -m "feat(backup): pg_dump+mc-mirror backup with retention + restore drill against db-test; runbook; CI dry-run"`。
|
||||||
|
|
||||||
|
### Task 4: 本机全量验证(控制者亲跑,background+poll)
|
||||||
|
|
||||||
|
- [ ] 4.1 server 容器四连(Task 2 后终态复跑一遍,含全部新旧单测)。
|
||||||
|
- [ ] 4.2 server 集成层:debug profile 起 db-test → `TEST_DATABASE_URL=postgres://crearte:crearte@127.0.0.1:5432/crearte?sslmode=disable go test ./internal/api/ -run Integration -count=1` 容器内跑(网络 `--network host` 或复用 compose 网),期望全 PASS 0 skip(P2 同款守卫)。
|
||||||
|
- [ ] 4.3 prod 实栈冒烟:`compose --profile prod up -d --build` → 灌一轮真实流量(curl /api/games、login、404 路径、`GET /metrics`×2)→ `exec api-prod wget -qO- localhost:8080/metrics` 断言计数吻合;`logs api-prod` 断言 JSON 行(`LOG_FORMAT=json` 需在 .env 或 compose env 生效——验证 compose 是否透传,**若 api-env anchor 无此变量则本任务给 compose 增 `LOG_FORMAT: ${LOG_FORMAT:-text}`、`LOG_LEVEL: ${LOG_LEVEL:-info}` 两行透传**,属 deploy 侧合理补充,记入报告与 CHANGELOG 0.6.0);full-loop:`GO=1 REQUIRED=1 bash scripts/e2e-stack.sh`(端口冲突先停 prod 栈或错峰)。
|
||||||
|
- [ ] 4.4 备份演练:`bash scripts/backup.sh` 全绿 → 产物三件套+latest 链在场;连跑第二次 + `BACKUP_KEEP_DAYS=1` 修剪断言;`bash scripts/restore-drill.sh` → PASS。
|
||||||
|
- [ ] 4.5 收尾:prod 栈 down、4173/4174 释放、台账。
|
||||||
|
|
||||||
|
### Task 5: 合主推送 + wrapper 收尾(控制者)
|
||||||
|
|
||||||
|
- [ ] 5.1 server:`checkout master && pull --ff-only && merge --no-ff feat/backup-observability -m "Merge branch 'feat/backup-observability' — slog + /metrics (P3)" && push && branch -d`。
|
||||||
|
- [ ] 5.2 deploy:同款,merge 信息 `— backup/restore scripts + runbook (P3)`。
|
||||||
|
- [ ] 5.3 wrapper:ROADMAP P3 行 → 完成(双 hash+日期);索引表 P3 spec/plan 行加(已执行);CHANGELOG 0.2.6 双语;commit+push。
|
||||||
|
|
||||||
|
## Self-Review 记录
|
||||||
|
|
||||||
|
- spec §3.1/§3.2/§3.3 → Task 1/2/2-Step4;§4 七步 → Task 3 Step 1-5;§5 四条验收 → Task 4.1-4.4;D1/D2/D3 无未落地项。
|
||||||
|
- 已知不确定点(实现者探明回填):pgsty/minio 镜像内 mc 与 sh 可用性(Task 3 Step 1 先探测);compose 对 LOG_FORMAT 透传(Task 4.3 现场定);router Deps 兼容性(Task 2 Step 2 注意既有 router_test 构造点零值安全)。
|
||||||
|
- 占位符扫描:无 TBD;每步含可执行命令或精确文件行为。
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# P3 备份 + 可观测设计 / Backup & Observability Design
|
||||||
|
|
||||||
|
- **状态**:设计已定(2026-09-30;三个选型向维护者提问未被应答,按推荐默认拍板并记录 §2,维护者可事后推翻)。
|
||||||
|
- **路线图**:P3(第一波末项),依赖 P1 拓扑已定型。
|
||||||
|
- **涉及仓库**:crearte-server(0.11.1 → **0.12.0**)、crearte-deploy(0.5.1 → **0.6.0**);**crearte 前端零改动**。
|
||||||
|
|
||||||
|
## 1. 背景与现状(2026-09-30 实地盘点)
|
||||||
|
|
||||||
|
- prod 栈(`db-prod` postgres:17-alpine / `minio-prod` pgsty/minio / `api-prod` / `web-prod`)数据落命名卷 `pgdata-prod`、`minio-data-prod`,**没有任何备份面**;`api-prod` 无宿主端口映射(仅 `web-prod` 的 8080 反代 `/api/` 与静态),MinIO console 宿主端口 `MINIO_PROD_PORT:-9001`。
|
||||||
|
- server 日志 = 26 处 stdlib `log.Printf` 散在 12 个文件(handler×7、service×2、api 中间件×2、cmd/serve.go×1 等);无请求日志中间件;无 `/metrics`。`gin.New()` 仅挂 `Recovery` + CORS。
|
||||||
|
- 限流器 `internal/api/ratelimit.go`:进程内 `map[ip]*windowCounter` 固定窗口(bundle-key 60/min、login 10/min、register 5/h、upload 10/min、submission 20/h、reaction 30/min),`maxTrackedIPs=10000` 惰性清理——纯单实例语义。
|
||||||
|
- 依赖基线:gin 1.11 / pgx 5.8 / aws-sdk-go-v2;**无 prometheus 依赖**。go.mod `go 1.24.1`。
|
||||||
|
|
||||||
|
## 2. 选型拍板(默认=推荐项)
|
||||||
|
|
||||||
|
| # | 岔路 | 决定 | 理由 / 放弃项 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D1 | `/metrics` 实现 | **手写极简 exposition**(零新依赖,Prometheus 文本格式 v0.0.4) | 单实例小站够用;代码可控可测;延续本仓最小依赖风格。放弃 `prometheus/client_golang`(依赖链重,Grafana 生态是未来事)与"不做"(ROADMAP 明列交付物)。 |
|
||||||
|
| D2 | 备份调度 | **deploy 仓 `backup.sh` + 主机 crontab**(runbook 给一行安装示例) | 脚本可手跑、可 CI `--dry-run` 验证、不绑 systemd。放弃栈内 cron sidecar(挂 docker.sock = 攻击面);systemd timer 留作 runbook 备选写法。 |
|
||||||
|
| D3 | 备份落点 | **主机目录 `BACKUP_DIR`(默认 `./backups/prod/<UTC 时间戳>/`)+ 保留最新 `BACKUP_KEEP_DAYS=7` 份** | 防误删/防配置炸;同盘不防磁盘坏——3-2-1 与 rclone 异地钩子写进 runbook 作可选步骤,不配不生效(本轮不引入外部 S3 凭据依赖)。 |
|
||||||
|
|
||||||
|
## 3. server 设计(0.12.0)
|
||||||
|
|
||||||
|
### 3.1 结构化日志(slog)
|
||||||
|
|
||||||
|
- `config.Load()` 新增 `LOG_FORMAT=text|json`(默认 `text`)、`LOG_LEVEL=debug|info|warn|error`(默认 `info`;非法值报错)。`serve.go` 启动时据此配置 `slog.Default`(输出 stderr;JSON 供 prod/容器采集)。
|
||||||
|
- 26 处 `log.Printf` 全量迁移到 `slog`,**机械映射规则**:原 `msg k1=%s k2=%v` → `slog.<lvl>("<msg 前缀短语>", "k1", v1, "k2", v2)`;携带 `error=%v` 的一律 `slog.Error(..., "error", err)`;启动/生命周期一行 `slog.Info`。文案短语与键名不变。
|
||||||
|
- 新增**请求日志中间件**(`internal/observability`):每请求一行 `http_request`,attrs = `method`、`route`(`c.FullPath()`,未匹配路由记 `nomatch`,基数有界)、`status`、`duration_ms`、`ip`;`/healthz` 整体跳过(不记日志**也不计指标**,防 scrape 噪声);`status >= 500` 用 Error 级。
|
||||||
|
- `gin.Recovery()` 保留(panic 兜底),不动。
|
||||||
|
|
||||||
|
### 3.2 `/metrics`(手写)
|
||||||
|
|
||||||
|
- `internal/observability`:并发安全 Registry(mutex + `map[method|route|status]→{count,sumSeconds}`)。中间件一次计时同时喂日志与指标。
|
||||||
|
- 指标族:
|
||||||
|
- `crearte_http_requests_total{method,route,status}`(counter)
|
||||||
|
- `crearte_http_request_duration_seconds_sum/_count{method,route,status}`(counter 语义,够算均值)
|
||||||
|
- `go_goroutines`、`go_memstats_alloc_bytes`(`runtime` 现值 gauge)
|
||||||
|
- `crearte_pgpool_*`(`pgxpool.Stat()`:`max_conns/total_conns/acquired_conns/idle_conns/acquire_count/empty_acquire_count` gauge;pool 为 nil 时省略——纯 mock 测试路由无池)
|
||||||
|
- `GET /metrics` 与 `/healthz` 同口注册(`RouteMetrics="/metrics"`),无 auth、无 CORS 例外。
|
||||||
|
- **暴露边界**:prod 形态 api-prod 宿主零端口 → `/metrics` 仅容器网内可达,抓取配方 `docker compose exec -T api-prod wget -qO- http://127.0.0.1:8080/metrics`。外网暴露 + allowlist/basic-auth 明确不做(未来接 Prometheus 抓取器再开 nginx 转发段)。
|
||||||
|
- 测试路径无池、无真栈:httptest 经真 router 打请求,断言文本行、计数增长、`nomatch` 桶存在。
|
||||||
|
|
||||||
|
### 3.3 限流单实例权衡(记录,不改造)
|
||||||
|
|
||||||
|
server `docs/README.md` 新增「已知权衡」节:多实例部署时每实例独立计数(有效限额 ≈ N×、重启清零、跨实例不共享);`maxTrackedIPs` 上限的内存数量级;扩多实例的前置条件是引入 Redis/网关层共享计数。**只记录现状语义,不改限流器。**
|
||||||
|
|
||||||
|
## 4. deploy 设计(0.6.0)
|
||||||
|
|
||||||
|
- **compose 结构零改动**:备份全走 `docker compose exec` / `run --no-deps` 一次性容器(`db-prod` 自带 pg_dump/pg_restore/psql,unix socket trust 免口令;`minio-prod` 镜像自带 `mc`,凭据经容器 env 注入而非命令行字面量)。
|
||||||
|
- `scripts/backup.sh`:
|
||||||
|
1. `--dry-run`:不碰 docker,仅校验 `.env` 存在、`POSTGRES_PASSWORD`/`MINIO_ROOT_*` 非空(`:?` 守卫)并打印计划——CI 步骤用它。
|
||||||
|
2. 预检:docker 可用、`compose --profile prod config -q`、`db-prod` 存活(`pg_isready`)。
|
||||||
|
3. `pg_dump --format=custom` 流式 → `$BACKUP_DIR/prod/<UTC 时间戳>/database.dump`。
|
||||||
|
4. `mc mirror --overwrite --remove` 桶(`MINIO_BUCKET:-crearte`)→ 同目录 `objects/`。
|
||||||
|
5. 完整性:`pg_restore --list` 读 dump 头。
|
||||||
|
6. manifest:全表**精确行数**(循环 `pg_tables` 逐表 `count(*)`)+ 产物体积。
|
||||||
|
7. `prod/latest` 软链指向本次目录;按目录名排序保留最新 `BACKUP_KEEP_DAYS` 份,只删 `^\d{8}T\d{6}Z$` 白名单形态目录。
|
||||||
|
- `scripts/restore-drill.sh`:latest dump 恢复到 **debug 栈 `db-test`**(drop/recreate 仅限该测试库),逐表行数与 manifest 对比,输出 PASS/FAIL。绝不连 prod 库。
|
||||||
|
- `docs/BACKUP-RUNBOOK.md`:定时约定(crontab 一行 + systemd timer 备选写法);恢复步骤(恢复到现有 prod 库 / 恢复到全新栈两条路径);对象侧 `mc mirror` 反向推送;异地 3-2-1 与 rclone 可选钩子;已知风险(同盘单拷贝、`mc mirror` 非快照一致性——写侧对象上传少可接受、备份窗口内行数漂移 manifest 与 dump 快照的微小差)。
|
||||||
|
- `.env.example` 增 `BACKUP_DIR`/`BACKUP_KEEP_DAYS` 注释示例;README prod 节链 runbook;`validate.yml` 增 `backup.sh --dry-run` 步骤(CI 里先 `cp .env.example .env` 再 append 假值过 `:?` 守卫)。
|
||||||
|
- CHANGELOG 0.6.0(英/中连续行)。
|
||||||
|
|
||||||
|
## 5. 验收标准(本机任务 4 实测)
|
||||||
|
|
||||||
|
1. server:容器四连(gofmt/vet/build/test)+ `TEST_DATABASE_URL` 集成层全绿、0 skip;新单测覆盖指标格式、中间件计数、`nomatch` 桶、日志 JSON 合法(jq)、`LOG_*` 解析。
|
||||||
|
2. 实栈冒烟(prod profile 演练栈):打一轮真实请求 → `docker compose exec` 抓 `/metrics` 计数吻合、JSON 日志行可读;`backup.sh` 全绿产出 dump + objects + manifest;`restore-drill.sh` 对 db-test **PASS**;连跑两次 + `BACKUP_KEEP_DAYS=1` 验证 latest 软链与修剪。
|
||||||
|
3. `backup.sh --dry-run` 无栈可跑(CI 等价);deploy 新 workflow 步骤本机过。
|
||||||
|
4. crearte e2e:stack full-loop 不回归(中间件动过路由)。
|
||||||
|
|
||||||
|
## 6. 非目标
|
||||||
|
|
||||||
|
Prometheus/Grafana 服务端搭建、`/metrics` 外网暴露与鉴权、异地同步落地实现、PITR/WAL 归档、卷快照级备份、限流器改造、dev/debug 栈备份(仅 prod profile)。
|
||||||
Reference in New Issue
Block a user