13 KiB
13 KiB
背景
当前项目后端通过 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.* 没有任何限制。
用户希望:
- 构造统一的前端日志工具,镜像后端的接口设计和多实现模式
- 业务错误(warn 及以上)同时通过 antd notification 和 console 输出
- 避免前端调试日志散落为原始
console.*调用 - 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 接口
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 接口
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() 绑定格式
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/infosetLevel(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()。
执行计划
- 创建
src/web/utils/logger.ts:定义Logger接口、LogLevel类型、Sink概念、ConsoleSink、AntdMessageSink、BaseLogger、NoopLogger、MemoryLogger、工厂函数 - 创建
src/web/hooks/use-logger.ts:useLogger()hook,组装 BaseLogger + ConsoleSink + AntdMessageSink - 修改
eslint.config.js:新增前端console.*禁止区块 - 改造
src/web/components/ErrorBoundary.tsx:使用createConsoleLogger() - 创建
tests/web/utils/logger.test.ts:Logger 各实现的单元测试 - 创建
tests/web/hooks/use-logger.test.ts:useLogger hook 单元测试 - 更新
docs/development/frontend.md:新增"日志模块"章节 - 运行
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 一处)首批改造
待解决问题
| 状态 | 问题 | 所需决策 |
|---|---|---|
| 无 | 无待解决问题。 | 无需决策 |