docs: require explicit logger env
This commit is contained in:
170
docs/superpowers/specs/2026-06-17-explicit-env-logger-design.md
Normal file
170
docs/superpowers/specs/2026-06-17-explicit-env-logger-design.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Logger Env 显式传递设计
|
||||
|
||||
## 背景
|
||||
|
||||
`server/src` 正在改造成可以在 Cloudflare 运行的服务。Cloudflare Worker 的 bindings 由入口函数参数 `env` 提供,不能依赖 Node.js 进程环境、模块级全局变量或默认空对象。当前代码已经在大范围迁移 `runtimeEnv`,但日志模块仍存在一类独立风险:logger 会在 import 阶段用空对象初始化,或者通过默认值隐藏调用方没有传入真实 `Env` 的问题。
|
||||
|
||||
当前发现的核心残留位于 `server/src/external/logtail/logtailUtils.ts`:
|
||||
|
||||
```ts
|
||||
const pinoLogger = initLogger({}, {});
|
||||
export const logger = createLoggerStructure(pinoLogger, {});
|
||||
```
|
||||
|
||||
这会让上游即使没有从 Cloudflare 入口传入 `env`,也能得到一个看似可用的 logger。结果是 `NODE_ENV`、`AXIOM_TOKEN` 等配置可能静默缺失,类型检查也无法暴露调用链断点。
|
||||
|
||||
## 目标
|
||||
|
||||
- 所有运行时 logger 初始化必须显式接收 `env: Env`。
|
||||
- `Env` 参数不允许默认值,不允许 `env?: Env`,不允许 `env = {} as Env`。
|
||||
- `logtailUtils.ts` 不导出模块级 `logger` singleton。
|
||||
- `server/src` 不从 `logtailUtils.ts` 导入全局 `logger`。
|
||||
- 调用方必须从 Cloudflare/Hono/cron/worker/Node 显式入口逐层传递 `env` 或传递已由入口 env 创建出的 `ctx.logger`。
|
||||
- 类型检查和单元扫描测试能阻止后续重新引入空对象 env 或全局 logger。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不在本次任务中重写全部 Cloudflare 部署结构。
|
||||
- 不改变 pino、Axiom、Logtail 或现有 logger 输出格式。
|
||||
- 不把所有业务函数都强制新增 `env: Env`;如果已有 `ctx.logger`,优先复用 logger。
|
||||
- 不处理 `better-auth` 包自身导出的 `logger`,它不是本项目 `logtailUtils.ts` 的全局 logger。
|
||||
- 不整理与 logger/env 无关的既有大规模未提交改动。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用“logger factory only,加扫描测试”的方案。
|
||||
|
||||
备选方案比较:
|
||||
|
||||
- 只删除当前 `initLogger({}, {})`:改动最小,但无法防止其他文件继续导入或新增全局 `logger`。
|
||||
- logger factory only,加扫描测试:删除 import-time logger,保留现有 `createLogger(env)` 和 `createDualLogger(env)` 模式,并用单元测试禁止回退。改动聚焦,能满足本次强约束。
|
||||
- AST 级全仓 Env 规则:最严谨,但当前 repo 已有大量 env threading 改动,成本和误伤风险更高。可以作为后续强化,不作为本轮必要条件。
|
||||
|
||||
因此推荐第二种方案。
|
||||
|
||||
## 架构边界
|
||||
|
||||
`Logger` 应被视为依赖 Cloudflare bindings 的入口级对象,而不是模块级 singleton。
|
||||
|
||||
合法来源:
|
||||
|
||||
- Cloudflare `fetch(request, env, ctx)`。
|
||||
- Hono `c.env`。
|
||||
- cron/worker/Node 本地入口显式接收的 `initialEnv: Env` 或 `env: Env`。
|
||||
- 由上述 `env` 创建并传入上下文的 `ctx.logger`。
|
||||
|
||||
非法来源:
|
||||
|
||||
- `initLogger({}, {})`。
|
||||
- `createLoggerStructure(..., {})`。
|
||||
- `export const logger = ...`。
|
||||
- 函数参数 `env: Env = {} as Env`。
|
||||
- 可选参数 `env?: Env` 后再用 `{}` fallback。
|
||||
|
||||
## 组件改造
|
||||
|
||||
### `logtailUtils.ts`
|
||||
|
||||
该模块应只提供 factory 和类型:
|
||||
|
||||
```ts
|
||||
export const createLogger = (env: Env) =>
|
||||
createLoggerStructure(initLogger({}, env), env);
|
||||
|
||||
export const createDualLogger = (env: Env) =>
|
||||
createLoggerStructure(initLogger({ mode: "dual" }, env), env);
|
||||
```
|
||||
|
||||
`createLoggerStructure` 的 `env` 参数使用 `Env`,不使用 `Partial<Env>`,也不提供默认值。删除模块级 `pinoLogger` 和 `export const logger`。
|
||||
|
||||
### `initLogger.ts`
|
||||
|
||||
`initLogger` 收紧为:
|
||||
|
||||
```ts
|
||||
export const initLogger = (options: InitLoggerOptions, env: Env) => { ... }
|
||||
```
|
||||
|
||||
可选配置继续通过真实 `env` 判断,例如 `env.AXIOM_TOKEN`。缺少可选 token 时不创建 Axiom stream,但不能通过空对象默认值制造这个状态。
|
||||
|
||||
### 调用点
|
||||
|
||||
从 `@/external/logtail/logtailUtils.js` 导入 `logger` 的 `server/src` 文件必须逐个改造:
|
||||
|
||||
- 已有 `ctx.logger` 的位置使用 `ctx.logger`。
|
||||
- 已有 `env: Env` 或 `c.env` 的位置局部创建 `const logger = createLogger(env)`。
|
||||
- 没有 `env` 的位置向上游补 `env: Env`,直到接到真实入口。
|
||||
|
||||
测试文件如果需要 logger,应使用测试环境显式 `Env` 创建,或 mock `createLogger`,不能依赖生产模块导出的 singleton。
|
||||
|
||||
## 数据流
|
||||
|
||||
请求路径:
|
||||
|
||||
```text
|
||||
Cloudflare fetch(request, env, ctx)
|
||||
-> app.fetch(request, env, ctx)
|
||||
-> Hono c.env
|
||||
-> baseMiddleware/createLogger(c.env)
|
||||
-> ctx.logger
|
||||
-> service/helper 使用 ctx.logger 或显式 env
|
||||
```
|
||||
|
||||
后台路径:
|
||||
|
||||
```text
|
||||
Cloudflare queue/cron env 或 Node/Bun 显式 initialEnv
|
||||
-> startWorkers/startCron/startNodeServer(env)
|
||||
-> createLogger(env)
|
||||
-> createWorkerContext({ ..., logger })
|
||||
-> job/service/helper
|
||||
```
|
||||
|
||||
任何 logger 初始化都必须能从参数链追溯到入口 `env`。
|
||||
|
||||
## 错误处理
|
||||
|
||||
logger 初始化不应因为缺少可选配置而抛错。例如 `AXIOM_TOKEN` 缺失时可以只使用 stdout/dev stream。这个判断必须基于真实传入的 `env`。
|
||||
|
||||
必填配置仍应通过显式 helper 校验,例如 `requireEnv(env, key, context)`。错误消息包含缺失 key 和调用场景,不输出 secret value。
|
||||
|
||||
如果实现时发现调用链无法提供 `Env`,正确处理方式是补齐上游参数或改用已存在的 `ctx.logger`,不是重新引入默认值。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
新增或扩展单元扫描测试,覆盖 `server/src`:
|
||||
|
||||
- 禁止 `env: Env = ...`。
|
||||
- 禁止 `env?: Env`。
|
||||
- 禁止 `env = {} as Env`。
|
||||
- 禁止 `initLogger({}, {})`。
|
||||
- 禁止 `createLoggerStructure(..., {})`。
|
||||
- 禁止 `logtailUtils.ts` 导出 `logger`。
|
||||
- 禁止 `server/src` 从 `@/external/logtail/logtailUtils` 导入 `logger`。
|
||||
|
||||
实现完成后运行:
|
||||
|
||||
```bash
|
||||
cd server && bun ts
|
||||
cd server && bun test:unit
|
||||
```
|
||||
|
||||
如果测试依赖本地服务或环境变量失败,应记录具体失败命令和错误,不把未运行或失败的验证报告为通过。
|
||||
|
||||
## 成功标准
|
||||
|
||||
- `server/src/external/logtail/logtailUtils.ts` 不再有 `initLogger({}, {})`。
|
||||
- `server/src/external/logtail/logtailUtils.ts` 不再导出全局 `logger`。
|
||||
- `server/src` 不再从本项目 `logtailUtils.ts` 导入全局 `logger`。
|
||||
- `initLogger`、`createLoggerStructure`、`createLogger`、`createDualLogger` 都要求显式 `env: Env`。
|
||||
- 扫描测试能防止默认 env 和全局 logger 回归。
|
||||
- 类型检查和单元测试完成,或明确记录无法通过的外部原因。
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
- 风险:部分后台代码当前没有 `env` 参数,只能通过全局 logger 记录日志。
|
||||
缓解:优先使用已有 `ctx.logger`;没有上下文时沿入口调用链补 `env: Env`。
|
||||
- 风险:测试代码导入了生产全局 `logger`。
|
||||
缓解:测试改为显式 env logger 或 mock factory,不保留生产 singleton。
|
||||
- 风险:扫描测试误伤第三方 `logger` 名称。
|
||||
缓解:规则限定在 `@/external/logtail/logtailUtils`、相对路径 `external/logtail/logtailUtils` 和本文件导出,不禁止 `better-auth` 的 `logger`。
|
||||
Reference in New Issue
Block a user