Files
Alfred/openspec/changes/add-frontend-logger/design.md

225 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 背景
当前项目后端通过 `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 中不如纯文本直观
- 引入 loglevelAPI 简洁但无原生 Sink 模式,无法优雅集成 antd notification
- 引入 consola功能全但与后端 pino 风格不一致,增加理解成本
## 需求
| 需求 | 验收标准 |
| ---- | -------- |
| 统一的前端 Logger 接口 | `src/web/utils/logger.ts` 导出 `Logger` 接口,含 `debug/info/warn/error/child/setLevel` 方法 |
| Console 输出 | 所有级别日志均输出到 consoledebug→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.warningerror→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 一处)首批改造
## 待解决问题
| 状态 | 问题 | 所需决策 |
| ---- | ---- | -------- |
| 无 | 无待解决问题。 | 无需决策 |