Activepieces 代码审查指南:用 evlog 宽事件与结构化错误重构日志模式
【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces
本篇指南基于 Activepieces 仓库内.agents/skills/review-logging-patterns技能附带的代码审查清单(code-review.md)整理而成,面向需要在 TypeScript/JavaScript 代码库中审查并改进日志与错误处理模式的开发者。你将掌握一套可立即落地的审查流程:扫描console.log泛滥、识别无上下文的泛化错误、为请求处理器补齐请求级日志,并学会把散落的日志与错误转换为 evlog 的「宽事件(wide event)」与「结构化错误(structured error)」,让每条日志都自带可排障的完整上下文。
背景:为什么要审查日志模式
Activepieces 服务端本身已在生产代码中实践了这套日志体系。在 packages/server/api/src/app/helper/logger/index.ts 中可以看到,API 服务通过evlogSetup.init()初始化全局日志门面,支持LOG_LEVEL、LOG_PRETTY、LOG_SAMPLE_RATE_INFO(info 级采样率)、LOG_KEEP_SLOW_MS(慢请求保留阈值)等环境变量,并可通过HYPERDX_TOKEN、AXIOM_TOKEN、AXIOM_DATASET、LOKI_URL、BETTERSTACK_TOKEN、OTEL_ENABLED、LOG_FILE等配置把事件投递到 HyperDX、Axiom、Loki、BetterStack、OTLP 或本地文件:
// packages/server/api/src/app/helper/logger/index.ts(节选) return evlogSetup.init({ params: { serviceName: 'activepieces-api', version: apVersionUtil.getCurrentRelease(), logLevel, logPretty, sampleRateInfo, keepSlowMs, drainConfig: { hyperdxToken: environmentVariables.getEnvironment(AppSystemProp.HYPERDX_TOKEN), axiomToken: environmentVariables.getEnvironment(AppSystemProp.AXIOM_TOKEN), lokiUrl: environmentVariables.getEnvironment(AppSystemProp.LOKI_URL), betterstackToken: environmentVariables.getEnvironment(AppSystemProp.BETTERSTACK_TOKEN), otlpEnabled: environmentVariables.getEnvironment(AppSystemProp.OTEL_ENABLED) === 'true', }, }, })同时,Activepieces 使用 Fastify 的FastifyBaseLogger作为日志门面类型(例如 action-run.service.ts 中import { FastifyBaseLogger } from 'fastify'),并通过log.child({ flowRunId, webhookId, flowId, flowVersionId })为每次流程运行、每个 Webhook 请求创建携带业务上下文的子日志器——这与本指南要讲的「在请求/任务作用域内累积上下文、最终一次性输出」的理念一脉相承。
理解了这个背景,下面进入完整的审查清单。
一、快速扫描(Quick Scan):三分钟定位问题
开始正式审查前,先用下面三项快速检查确定改进机会。它们覆盖了最常见的三类日志问题:散落的 console 语句、无上下文的错误、缺失日志的请求处理器。
1. Console 语句审计
在代码库中搜索以下模式:
// ❌ 需要查找并改造的模式 console.log(...) console.error(...) console.warn(...) console.info(...) console.debug(...)需要追问的问题:
- 同一个函数里是否存在多条 console 语句?
- 它们是否在记录请求/响应数据?
- 是否可以把它们合并成一个宽事件(wide event)?
关于宽事件的完整概念(为什么一次请求只应输出一条包含全部上下文的日志),参见仓库内的 wide-events.md。
2. 错误模式审计
搜索以下模式:
// ❌ 泛化错误 throw new Error('...') throw Error('...') // ❌ 无上下文的重新抛出 catch (error) { throw error } // ❌ 先记录再抛出(重复记录) catch (error) { console.error(error) throw error }需要追问的问题:
- 错误消息是否解释了「发生了什么」?
- 是否有
why字段说明根本原因? - 是否有
fix字段给出解决方案建议? - 原始错误是否作为
cause被保留下来?
3. 请求处理器审计
对每个 API 路由/处理器,检查:
// ❌ 缺少请求上下文 export default defineEventHandler(async (event) => { // 完全没有日志,或者散落着 console.log })需要追问的问题:
- 是否有请求作用域(request-scoped)的 logger?
- 上下文是否在整个请求过程中被持续累积?
- 是否在请求结束时只输出一次(single emit)?
二、详细审查(Detailed Review):三类转换的完整范式
快速扫描发现的问题,按下述三类范式逐一改造。
Console.log 转换
单个调试日志
// ❌ 改造前 console.log('Processing user:', userId) // ✅ 改造后 - 如果是更大操作的一部分 log.set({ user: { id: userId } }) // ✅ 改造后 - 如果是独立的调试信息 log.debug('user', `Processing user ${userId}`)多条相关日志合并
// ❌ 改造前 console.log('Starting checkout') console.log('User:', user.id) console.log('Cart items:', cart.items.length) console.log('Total:', cart.total) // ✅ 改造后 - 合并为一条宽事件 log.info({ action: 'checkout', user: { id: user.id }, cart: { items: cart.items.length, total: cart.total }, })请求生命周期日志
// server/api/process.post.ts // ❌ 改造前 export default defineEventHandler(async (event) => { console.log('Request started') const user = await getUser(event) console.log('User loaded') const result = await processData(user) console.log('Processing complete') return result }) // ✅ 改造后(Nuxt - 自动导入,无需 import) // Nitro v3: import { useLogger } from 'evlog/nitro/v3' // Nitro v2: import { useLogger } from 'evlog/nitro' export default defineEventHandler(async (event) => { const log = useLogger(event) const user = await getUser(event) log.set({ user: { id: user.id } }) const result = await processData(user) log.set({ result: { id: result.id } }) return result // emit() 自动触发 })注意:在 Nuxt/Nitro 下
emit()由框架在请求结束时自动调用;而在独立 TypeScript 场景(脚本、Worker)下必须手动调用emit(),参见 SKILL 中「Standalone TypeScript」一节的 SKILL.md。
错误转换
泛化错误
// ❌ 改造前 throw new Error('Failed to create user') // ✅ 改造后 throw createError({ message: 'Failed to create user', why: 'Email address already registered', fix: 'Use a different email or log in to existing account', link: 'https://your-app.com/docs/registration', })无上下文的包装错误
// ❌ 改造前 try { await externalApi.call() } catch (error) { throw new Error('API call failed') } // ✅ 改造后 try { await externalApi.call() } catch (error) { throw createError({ message: 'External API call failed', why: `API returned: ${error.message}`, fix: 'Check API credentials and try again', link: 'https://api-docs.example.com/errors', cause: error, }) }「先记录再抛出」反模式
// ❌ 改造前 try { await riskyOperation() } catch (error) { console.error('Operation failed:', error) throw error } // ✅ 改造后 - 记录宽事件,同时抛出带上下文的错误 try { await riskyOperation() } catch (error) { log.error(error, { step: 'riskyOperation' }) throw createError({ message: 'Operation failed', why: error.message, fix: 'Check input and retry', cause: error, }) }请求处理器转换
完全无日志的处理器
// server/api/orders.post.ts // ❌ 改造前 export default defineEventHandler(async (event) => { const body = await readBody(event) const result = await processOrder(body) return result }) // ✅ 改造后(Nuxt - 自动导入,无需 import) // Nitro v3: import { useLogger } from 'evlog/nitro/v3' // Nitro v2: import { useLogger } from 'evlog/nitro' import { createError } from 'evlog' export default defineEventHandler(async (event) => { const log = useLogger(event) const body = await readBody(event) log.set({ order: { items: body.items?.length } }) try { const result = await processOrder(body) log.set({ result: { orderId: result.id, status: result.status } }) return result } catch (error) { log.error(error, { step: 'processOrder' }) throw createError({ message: 'Order processing failed', why: error.message, fix: 'Check the order data and try again', }) } // emit() 自动触发 })三、审查清单汇总(Review Checklist Summary)
审查收尾时,逐项核对以下清单。
日志(Logging)
- 生产代码中不存在裸
console.log语句 - 请求处理器使用
useLogger(event)(Nuxt/Nitro)或createRequestLogger()(独立场景) - 上下文通过
log.set()在整个请求过程中持续累积 emit()在使用useLogger()时自动触发,使用createRequestLogger()时手动触发- 宽事件包含:用户信息、业务上下文、结果(outcome)
错误(Errors)
- 所有错误使用
createError()而非new Error()(从evlog导入) - 每个错误都有清晰的
message和恰当的status状态码 - 复杂错误包含解释根因的
why - 可修复的错误包含可操作步骤的
fix - 有文档的错误包含
link指向文档 - 包装错误保留原始错误的
cause - 仅面向运维或敏感的诊断信息放在
internal,而非message/why/fix
internal字段只出现在服务端日志与 drain 输出中,不会进入 HTTP 错误响应体,也不会出现在客户端parseError()的结果中;它存放在非枚举 Symbol 上,JSON.stringify(error)不会泄露其内容。更完整的字段规范与错误模板参见 structured-errors.md。
前端错误处理(Frontend Error Handling)
- API 错误被捕获并以完整上下文展示(message、why、fix)
- Toast 或错误组件使用
error.data.data中的结构化数据 - 文档链接可点击操作(Toast 中的按钮/链接)
上下文(Context)
- 用户上下文包含:id、套餐/订阅、相关业务数据
- 请求上下文包含:method、path、requestId
- 业务上下文与领域相关且对排障有用
- 日志中不包含敏感数据(密码、令牌、完整卡号)
四、反模式汇总表
| 反模式 | 修复方案 |
|---|---|
一个函数内多条console.log | 用useLogger(event).set()累积为单条宽事件 |
throw new Error('...') | throw createError({ message, status, why, fix }) |
console.error(e); throw e | log.error(e); throw createError(...) |
| 请求处理器完全无日志 | 添加useLogger(event)(Nuxt/Nitro)或createRequestLogger()(独立场景) |
| 扁平化日志数据 | 分组对象:{ user: {...}, cart: {...} } |
| 缩写字段名 | 使用描述性命名:用userId而不是uid |
五、可直接复用的审查评论模板
审查者在 PR 评论中使用以下措辞,能帮助作者快速理解改法。
发现 Console.log
Consider using evlog's wide event pattern here. Instead of multiple console.log statements, use
useLogger(event)to accumulate context and emit a single comprehensive event.
泛化错误
This error would benefit from evlog's structured error pattern. Consider using
import { createError } from 'evlog'andcreateError({ message, status, why, fix })to provide more debugging context.
缺少请求上下文
This handler would benefit from request-scoped logging. Add
useLogger(event)at the start to capture context throughout the request lifecycle.
正向反馈(日志质量好)
Nice use of wide events here! The context is well-structured and will be very useful for debugging.
六、进阶:审查后的落地与深化
宽事件应包含哪些字段
每条宽事件都应包含三层上下文(详见 wide-events.md):
- 请求上下文:
method、path、requestId、traceId(用于分布式追踪); - 用户上下文:
user.id、user.plan、user.accountAge等与业务相关的字段; - 业务上下文:按领域补充,如电商的
cart、payment、order,上传场景的upload.filename、size、mimeType。
结果(outcome)通过status字段记录,duration由emit()自动计算,无需手动计时。
安全红线:显式选择要记录的字段
// ❌ 危险 - 会把 password 也打出来 log.set({ user: body }) // ✅ 安全 - 显式选择字段 log.set({ user: { id: body.id, email: maskEmail(body.email), // password: body.password ← 永远不要包含 }, })永远不要记录:密码、API 密钥、令牌、密钥、完整卡号、CVV、SSN、PII、会话令牌、JWT。
生产环境:排水管道与采样
生产环境建议用createDrainPipeline包裹 drain 适配器以获得批处理、指数退避重试与缓冲区溢出保护,并务必在服务close钩子中调用drain.flush(),否则进程退出时缓冲的事件会丢失(配置细节与反模式见 drain-pipeline.md)。结合 Activepieces 的实践,采样率与慢请求阈值这类策略都可以通过环境变量下发,例如LOG_SAMPLE_RATE_INFO控制 info 级事件的采样百分比,LOG_KEEP_SLOW_MS控制超过多少毫秒的请求必须保留——这正对应 evlog 的 head sampling(sampling.rates)与 tail sampling(sampling.keep)能力。
错误如何流向 HTTP 响应与前端
后端只需throw createError(...),框架会自动把结构化字段转成 HTTP 响应;前端用parseError()提取message、why、fix、link后,既可展示在 Toast 中,也可把link渲染为可点击的「了解更多」按钮——让用户看到的不是一句「出错了」,而是发生了什么、为什么、怎么解决(示例见 structured-errors.md)。
结语:让审查有据可依
把这份清单作为每次涉及日志与错误处理改动的 PR 审查基线:先快速扫描三类高频问题,再按「宽事件 + 结构化错误」两个范式逐例改造,最后对照汇总清单逐项验收。配合仓库内的 wide-events.md、structured-errors.md 与 drain-pipeline.md 三份参考文档,以及 SKILL.md 中各框架(Nuxt、Next.js、Nitro、Express、Fastify、Hono、NestJS 等)的接入方式,你就能把「散落的 console 日志」逐步收敛为「一条宽事件讲清一次请求」,把「一句 failed」升级为「message + why + fix + cause 俱全」的可诊断错误。
【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考