实践指南:用 evlog 构建可解释、可自愈的错误体系)
Activepieces 日志规范之结构化错误Structured Errors实践指南用 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本指南源自仓库内 review-logging-patterns 技能 的 structured-errors.md 参考文档是一份关于如何把throw new Error(Something went wrong)这类无信息量错误改造成自带发生了什么、为什么发生、如何修复完整上下文的结构化错误的实操手册。它适用于 Activepieces 后端 API 路由、worker 任务、以及任何基于 Nuxt / Nitro / Next.js / Express / Fastify / Hono 等框架的 TypeScript 服务。读完本文你将掌握createError()的完整字段体系、internal敏感信息的隔离机制、五大常见错误模式的标准写法、与 wide events 日志事件的集成方式以及前端parseError()的消费方法让每一次故障排查都从大海捞针变成按图索骥。通用错误的困境日志与监控里的三无信息先看几行在真实代码库里随处可见的错误写法// ❌ 无用的错误 throw new Error(Something went wrong) throw new Error(Failed) throw new Error(Invalid input) // ❌ 缺少上下文 throw new Error(Payment failed) // Why? How do I fix it?当这些错误最终出现在你的日志或监控系统里时你面临的是四个不知道什么实际上失败了what actually failed为什么失败why it failed如何修复how to fix it去哪里找到更多信息where to find more information没有上下文就没有排查路径。一条Payment failed无法区分是卡被拒、余额不足、网络超时还是密钥失效一条Failed甚至无法定位到具体模块。这正是结构化错误要解决的问题让错误自身携带诊断所需的一切信息。结构化错误的解剖createError()的七件套evlog 通过createError()工厂函数构造错误一个完整示例import { createError } from evlog throw createError({ message: Payment failed, // What happened status: 402, // HTTP status code why: Card declined by issuer, // Why it happened fix: Try a different payment method, // How to fix it link: https://docs.example.com/..., // More information cause: originalError, // Original error internal: { // Optional: backend / logs only correlationId: pay_abc, processorCode: card_declined, }, })每个字段各司其职下面逐一分化详解更详细的编写规范见后文字段编写指南。internal只进日志、不进响应体的后端专属字段internal是结构化错误中最容易用错、也最有价值的字段它用于存放ID、网关返回码、内部诊断信息等这些信息绝不能出现在 HTTP 错误响应体中也绝不能出现在客户端parseError()的结果里服务端代码通过error.internal访问在toJSON()以及框架的序列化器中该值会被省略当错误通过log.error()或等价的自动捕获记录时它会作为 wide event 的一部分出现在error.internal键下底层实现上internal存储在一个不可枚举的 Symbol 属性上因此JSON.stringify(error)不会泄漏它DevTools 中可能显示为[Symbol(evlog.error.internal)]。换句话说给运维/后端看的放internal给用户/前端看的放message/why/fix。两种输出形态开发期可读生产期可机器解析同一个错误在开发环境与控制台/日志系统里呈现为两种形态。控制台输出开发环境Error: Payment failed Why: Card declined by issuer Fix: Try a different payment method More info: https://docs.example.com/payments/declined Caused by: StripeCardError: card_declined开发模式下采用 pretty 树状格式why、fix、link分列展示cause以Caused by:链式呈现一眼看清因果。JSON 输出生产环境{ name: EvlogError, message: Payment failed, why: Card declined by issuer, fix: Try a different payment method, link: https://docs.example.com/payments/declined, cause: { name: StripeCardError, message: card_declined }, stack: ... }注意internal不出现在这个 JSON 中cause被序列化为其自身的namemessagestack仅在相应配置下保留。生产环境的 JSON 直接进入日志管道、可被任意日志分析系统Axiom、OTLP、Datadog 等 drain 适配器结构化检索。字段编写指南每个字段的好与坏message—— 发生了什么面向用户的、描述什么东西出了错的文案。// ✅ 好清晰、可行动 message: Failed to sync repository message: Unable to process payment message: User not found // ❌ 坏模糊、无帮助 message: Error message: Something went wrong message: Failedwhy—— 为什么发生面向调试的技术性根因解释。它不能只是复述 message。// ✅ 好具体、技术性 why: GitHub API rate limit exceeded (403) why: Card declined by issuer: insufficient_funds why: No user with ID user_123 exists in database // ❌ 坏只是把 message 换个说法 why: It failed why: Error occurredfix—— 如何修复可执行的解决步骤。写不出来就说明你对这个错误还没有足够的理解。// ✅ 好具体动作 fix: Wait 1 hour or use a different API token fix: Use a different payment method or contact your bank fix: Check the user ID and try again // ❌ 坏不可执行 fix: Fix the error fix: Try againlink—— 更多信息指向详细排查文档的 URL用于复杂错误。// ✅ 好指向具体文档 link: https://docs.github.com/en/rest/rate-limit link: https://docs.stripe.com/declines/codes link: https://your-app.com/docs/errors/user-not-foundcause—— 原始错误触发当前错误的下层错误用于保留原始调用栈。try { await stripe.charges.create(...) } catch (error) { throw createError({ message: Payment failed, why: Stripe error: ${error.code}, fix: Contact support with error code, cause: error, // Preserves original stack trace }) }常见错误模式四类高频场景的标准模板外部 API / 服务错误// 限流 throw createError({ message: GitHub sync temporarily unavailable, status: 429, why: API rate limit exceeded (5000/hour), fix: Wait until rate limit resets or use authenticated requests, link: https://docs.github.com/en/rest/rate-limit, cause: error, }) // 认证失败 throw createError({ message: Unable to connect to Stripe, status: 503, why: Invalid API key provided, fix: Check STRIPE_SECRET_KEY environment variable, link: https://docs.stripe.com/keys, cause: error, }) // 网络问题 throw createError({ message: Failed to fetch user data, status: 504, why: Connection timeout after 30s, fix: Check network connectivity and try again, cause: error, })注意这三个例子的status语义429限流、503依赖服务不可用、504网关超时——状态码本身就是第一层诊断信号。校验错误// 缺少必填字段 throw createError({ message: Invalid checkout request, status: 400, why: Required field email is missing, fix: Include a valid email address in the request body, link: https://your-api.com/docs/checkout#request-body, }) // 格式非法 throw createError({ message: Invalid email format, status: 422, why: ${email} is not a valid email address, fix: Provide an email in the format userexample.com, }) // 违反业务规则 throw createError({ message: Cannot cancel subscription, status: 409, why: Subscription has already been cancelled, fix: No action needed - subscription is already inactive, })注意 400请求错误与 422语义/格式错误的区分以及 409状态冲突用于业务上不可执行的场景。数据库错误// 记录不存在 throw createError({ message: User not found, status: 404, why: No user with ID ${userId} exists, fix: Verify the user ID is correct, }) // 约束冲突重复 throw createError({ message: Cannot create duplicate account, status: 409, why: User with email ${email} already exists, fix: Use a different email or log in to existing account, link: https://your-app.com/login, }) // 连接问题 throw createError({ message: Database unavailable, status: 503, why: Connection pool exhausted, fix: Reduce concurrent connections or increase pool size, cause: error, })权限错误throw createError({ message: Access denied, status: 403, why: User lacks admin role required for this action, fix: Contact an administrator to request access, link: https://your-app.com/docs/permissions, })改造示例同一段支付逻辑的前后对比改造前通用错误async function processPayment(cart, user) { try { return await stripe.charges.create({ amount: cart.total, currency: usd, source: user.paymentMethodId, }) } catch (error) { throw new Error(Payment failed) // ❌ No context } }改造后结构化错误async function processPayment(cart, user) { try { return await stripe.charges.create({ amount: cart.total, currency: usd, source: user.paymentMethodId, }) } catch (error) { throw createError({ message: Payment failed, why: getStripeErrorReason(error), fix: getStripeErrorFix(error), link: https://docs.stripe.com/declines/codes, cause: error, }) } } function getStripeErrorReason(error) { const reasons { card_declined: Card was declined by the issuer, insufficient_funds: Card has insufficient funds, expired_card: Card has expired, // ... } return reasons[error.code] ?? Stripe error: ${error.code} } function getStripeErrorFix(error) { const fixes { card_declined: Try a different payment method or contact your bank, insufficient_funds: Use a different card or add funds, expired_card: Update your card details with a valid expiration date, // ... } return fixes[error.code] ?? Contact support with error code }这个模式值得反复推敲getStripeErrorReason/getStripeErrorFix把错误码到原因、错误码到修复方案的映射集中管理新增一种失败码只需要在两张表里各加一行??兜底保证任何未枚举的 Stripe 错误码也能得到合理的默认文案。与 Wide Events 的集成错误即事件的一部分结构化错误与 wide events宽事件一次逻辑操作的全部上下文汇聚成一条日志天然互补。在 API 路由中// server/api/checkout.post.ts // Nuxt: useLogger and createError are auto-imported // 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) try { // ... business logic ... } catch (error) { // EvlogError fields are automatically captured log.error(error, { step: payment }) throw createError({ message: Payment failed, why: error.message, fix: Try a different payment method, }) } // emit() called automatically })这条 wide event 最终会包含{ error: { name: EvlogError, message: Payment failed, why: Card declined by issuer, fix: Try a different payment method, link: https://docs.stripe.com/declines/codes, internal: { stripeRequestId: req_123 } }, step: payment }关于internal的合并有一个重要细节如果你使用了createError({ ..., internal: { ... } })但没有手动调用log.error(error)框架集成在 emit 阶段把被抛出的错误挂载到 wide event 上时依然会把internal合并进error.internal——也就是说只要错误在请求链路上被抛出内部诊断信息就不会丢。最佳实践Do 与 DontDo应该做至少始终提供message和why存在可执行的解决方案时提供fix复杂错误补充指向文档的link包装错误时保留cause尽量具体地说明什么失败了、为什么运维/敏感诊断信息放internal不要放进why/fix/message。Dont不要做使用 Error、Failed 之类的通用 message泄漏敏感数据密码、令牌、PII期待internal出现在 HTTP JSON 或parseError()中——它只属于服务端日志与 drain让why和message完全相同给出实际上不可能执行的修复建议创建没有任何上下文的错误。Nitro 兼容性抛出去就是标准 HTTP 响应evlog 错误适用于任何 Nitro 驱动的框架Nuxt、Nitro v2/v3、TanStack Start 等。当错误在 API 路由中被抛出时框架自动将其转换为 HTTP 响应// 后端——直接抛 throw createError({ message: Payment failed, status: 402, why: Card declined, fix: Try another card, link: https://docs.example.com/payments, }) // HTTP Response: // Status: 402 // Body: { // statusCode: 402, // message: Payment failed, // data: { why: Card declined, fix: Try another card, link: ... } // }注意响应体的结构statusCodemessage在顶层why/fix/link收进data对象——这意味着前端必须通过parseError()来提取这些嵌套字段而不是直接读error.message就完事。前端集成parseError()把字段提回顶层import { parseError } from evlog try { await $fetch(/api/checkout) } catch (err) { const error parseError(err) // Direct access: error.message, error.why, error.fix, error.link toast.add({ title: error.message, description: error.why, color: error, actions: error.link ? [{ label: Learn more, onClick: () window.open(error.link) }] : undefined, }) if (error.fix) console.info( Fix: ${error.fix}) }差异一目了然通用错误只显示 An error occurred结构化错误则展示 message、解释 why、给出 fix并提供可点击的文档链接。用户看到的不是一句冰冷的报错而是一条可执行的行动指引。错误消息模板速查表把常见场景套用固定模板按需填充各字段PatternStatusFieldsResource not found资源不存在404why: 缺了什么fix: 核对标识符External service failure外部服务失败503why: 服务错误fix: 可执行步骤link: 服务文档cause: 原始错误Validation failure校验失败400why: 哪里非法fix: 期望格式Permission denied权限不足403why: 缺什么权限fix: 如何获得权限仓库佐证Activepieces 自身的结构化错误实现结构化错误的思路在 Activepieces 仓库内并非孤例。作为该技能文档的落地对象仓库核心包实现了自己的ActivepiecesError位于 packages/core/utils/src/lib/activepieces-error.tsexport class ActivepiecesError extends Error { constructor(public error: ApErrorParams, message?: string) { super(error.code (message ? : ${message} : )) } override toString(): string { return JSON.stringify({ code: this.error.code, message: this.message, params: this.error.params, }) } }从源码结构看它与 evlog 结构化错误共享同一哲学但落点不同机器可读的稳定标识evlog 用statusHTTP 状态码messageActivepieces 用ErrorCode枚举 params。ApErrorParams联合类型覆盖了上百种业务错误码——从ENTITY_NOT_FOUND、INVALID_APP_CONNECTION、INVALID_BEARER_TOKEN到SANDBOX_MEMORY_ISSUE、CHAT_CONTEXT_LIMIT_EXCEEDED、SECRET_MANAGER_GET_SECRET_FAILED等见 activepieces-error.ts 的完整联合类型与 evlog 文档中用稳定、可检索的标识承载错误身份的理念一致上下文参数化params承载具体细节如缺失的用户 ID、超出限额的指标对应 evlog 的why字段定位序列化即诊断toString()直接输出{code, message, params}的 JSON——这正是错误本身可被日志系统结构化消费的实现与本文生产环境 JSON 输出一节的目标一致。该错误类在整个服务端被广泛使用例如 packages/core/shared/src/lib/automation/pieces/utils.ts 中导入ActivepiecesError与ErrorCode并在 第 47 行 以new ActivepiecesError({ code: ..., params: ... })抛出。也就是说无论用 evlog 的createError()还是项目自带的ActivepiecesError结构化错误都要求每个错误具备可编程识别的身份、可诊断的上下文以及可序列化进日志的表现形态。延伸阅读review-logging-patterns 技能入口技能总览包含 evlog 在 Nuxt、Next.js、SvelteKit、Nitro、NestJS、Express、Hono、Fastify、Elysia、React Router、oRPC、Cloudflare Workers 及独立 TypeScript 中的完整接入配置以及 drain 适配器、采样、脱敏、AI SDK 集成wide-events.md与本文配套的宽事件参考讲述如何用log.set()聚合一次请求的全部上下文并一次性 emit以及字段命名规范与敏感数据防护code-review.md把本文的规范落成可勾选的审查清单与反模式速查表适合在 PR 评审时逐条对照drain-pipeline.md错误被记录之后如何批量、重试、防溢出地送往外部日志系统Axiom、OTLP、Sentry、Datadog 等Activepieces 结构化错误实现项目自有的ActivepiecesError类与ErrorCode/ApErrorParams类型体系可对照学习。【免费下载链接】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),仅供参考