feat: 前端统一 Logger 模块 — 接口、双流 Sink、ESLint 规则、测试
This commit is contained in:
2
openspec/changes/add-frontend-logger/.openspec.yaml
Normal file
2
openspec/changes/add-frontend-logger/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: fast-drive
|
||||
created: 2026-06-01
|
||||
224
openspec/changes/add-frontend-logger/design.md
Normal file
224
openspec/changes/add-frontend-logger/design.md
Normal file
@@ -0,0 +1,224 @@
|
||||
## 背景
|
||||
|
||||
当前项目后端通过 `src/server/logger.ts` 提供了统一的 `Logger` 接口和四种实现(PinoLoggerWrapper、ConsoleFallbackLogger、NoopLogger、MemoryLogger),通过依赖注入在运行时代码中使用,并通过 ESLint 的 `no-restricted-syntax` 规则禁止 `src/server/**/*.ts` 直接使用 `console.*`。
|
||||
|
||||
前端目前没有统一的日志处理。唯一的 console 使用在 `ErrorBoundary.tsx:22` 的 `console.error`,业务错误反馈通过各组件零散调用 `App.useApp().message.error()` 完成。ESLint 对前端 `console.*` 没有任何限制。
|
||||
|
||||
用户希望:
|
||||
1. 构造统一的前端日志工具,镜像后端的接口设计和多实现模式
|
||||
2. 业务错误(warn 及以上)同时通过 antd notification 和 console 输出
|
||||
3. 避免前端调试日志散落为原始 `console.*` 调用
|
||||
4. ESLint 按前后端分区管理 console 限制
|
||||
|
||||
## 讨论记录
|
||||
|
||||
- **已确认结论**:自己封装前端 Logger,不引入第三方日志库(loglevel / consola 等)。理由:后端已有成熟的接口+多实现模式可直接镜像,核心逻辑不到 100 行,引入库属于过度工程
|
||||
- **用户偏好**:
|
||||
- notification 仅展示 warn 及以上的业务错误信息,debug/info 不给用户看,写入文档作为开发引导
|
||||
- `child()` 作用域日志首批实现
|
||||
- `data` 参数不做 JSON 序列化,直接透传给 console 以保留 Error 堆栈
|
||||
- 生产环境(`import.meta.env.PROD`)屏蔽 warn 以下级别的日志输出
|
||||
- Logger 前缀 `[Alfred]` 硬编码在代码中
|
||||
- **约束**:
|
||||
- 不新增依赖
|
||||
- 符合 `docs/development/frontend.md` 的前端规约
|
||||
- ESLint 保持单文件 + file glob 分区模式
|
||||
- **被否决方案**:
|
||||
- 引入 pino 浏览器版:体积偏大(~15KB gzip),且 pino 的 JSON 结构化在浏览器 DevTools 中不如纯文本直观
|
||||
- 引入 loglevel:API 简洁但无原生 Sink 模式,无法优雅集成 antd notification
|
||||
- 引入 consola:功能全但与后端 pino 风格不一致,增加理解成本
|
||||
|
||||
## 需求
|
||||
|
||||
| 需求 | 验收标准 |
|
||||
| ---- | -------- |
|
||||
| 统一的前端 Logger 接口 | `src/web/utils/logger.ts` 导出 `Logger` 接口,含 `debug/info/warn/error/child/setLevel` 方法 |
|
||||
| Console 输出 | 所有级别日志均输出到 console(debug→log, info→log, warn→warn, error→error),带有 `[Alfred:LEVEL]` 前缀和可选附加数据 |
|
||||
| antd notification 集成 | `useLogger()` hook 自动将 warn 映射为 `message.warning()`,error 映射为 `message.error()`;debug/info 不触发 notification |
|
||||
| 生产环境静默 | `import.meta.env.PROD` 时 debug/info 不输出到 console 也不触发 notification |
|
||||
| 作用域日志 | `child(bindings)` 返回带绑定的子 Logger,日志消息自动追加 `[key=value]` 后缀 |
|
||||
| MemoryLogger 测试替身 | 提供 `createMemoryLogger()`,将日志条目存储在内存数组中以供测试断言 |
|
||||
| NoopLogger 测试替身 | 提供 `createNoopLogger()`,静默丢弃所有日志 |
|
||||
| ESLint 前端 console 禁止 | `eslint.config.js` 新增规则:`src/web/**/*.{ts,tsx}` 禁止 `console.*`,`logger.ts` 除外 |
|
||||
| Error.data 透传 | `logger.error("失败", { error: someError })` 保留 Error 对象的堆栈信息,不做 JSON 序列化 |
|
||||
| 前缀硬编码 | 日志前缀固定为 `[Alfred]`,不使用模块路径检测 |
|
||||
| 开发文档补充 | `docs/development/frontend.md` 新增日志模块章节,明确 notification 仅用于 warn+error 的红线 |
|
||||
|
||||
## 目标 / 非目标
|
||||
|
||||
**目标:**
|
||||
- 建立与后端一致的 `Logger` 接口和实现模式
|
||||
- 提供 `useLogger()` React hook,在组件内自动集成 antd notification
|
||||
- 通过 ESLint 强制前端使用统一 Logger,杜绝散落的 `console.*`
|
||||
- 支持测试替身(MemoryLogger / NoopLogger)用于前端单元测试
|
||||
|
||||
**非目标:**
|
||||
- 不引入任何第三方日志依赖
|
||||
- 不实现前端日志持久化到文件(浏览器无此能力)
|
||||
- 不改造所有现有组件(渐进式迁移,先建工具再逐步替换)
|
||||
- 不修改后端 Logger 接口或 ESLint 后端规则
|
||||
- 不影响 `message.success()` / `message.info()` 等业务成功反馈的调用方式
|
||||
|
||||
## 执行约束
|
||||
|
||||
- **依赖限制**:零新增依赖,纯 TypeScript + React hooks
|
||||
- **约束**:
|
||||
- 遵守 `docs/development/frontend.md` 全部规约
|
||||
- `useLogger()` 内部使用 `App.useApp()` 获取 antd message 实例
|
||||
- 仅 `warn` 和 `error` 级别调用 antd notification
|
||||
- `data` 参数不加工,直接透传
|
||||
- 生产环境 `import.meta.env.PROD` 时 debug/info 静默
|
||||
- **质量门禁**:
|
||||
- `bun test` 全部通过
|
||||
- `bun run lint` 零违规
|
||||
- `bun run typecheck` 零错误
|
||||
- **文档 / 沟通**:
|
||||
- 更新 `docs/development/frontend.md`,新增"日志模块"章节
|
||||
- 新增的 ESLint 规则需在 ESLint 报错信息中包含明确的替代方案(`useLogger()` / `createConsoleLogger()`)
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 范围 | Artifacts / 参考资料 | 预期变更 | 备注 |
|
||||
| ---- | -------------------- | -------- | ---- |
|
||||
| 核心新增 | `src/web/utils/logger.ts` | 新建,Logger 接口 + ConsoleLogger + DefaultLogger + NoopLogger + MemoryLogger + Sink 概念 | 约 150 行 |
|
||||
| React 集成 | `src/web/hooks/use-logger.ts` | 新建,`useLogger()` hook | 约 30 行 |
|
||||
| ESLint | `eslint.config.js` | 新增前端 console 禁止区块,排除 `logger.ts` | 约 15 行 |
|
||||
| 错误边界 | `src/web/components/ErrorBoundary.tsx` | `console.error` 改为 `createConsoleLogger().error()` | 首处改造,只能使用 `createConsoleLogger()` 不可用 `useLogger()` |
|
||||
| 错误边界测试 | `tests/web/components/ErrorBoundary.test.tsx` | 断言字符串从 `"渲染错误:"` 更新为 `"[Alfred:ERROR] 渲染错误"` | 适配新日志格式 |
|
||||
| 开发文档 | `docs/development/frontend.md` | 新增"日志模块"章节 | 描述接口、用法、notification 红线 |
|
||||
| 测试 | `tests/web/utils/logger.test.ts` | 新建,Logger 各实现的单元测试 | 覆盖 ConsoleSink(级别映射/PROD静默/child绑定/data透传)、AntdMessageSink(warn→warning/error→error/debug+info不触发)、MemoryLogger、NoopLogger、setLevel运行时调整、嵌套child追加和覆盖 |
|
||||
| 测试 | `tests/web/hooks/use-logger.test.ts` | 新建,useLogger hook 单元测试 | 覆盖 antd notification 调用映射 |
|
||||
|
||||
## 决策
|
||||
|
||||
| 决策 | 理由 | 已否决替代方案 |
|
||||
| ---- | ---- | -------------- |
|
||||
| 自己封装而非引入库 | 后端已有成熟模式可镜像,核心逻辑不足 100 行;引入库需额外审计和升级维护;项目原则不引入额外依赖 | pino(浏览器)/loglevel/consola |
|
||||
| 裸对象透传而不做 JSON 序列化 | 保留 Error 的 `stack` 属性,DevTools 的 Error 对象交互(点击跳转源代码)是重要调试体验 | `JSON.stringify(data)` |
|
||||
| `useLogger()` 内部使用 `App.useApp()` | 与项目 Ant Design 使用规范一致("需要 message 时在 ConfigProvider 内包裹 App,组件内通过 App.useApp() 获取") | 全局 message 实例 / 独立 Context |
|
||||
| ESLint 保持单文件 + glob 分区 | 前后端共享 TS/import/perfectionist 规则,分文件产生重复;glob 已提供足够精细的作用域控制 | 拆分为 `eslint.server.js` 和 `eslint.web.js` |
|
||||
| 生产环境屏蔽 warn 以下级别 | 减少生产环境的 console 噪音,debug 和 info 是面向开发者的调试日志 | 全部保留 / 全部去除 |
|
||||
| notification 仅 warn + error | 避免过度弹提示干扰用户,debug/info 是开发者调试日志不应暴露给用户 | 全级别都走 notification |
|
||||
|
||||
## 日志模块设计
|
||||
|
||||
### Logger 接口
|
||||
|
||||
```ts
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
interface Logger {
|
||||
debug(message: string, data?: unknown): void;
|
||||
info(message: string, data?: unknown): void;
|
||||
warn(message: string, data?: unknown): void;
|
||||
error(message: string, data?: unknown): void;
|
||||
child(bindings: Record<string, unknown>): Logger;
|
||||
setLevel(level: LogLevel): void;
|
||||
}
|
||||
```
|
||||
|
||||
`data` 为 `unknown`,不做 JSON 序列化,直接透传给 console,保留 Error 堆栈和 DevTools 交互能力。
|
||||
|
||||
### Sink 接口
|
||||
|
||||
```ts
|
||||
interface Sink {
|
||||
write(level: LogLevel, message: string, data: unknown, bindings: Record<string, unknown>): void;
|
||||
}
|
||||
```
|
||||
|
||||
每个 Logger 实现持有零个或多个 Sink。DefaultLogger 默认持有 `ConsoleSink` + `AntdMessageSink`。
|
||||
|
||||
### ConsoleSink
|
||||
|
||||
- 按级别映射:debug/info → `console.log`,warn → `console.warn`,error → `console.error`
|
||||
- 格式:`[Alfred:LEVEL] message`,后跟 data(如有)
|
||||
- `bindings` 追加为 ` [key=value][key=value]` 后缀
|
||||
- 生产环境(`isProduction=true`)时,debug/info 不输出
|
||||
|
||||
### AntdMessageSink
|
||||
|
||||
- 接收 antd `MessageInstance`(来自 `App.useApp()`)
|
||||
- warn → `message.warning(message)`,error → `message.error(message)`
|
||||
- debug/info 不触发任何 notification
|
||||
- `data` 和 `bindings` 不传递给 notification,仅传 `message` 字符串
|
||||
|
||||
### child() 绑定格式
|
||||
|
||||
```ts
|
||||
const logger = useLogger().child({ page: "projects", providerId: "abc" });
|
||||
logger.warn("供应商删除失败");
|
||||
// Console: [Alfred:WARN] 供应商删除失败 [page=projects][providerId=abc]
|
||||
// Notification: message.warning("供应商删除失败")
|
||||
```
|
||||
|
||||
嵌套 child 追加绑定(同 key 覆盖)。
|
||||
|
||||
### 级别过滤
|
||||
|
||||
- Logger 构造函数接收 `isProduction: boolean` 参数
|
||||
- `createConsoleLogger()` 和 `useLogger()` 内部读取 `import.meta.env.PROD`,使生产环境默认屏蔽 debug/info
|
||||
- `setLevel(level)` 允许运行时调整最小输出级别,覆盖 `isProduction` 的默认行为,用于开发调试和线上临时排查
|
||||
|
||||
### 四种实现
|
||||
|
||||
| 实现 | 构造函数 | Sink | 用途 |
|
||||
| ---- | -------- | ---- | ---- |
|
||||
| DefaultLogger | `createDefaultLogger(sinks, isProduction)` | 外部注入 | 生产/开发运行时的双流 Logger (内部类 BaseLogger) |
|
||||
| ConsoleLogger | `createConsoleLogger()` | ConsoleSink | React 组件外(utils/hooks 纯函数)使用 |
|
||||
| NoopLogger | `createNoopLogger()` | 无 | 测试静默 |
|
||||
| MemoryLogger | `createMemoryLogger()` | 内存数组 `entries` | 测试断言,entries 类型 `{ level: LogLevel; message: string; data?: unknown }[]` |
|
||||
|
||||
### 使用场景区分
|
||||
|
||||
```
|
||||
组件内 (有 App.useApp):
|
||||
const logger = useLogger(); // Console + antd notification
|
||||
|
||||
非组件 (utils / hooks 纯函数):
|
||||
const logger = createConsoleLogger(); // 仅 console
|
||||
|
||||
测试:
|
||||
const logger = createMemoryLogger(); // 断言日志内容
|
||||
const logger = createNoopLogger(); // 静默丢弃
|
||||
```
|
||||
|
||||
**重要约束**:`useLogger()` 依赖 `App.useApp()`,仅可在 `<App>` 子树内的组件中使用。`ErrorBoundary` 位于 `<App>` 之上(见 `test-utils.tsx:84`),因此 ErrorBoundary 必须使用 `createConsoleLogger()`,不能使用 `useLogger()`。
|
||||
|
||||
## 执行计划
|
||||
|
||||
1. 创建 `src/web/utils/logger.ts`:定义 `Logger` 接口、`LogLevel` 类型、`Sink` 概念、`ConsoleSink`、`AntdMessageSink`、`BaseLogger`、`NoopLogger`、`MemoryLogger`、工厂函数
|
||||
2. 创建 `src/web/hooks/use-logger.ts`:`useLogger()` hook,组装 BaseLogger + ConsoleSink + AntdMessageSink
|
||||
3. 修改 `eslint.config.js`:新增前端 `console.*` 禁止区块
|
||||
4. 改造 `src/web/components/ErrorBoundary.tsx`:使用 `createConsoleLogger()`
|
||||
5. 创建 `tests/web/utils/logger.test.ts`:Logger 各实现的单元测试
|
||||
6. 创建 `tests/web/hooks/use-logger.test.ts`:useLogger hook 单元测试
|
||||
7. 更新 `docs/development/frontend.md`:新增"日志模块"章节
|
||||
8. 运行 `bun test && bun run lint && bun run typecheck` 验证
|
||||
|
||||
## 验证计划
|
||||
|
||||
| 需求 / 风险 | 验证方式 |
|
||||
| ----------- | -------- |
|
||||
| Logger 接口正确性 | `tests/web/utils/logger.test.ts` 覆盖 debug/info/warn/error 级别的 ConsoleLogger/MemoryLogger/NoopLogger |
|
||||
| 级别过滤 | MemoryLogger 测试:`isProduction=true` 时 debug/info 不记录 |
|
||||
| child() 作用域 | MemoryLogger 测试:child 绑定后日志包含对应键值 |
|
||||
| notification 映射 | `tests/web/hooks/use-logger.test.ts`:warn→message.warning,error→message.error |
|
||||
| notification 不映射 info | use-logger 测试:info 不调用 message 任何方法 |
|
||||
| ESLint 规则生效 | 手动验证:在 `src/web/` 非 logger.ts 文件写入 `console.log` 触发 lint 报错 |
|
||||
| 类型检查 | `bun run typecheck` 零错误 |
|
||||
| Error 堆栈保留 | 手动验证:`logger.error("msg", { error: new Error("test") })` 在 DevTools 中可展开查看堆栈 |
|
||||
| 文档完整性 | 检查 `docs/development/frontend.md` 包含日志模块章节,含 notification 红线说明 |
|
||||
|
||||
## 风险 / 权衡
|
||||
|
||||
- [风险] `useLogger()` 依赖 `App.useApp()`,仅能在 `AntApp` 组件内部使用,不可在 utils 纯函数中调用 → 纯函数使用 `createConsoleLogger()`,文档明确说明两种使用场景
|
||||
- [风险] antd notification 的 `message` 实例在 React 组件外不可用 → 设计上 `useLogger()` 本身就是 React hook,外部场景用 ConsoleLogger 是预期的
|
||||
- [风险] 生产环境屏蔽 debug/info 可能导致线上排查困难 → 可在后续通过 URL 参数或配置开关临时开启,当前不实现以保持简单
|
||||
- [风险] 现有组件渐进式改造可能长期不完成 → ESLint 规则确保不再新增 `console.*` 调用,现有代码中的 `console.*`(仅 ErrorBoundary 一处)首批改造
|
||||
|
||||
## 待解决问题
|
||||
|
||||
| 状态 | 问题 | 所需决策 |
|
||||
| ---- | ---- | -------- |
|
||||
| 无 | 无待解决问题。 | 无需决策 |
|
||||
45
openspec/changes/add-frontend-logger/tasks.md
Normal file
45
openspec/changes/add-frontend-logger/tasks.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## 1. 上下文审查
|
||||
|
||||
- [x] 1.1 阅读 design.md,确认范围、需求、决策、执行约束和待解决问题
|
||||
- [x] 1.2 审查后端 `src/server/logger.ts` 的 Logger 接口和实现模式,确保前端镜像的一致性
|
||||
- [x] 1.3 审查 `eslint.config.js` 当前 rules 结构,确认新增前端 console 禁止规则的位置和写法
|
||||
|
||||
## 2. 核心日志模块
|
||||
|
||||
- [x] 2.1 新建 `src/web/utils/logger.ts`:定义 `LogLevel` 类型、`Logger` 接口(debug/info/warn/error/child/setLevel)、`Sink` 接口
|
||||
- [x] 2.2 实现 `ConsoleSink`:按级别映射到 console.log/warn/error,前缀 `[Alfred:LEVEL]`,data 透传不序列化
|
||||
- [x] 2.3 实现 `NoopLogger`:所有方法空实现,child 返回自身,静默丢弃日志
|
||||
- [x] 2.4 实现 `MemoryLogger`:将日志条目存入 `entries` 数组,用于测试断言
|
||||
- [x] 2.5 实现 `ConsoleLogger`:组合 ConsoleSink,生产环境(`import.meta.env.PROD`)屏蔽 debug/info
|
||||
- [x] 2.6 实现 `AntdMessageSink`:接收 antd MessageInstance,warn→message.warning,error→message.error,debug/info 不触发
|
||||
- [x] 2.7 实现 `DefaultLogger`:组合多个 Sink(ConsoleSink + AntdMessageSink),支持 `child()` 作用域绑定
|
||||
|
||||
## 3. React 集成
|
||||
|
||||
- [x] 3.1 新建 `src/web/hooks/use-logger.ts`:`useLogger()` 内部调用 `App.useApp()` 获取 message 实例,组装 DefaultLogger + ConsoleSink + AntdMessageSink
|
||||
- [x] 3.2 改造 `src/web/components/ErrorBoundary.tsx`:`console.error` 替换为 `createConsoleLogger().error()`
|
||||
|
||||
## 4. ESLint 规则
|
||||
|
||||
- [x] 4.1 在 `eslint.config.js` 新增 `src/web/**/*.{ts,tsx}` 的 `no-restricted-syntax` 规则,禁止 `console.*`,排除 `logger.ts`
|
||||
- [x] 4.2 运行 `bun run lint` 验证规则生效且无其他文件违规
|
||||
|
||||
## 5. 测试
|
||||
|
||||
- [x] 5.1 新建 `tests/web/utils/logger.test.ts`:测试 ConsoleLogger/MemoryLogger/NoopLogger 的 debug/info/warn/error 级别输出
|
||||
- [x] 5.2 测试生产环境静默:MemoryLogger 在 `isProduction=true` 时 debug/info 不记录
|
||||
- [x] 5.3 测试 `child()` 作用域:绑定后的 Logger 在日志消息中携带对应键值,嵌套 child 追加(同 key 覆盖)
|
||||
- [x] 5.4 新建 `tests/web/hooks/use-logger.test.ts`:测试 useLogger 返回含 ConsoleSink 的 Logger 实例,warn/error 正常输出到 console(AntdMessageSink 映射在 logger.test.ts 的 AntdMessageSink describe 中测试)
|
||||
|
||||
## 6. 文档
|
||||
|
||||
- [x] 6.1 更新 `docs/development/frontend.md`,新增"日志模块"章节:Logger 接口、使用方式、notification 红线(仅 warn+error)
|
||||
- [x] 6.2 确认 `docs/development/frontend.md` 新增的日志模块章节包含 notification 红线、使用场景区分和 ErrorBoundary 特殊说明
|
||||
|
||||
## 7. 质量门禁
|
||||
|
||||
- [x] 7.1 运行 `bun test` 确保所有新增和已有测试通过
|
||||
- [x] 7.2 运行 `bun run typecheck` 确保零类型错误
|
||||
- [x] 7.3 运行 `bun run lint` 确保零 lint 违规
|
||||
- [ ] 7.4 手动验证:在 `src/web/` 非 logger.ts 文件临时写入 `console.log("test")` 确认 ESLint 报错
|
||||
- [ ] 7.5 手动验证:在浏览器 DevTools 中调用 `logger.error("msg", { error: new Error("test") })` 确认 Error 堆栈可展开
|
||||
Reference in New Issue
Block a user