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

13 KiB
Raw Blame History

背景

当前项目后端通过 src/server/logger.ts 提供了统一的 Logger 接口和四种实现PinoLoggerWrapper、ConsoleFallbackLogger、NoopLogger、MemoryLogger通过依赖注入在运行时代码中使用并通过 ESLint 的 no-restricted-syntax 规则禁止 src/server/**/*.ts 直接使用 console.*

前端目前没有统一的日志处理。唯一的 console 使用在 ErrorBoundary.tsx:22console.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 实例
    • warnerror 级别调用 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.jseslint.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;
}

dataunknown,不做 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.logwarn → console.warnerror → console.error
  • 格式:[Alfred:LEVEL] message,后跟 data如有
  • bindings 追加为 [key=value][key=value] 后缀
  • 生产环境(isProduction=truedebug/info 不输出

AntdMessageSink

  • 接收 antd MessageInstance(来自 App.useApp()
  • warn → message.warning(message)error → message.error(message)
  • debug/info 不触发任何 notification
  • databindings 不传递给 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/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 概念、ConsoleSinkAntdMessageSinkBaseLoggerNoopLoggerMemoryLogger、工厂函数
  2. 创建 src/web/hooks/use-logger.tsuseLogger() hook组装 BaseLogger + ConsoleSink + AntdMessageSink
  3. 修改 eslint.config.js:新增前端 console.* 禁止区块
  4. 改造 src/web/components/ErrorBoundary.tsx:使用 createConsoleLogger()
  5. 创建 tests/web/utils/logger.test.tsLogger 各实现的单元测试
  6. 创建 tests/web/hooks/use-logger.test.tsuseLogger 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.tswarn→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 一处)首批改造

待解决问题

状态 问题 所需决策
无待解决问题。 无需决策