From 0c0cfd04850d3089a311174431a1dd944f07f0e3 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 11 Jun 2026 23:29:26 +0800 Subject: [PATCH] docs(specs): add P0 production readiness design specification --- ...26-06-11-p0-production-readiness-design.md | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-11-p0-production-readiness-design.md diff --git a/docs/superpowers/specs/2026-06-11-p0-production-readiness-design.md b/docs/superpowers/specs/2026-06-11-p0-production-readiness-design.md new file mode 100644 index 0000000..c347fe3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-p0-production-readiness-design.md @@ -0,0 +1,230 @@ +# P0: 生产可用性特性设计 + +日期: 2026-06-11 +状态: Approved + +## 概述 + +5 个独立特性,使 lolly 达到生产可部署状态。所有特性互不依赖,可完全并行开发。 + +--- + +## 1. CORS 中间件 + +### 配置 + +在 `security.cors` 下新增 `CORSConfig` 结构体,server 级别配置: + +```yaml +security: + cors: + enabled: true + allowed_origins: ["https://example.com", "https://api.example.com"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["Content-Type", "Authorization", "X-Request-ID"] + expose_headers: ["X-Total-Count"] + allow_credentials: true + max_age: 3600 +``` + +字段说明: +- `enabled`: 启用开关(默认 false) +- `allowed_origins`: 允许的源列表,支持 `"*"` 通配(与 `allow_credentials: true` 互斥,遵循 CORS 规范) +- `allowed_methods`: 允许的 HTTP 方法 +- `allowed_headers`: 允许的请求头 +- `expose_headers`: 允许前端读取的响应头 +- `allow_credentials`: 是否允许发送 Cookie +- `max_age`: preflight 缓存时间(秒),默认 0 + +### 文件 + +| 文件 | 操作 | +|------|------| +| `internal/middleware/cors/cors.go` | 新建:CORS 中间件实现 | +| `internal/middleware/cors/cors_test.go` | 新建:单元测试 | +| `internal/config/security_config.go` | 修改:添加 `CORSConfig` 结构体 | +| `internal/config/defaults.go` | 修改:CORS 默认值 +| `internal/config/validate.go` | 修改:CORS 验证(origins+credentials 互斥检查) +| `internal/server/middleware_builder.go` | 修改:注册 CORS 中间件(步骤 7.5,SecurityHeaders 之后、ErrorIntercept 之前) + +### 行为 + +1. 非 CORS 请求(无 `Origin` 头):直接 pass-through +2. Preflight (OPTIONS):返回 204 + CORS 头,不进入后续 handler +3. 实际请求:调用 `next(ctx)` 后添加 CORS 响应头 +4. Origin 不匹配:不加 CORS 头,浏览器阻止跨域 + +--- + +## 2. Request-ID 传播 + +### 配置 + +无需额外配置,默认启用。 + +### 文件 + +| 文件 | 操作 | +|------|------| +| `internal/middleware/requestid/requestid.go` | 新建:Request-ID 中间件 | +| `internal/middleware/requestid/requestid_test.go` | 新建:单元测试 | +| `internal/proxy/headers.go` | 修改:`SetForwardedHeaders` 添加 X-Request-ID 传播 | +| `internal/server/middleware_builder.go` | 修改:注册为第一个中间件(AccessLog 之前) + +### 行为 + +1. 检查入站 `X-Request-ID` 请求头 +2. 有值 → 复用(信任下游),存入 `ctx.SetUserValue("request_id", id)` +3. 无值 → 生成 UUID v4,存入 `ctx.SetUserValue` +4. 始终在响应中设置 `X-Request-ID` 头 +5. 代理转发时,`SetForwardedHeaders` 自动传播 `X-Request-ID`(从 `ctx.UserValue("request_id")` 读取) + +### 与现有代码的关系 + +`internal/variable/builtin.go` 已有 `$request_id` 变量,会从 `ctx.UserValue("request_id")` 读取。中间件在请求早期设置此值后,变量系统、access log、proxy header forwarding 都能正确使用。 + +--- + +## 3. /healthz + /readyz 端点 + +### 配置 + +在 `monitoring` 下新增: + +```yaml +monitoring: + healthz: + enabled: true + path: "/healthz" + readyz: + enabled: true + path: "/readyz" +``` + +字段说明: +- `enabled`: 默认 true(开箱即用) +- `path`: 可自定义路径 + +### 文件 + +| 文件 | 操作 | +|------|------| +| `internal/server/healthz.go` | 新建:healthz/readyz handler | +| `internal/server/healthz_test.go` | 新建:单元测试 | +| `internal/config/monitoring_config.go` | 修改:添加 `HealthzConfig`、`ReadyzConfig` | +| `internal/config/defaults.go` | 修改:默认值 +| `internal/server/server.go` | 修改:注册端点(三种模式都需要) | + +### 行为 + +**healthz(存活探针)**: +- GET → 200 `{"status":"ok"}` +- 无任何依赖检查,只要进程活着就返回 200 + +**readyz(就绪探针)**: +- GET → 200 `{"status":"ready"}` 或 503 `{"status":"not ready","reasons":["no healthy upstreams"]}` +- 检查条件:至少有一个 server 已启动 + 至少有一个 upstream 目标可用(如果有配置 proxy 的话) +- 无 proxy 配置的纯静态文件服务器永远返回 200 + +### 注册位置 + +与 status/pprof 同级: +- Single 模式:`locationEngine.AddExact` +- VHost/Multi 模式:`router.GET` + +默认启用,不需要 IP 白名单(K8s 探针来自 kubelet)。 + +--- + +## 4. 环境变量插值 + +### 语法 + +仅 `${ENV_VAR}` 花括号语法,与 lolly 自身 `$variable` 系统无歧义。 + +缺失环境变量时保留原样(`${MISSING_VAR}` 不展开)。 + +### 文件 + +| 文件 | 操作 | +|------|------| +| `internal/config/env.go` | 新建:`ExpandEnv(data []byte) []byte` 函数 | +| `internal/config/env_test.go` | 新建:单元测试 | +| `internal/config/config.go` | 修改:`Load()` 和 `processIncludes()` 中调用 `ExpandEnv` | + +### 实现 + +正则 `\$\{([^}]+)\}` 匹配: +- 匹配到 → `os.Getenv(key)` +- 环境变量存在 → 替换为值 +- 环境变量不存在 → 保留 `${key}` 原样 + +调用位置: +1. `Load()`: `os.ReadFile` 之后、`yaml.Unmarshal` 之前 +2. `processIncludes()`: 每个被 include 文件 `os.ReadFile` 之后、`yaml.Unmarshal` 之前 + +### 示例 + +```yaml +servers: + - listen: "${LISTEN_ADDR}:8080" + ssl: + cert: "${SSL_CERT_PATH}" + key: "${SSL_KEY_PATH}" + security: + auth: + users: + - name: admin + password: "${ADMIN_PASSWORD_HASH}" +``` + +--- + +## 5. CI/CD 流水线 + +### 配置 + +`.github/workflows/ci.yml` 单文件,push to master + PR 触发。 + +### Jobs + +| Job | 依赖 | 步骤 | +|-----|------|------| +| `lint` | 无 | gofumpt 检查 → golangci-lint | +| `test` | 无 | `go test -race ./internal/...` | +| `build` | lint + test | 多平台静态构建(linux/amd64, linux/arm64, darwin/amd64, darwin/arm64) | +| `docker` | build(仅 push/tag) | docker build + push | + +### 文件 + +| 文件 | 操作 | +|------|------| +| `.github/workflows/ci.yml` | 新建:GitHub Actions CI 流水线 | + +### 约束 + +- Go 1.26 +- `CGO_ENABLED=0` +- E2E 测试需要 Docker service(testcontainers) +- 使用 `make fmt`、`make lint`、`make test` 命令 + +--- + +## 依赖关系 + +``` +CORS ──────┐ +Request-ID ┤── 全部独立,可并行 +healthz ───┤ +env interp ┤ +CI/CD ─────┘ +``` + +## 提交策略 + +每个特性一个独立 commit: +1. `feat(middleware/cors): add CORS middleware with server-level config` +2. `feat(middleware/requestid): add request ID generation and propagation` +3. `feat(server): add /healthz and /readyz endpoints for k8s probes` +4. `feat(config): add ${ENV_VAR} interpolation in YAML config` +5. `ci: add GitHub Actions CI pipeline`