docs(specs): add P0 production readiness design specification

This commit is contained in:
xfy 2026-06-11 23:29:26 +08:00
parent f33117b940
commit 0c0cfd0485

View File

@ -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.5SecurityHeaders 之后、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 servicetestcontainers
- 使用 `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`