From 2bd177faf63d8ce701c3e50acca7dc1cafaaa143 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 24 Jun 2026 13:28:09 +0800 Subject: [PATCH] Add design spec: structured logging for stability diagnosis (log4js fileSync) --- .../2026-06-24-structured-logging-design.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-24-structured-logging-design.md diff --git a/docs/superpowers/specs/2026-06-24-structured-logging-design.md b/docs/superpowers/specs/2026-06-24-structured-logging-design.md new file mode 100644 index 0000000..c113ed9 --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-structured-logging-design.md @@ -0,0 +1,113 @@ +# 结构化日志系统(稳定性诊断)设计 + +- 日期:2026-06-24 +- 状态:设计(已口头确认,待实现) +- 范围:v1 聚焦**稳定性诊断**——捕获错误与崩溃(尤其 `/api/export-excel` 的 OOM 路径)。 + +## 1. 背景与问题 + +当前服务端只有 3 处 ad-hoc `console.error/warn`,且只记 `err.message`(**丢失堆栈**)。 +输出被 NSSM 原样灌进 `logs/stdout.log`/`stderr.log`,追加、不轮转——导致崩溃堆栈堆积、不可检索。 +诊断 export 的 OOM(单次全量导出峰值 ~1.3GB 堆)时,缺少"哪次导出、多少行、耗时多久"的现场。 + +约束:114 不通外网 → 自包含文件日志,排除 SaaS;单 Node 进程、内部工具、低流量。 + +## 2. 目标 + +在不改动业务逻辑的前提下,新增一层**结构化、分级、同步落盘、自带轮转**的应用日志, +让"出错/崩溃时能查到完整现场"。 + +## 3. 非目标(YAGNI) + +- 不做请求级全量审计 / 用户行为追踪(属"运行可见性/审计",非 v1)。 +- 不做客户端(浏览器)错误采集。 +- 不做集中式日志聚合(ELK 等)。 +- 不重构 `dbConfig` 两处重复(不属于本次范围)。 + +## 4. 方案 + +### 4.1 选型:log4js `fileSync` + +- **`fileSync` appender 同步写**(`fs.appendFileSync`)→ 进程 OOM/被杀前日志已落盘,满足崩溃诊断刚需。 +- 内置按大小轮转(`maxLogSize` + `backups`),无需手写。 +- 分级、category、pattern layout,成熟轻量。 +- 引入依赖 `log4js`(约 ~3 个小间接依赖)。 + +### 4.2 分层 + +``` +应用日志(新增) log4js fileSync → logs/app/app.log(同步 + 10MB×5 轮转) ← 查错误/崩溃 +框架/致命日志 NSSM 的 stdout/stderr(Next 启动、V8 崩溃栈) ← 兜底,保持不动 +``` + +### 4.3 logger 模块 `src/server/logger.ts` + +```ts +import log4js from "log4js"; + +log4js.configure({ + appenders: { + app: { + type: "fileSync", + filename: "logs/app/app.log", + maxLogSize: 10 * 1024 * 1024, // 10MB + backups: 5, + layout: { type: "pattern", pattern: "[%d{ISO8601}] [%p] [%c] %m" }, + }, + }, + categories: { default: { appenders: ["app"], level: "info" } }, +}); + +export const apiLogger = log4js.getLogger("api"); +export const exportLogger = log4js.getLogger("export"); +``` + +- `filename` 用相对路径,`next start`(NSSM 的 `AppDirectory`)/`next dev` 的 cwd 均为项目根,解析正确。 +- 输出样例: + `[2026-06-24T10:30:01.123] [ERROR] [export] Excel export failed · rows=33142 · dur=18234ms · RangeError: Invalid string length\n at ...` + +### 4.4 记什么(诊断聚焦,不给常规成功查询加噪) + +| 事件 | 级别 | 字段 | +|----|----|----| +| production-data 查询失败 | ERROR | workshopNo、完整堆栈 | +| production-data 慢查询(>3s) | WARN | workshopNo、耗时 | +| export 锁被并发占用(429) | WARN | — | +| export 锁超时回收 | WARN | 取代现有 `console.warn` | +| export 开始 / 成功 | INFO | rows、bytes、耗时(OOM 诊断核心线索) | +| export 失败 | ERROR | 完整堆栈、rows、耗时 | +| 进程启动 | INFO | logger ready | + +要点:现有 3 处 `console.error/warn` **全部替换**为 logger 调用;`err.message` **升级为完整 `err.stack`**。 + +### 4.5 路由改造 + +- `production-data/route.ts`:catch 分支用 `apiLogger.error(...)` 带栈 + workshopNo;查询耗时 >3s 时 `apiLogger.warn(...)`。 +- `export-excel/route.ts`:锁占用/回收 `exportLogger.warn`;开始记 `rows`、成功记 `rows/bytes/dur`、失败 `exportLogger.error` 带栈 + dur;保留现有返回逻辑不变。 +- 用一个纯函数 `formatError(err): string` 统一提取 `err.stack ?? String(err)`,便于单测。 + +### 4.6 涉及文件 + +| 文件 | 动作 | +|----|----| +| `src/server/logger.ts` | 新增(log4js 配置 + 导出 logger + `formatError`) | +| `src/app/api/production-data/route.ts` | 改:失败 ERROR(带栈)+ 慢查询 WARN | +| `src/app/api/export-excel/route.ts` | 改:锁/开始/成功/失败 全套日志(带栈) | +| `package.json` | +`log4js` 依赖 | + +## 5. 边界情况 + +- **OOM 落盘**:fileSync 同步写,每行即时 flush,崩溃前已写入。 +- **首次写入**:log4js 自动创建 `logs/app/` 目录与文件。 +- **轮转**:达到 `maxLogSize` 自动滚动为 `app.log.1..5`,更早的删除。 +- **本地 dev vs 114 生产**:均写入项目根下 `logs/app/app.log`;不影响 NSSM 的 stdout/stderr。 + +## 6. 测试 + +- 纯函数 `formatError(err)`:用 vitest 单测(Error→stack、非 Error→字符串、null/undefined 安全)。 +- 路由日志为副作用,以**手验**为准:本地断开 DB 触发 production-data 失败 → 确认 `logs/app/app.log` 有 ERROR 带栈行;触发一次导出 → 确认 INFO(rows/bytes/dur)/ 失败 ERROR 行。 +- 不引入 log4js 行为的单测(第三方,按项目惯例不测)。 + +## 7. 部署注意 + +新增 `log4js` 依赖,114 部署须执行 `npm install`(CLAUDE.local.md 第 3 步),再 `next build` + 重启。