Activepieces 结构化日志分析实战:基于 evlog 宽事件(Wide Event)的排障与性能排查指南
【免费下载链接】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 这类以 Node.js/TypeScript 为核心的应用中,用 evlog 的结构化日志体系(wide event / 宽事件)完成错误排障、慢请求分析与请求链路追踪。你将学会:定位.evlog/logs/下的 NDJSON 日志文件并识别格式、解析error.data.why/error.data.fix等结构化错误字段、按路径/耗时/来源过滤事件,以及从生产 drain 管道(批处理、重试、溢出保护)的角度理解日志为何会出现在文件里。文中的 log 字段表与过滤模式可直接落地到日常调试工作流。
一、背景:为什么日志分析要用"宽事件"而不是逐行 grep
传统日志把一次请求拆成多行输出,排障时要靠 grep 在成千上万行里手工拼凑时间线:
10:23:45.001 Request received POST /checkout 10:23:45.012 User authenticated: user_123 10:23:45.234 Payment failed: card_declined 10:23:45.235 Request completed: 500而 evlog 采用wide event(宽事件)模型:一个逻辑操作(通常是一个 HTTP 请求)的全部上下文——请求信息、用户信息、业务数据、错误详情——在一条JSON 对象里一次性发出。这正是仓库内 .agents/skills/analyze-logs/SKILL.md 所强调的核心理念,也是 .agents/skills/review-logging-patterns/references/wide-events.md 中"一条 wide event 即可独立还原一次事故"的原因。
{ "timestamp": "2025-01-24T10:23:45.235Z", "level": "error", "service": "api", "method": "POST", "path": "/checkout", "duration": "234ms", "user": { "id": "user_123", "plan": "premium" }, "cart": { "items": 3, "total": 9999 }, "payment": { "provider": "stripe", "method": "card" }, "error": { "code": "card_declined", "retriable": false } }在实际项目中,这类日志由 evlog 的file system drain(文件系统排水器)写入磁盘。仓库的 日志分析 skill 描述的正是读取这些文件的完整方法。你可以把它当作 Activepieces 及任何接入 evlog 的 Node 服务的"本地可观测性入口"。
二、定位日志:.evlog/logs/ 目录与文件命名规则
2.1 搜索位置与优先级
日志文件由 evlog 的文件系统 drain 写出,文件名按日期命名(如2026-03-14.jsonl),位于.evlog/logs/目录下。查找时按以下顺序(相对项目根目录):
.evlog/logs/(默认位置)- 各应用子目录内的
.evlog/logs/(monorepo 场景,如apps/*/.evlog/logs/)
可直接用 glob 模式定位:
.evlog/logs/*.jsonl */.evlog/logs/*.jsonl apps/*/.evlog/logs/*.jsonl分析时应从日期最新的文件开始读,因为排障通常关心最近发生的事件。
2.2 格式检测:NDJSON 还是 Pretty
文件系统 drain 支持两种输出格式,解析前务必先看文件前几个字节确定格式:
- NDJSON(默认,
pretty: false):每行一个紧凑 JSON 对象,逐行解析即可。判断方法:文件第二个字符是换行符或"。 - Pretty(
pretty: true):每个事件是多行缩进的 JSON。判断方法:第二个字符是空格,或换行后跟空格。解析方式有两种:- 整文件读取后按顶层对象切分:
JSON.parse('[' + content.replace(/\}\n\{/g, '},{') + ']'); - 或使用流式 JSON 解析器。
- 整文件读取后按顶层对象切分:
注意:
.evlog/logs/已被自动加入.gitignore,这些文件只存在于本地开发机或运行应用的服务器上,不会进入版本库——这也是它能作为"本地真相"的原因。
三、如果找不到日志:启用文件系统 drain
若.evlog/logs/目录不存在或为空,说明文件系统 drain 尚未启用。需要把createFsDrain()接入应用的 evlog 初始化位置,不同框架接入点不同(以下代码均来自 analyze-logs SKILL):
import { createFsDrain } from 'evlog/fs' // Nuxt / Nitro: server/plugins/evlog-drain.ts export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook('evlog:drain', createFsDrain()) }) // Hono / Express / Elysia: 作为中间件选项传入 app.use(evlog({ drain: createFsDrain() })) // Fastify: 作为插件选项传入 await app.register(evlog, { drain: createFsDrain() }) // NestJS: 作为模块选项传入 EvlogModule.forRoot({ drain: createFsDrain() }) // Standalone: 传给 initLogger initLogger({ drain: createFsDrain() })配置完成后,需要先触发一些真实请求(产生事件)再重新分析。生产环境中一般不直接把createFsDrain()挂裸函数,而是外包一层drain pipeline(见第七节),以获得批处理、重试与溢出保护。
四、日志格式:wide event 字段速查表
每个.jsonl文件中的每一行都是一个自包含的 JSON 对象。核心字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
timestamp | string | ISO 8601 时间戳 |
level | string | info、warn、error、debug |
service | string | 服务名 |
environment | string | development、production等 |
method | string | HTTP 方法(GET、POST等) |
path | string | 请求路径(如/api/checkout) |
status | number | HTTP 响应状态码 |
duration | string | 请求耗时(如"234ms") |
requestId | string | 唯一请求标识,用于链路追踪 |
error | object | 错误详情:name、message、stack、statusCode、data |
error.data.why | string | 失败原因的人类可读解释 |
error.data.fix | string | 针对该错误的建议修复方案 |
source | string | client表示浏览器端日志;服务端日志无此字段 |
userAgent | object | 解析后的浏览器 / 操作系统 / 设备信息 |
除以上字段外,其余均为业务上下文,由代码通过log.set()添加(例如user、cart、payment等)。
4.1 为什么why/fix是最高价值字段
error.data.why与error.data.fix是 evlog 特有的结构化错误字段。参照 structured-errors.md,它们来自createError()的结构化错误模型:
throw createError({ message: 'Payment failed', // 发生了什么 status: 402, // HTTP 状态码 why: 'Card declined by issuer', // 为什么发生 fix: 'Try a different payment method', // 如何修复 link: 'https://docs.example.com/payments/declined', // 更多信息 cause: originalError, // 保留原始错误与堆栈 internal: { // 仅服务端日志可见 correlationId: 'pay_abc', processorCode: 'card_declined', }, })其中internal字段通过非枚举 Symbol 存储,不会出现在 HTTP 响应体、toJSON()输出或客户端parseError()结果中,只随log.error()进入 wide event 的error.internal——这是"运营侧诊断信息不泄露给客户端"的关键机制。因此,分析日志时如果error.data.why/error.data.fix存在,它们就是最可操作的排障信息,应优先阅读。
4.2 客户端日志如何进入服务端文件
带"source":"client"的事件源自浏览器端日志,通过 transport 端点(如/api/_evlog/ingest)回传服务端后落盘。这一点在 Activepieces 服务端有真实实现佐证:在 packages/server/api/src/app/helper/logs/client-logs.controller.ts 中,clientLogsController暴露POST /client端点接收浏览器上报的事件数组(单次最多 500 条),并维护一组RESERVED_KEYS(service、version、level、msg、timestamp、error、timings、requestId、traceId、method、path、source)——客户端永远不允许覆盖这些由服务端中间件拥有的字段,其余业务字段则透传并以source: 'client'标记重发。这解释了为什么日志中的source字段可以用来区分浏览器端与服务端事件。
五、三步分析法:从原始文件到结论
Step 1:读取最新日志文件
打开日期最新的.jsonl文件,每行独立解析为一个 JSON 事件。
Step 2:按问题类型过滤事件
根据用户问题选择过滤维度:
- 错误:找
"level":"error"或status >= 400 - 特定接口:按
path匹配 - 慢请求:解析
duration(如"706ms"),过滤高值 - 特定用户/动作:匹配业务字段
- 客户端问题:过滤
"source":"client" - 时间范围:比较
timestamp
Step 3:逐个事件解释
对每个相关事件按以下五步输出结论:
- 发生了什么:概括
path、method、status、level - 为什么失败(错误):读
error.message、error.data.why与堆栈 - 如何修复:查看
error.data.fix中的建议 - 业务上下文:检查业务字段(用户信息、支付详情等)
- 规律总结:寻找重复出现的错误、性能劣化或关联性失败
六、五大分析模式:可直接套用的过滤套路
6.1 找出所有错误
Filter: level === "error" Group by: error.message 或 path Look for: 重复模式、共性失败点6.2 找出慢请求
Filter: 解析 duration 字符串,比较 > 阈值(如 1000ms) Sort by: duration 降序 Look for: 特定端点、时段性规律注意duration是带单位的字符串(如"706ms"),比较前需先解析出数字部分。参照 wide-events.md,duration 通常由emit()自动计算并写入,属于宽事件内置字段。
6.3 追踪单个请求
Filter: requestId === "the-request-id" Result: 该请求的单条宽事件,包含全部上下文这是宽事件模型与传统日志最大的差异点:不需要跨行关联,一个 requestId 对应一条完整事件。
6.4 按端点统计错误率
Group events by: path Count: 每个 path 的总事件数 vs 错误事件数 Look for: 错误率异常高的端点6.5 客户端 vs 服务端错误对比
Split by: source === "client" vs 无 source 字段 Compare: 两端错误模式 Look for: 服务端无对应错误的客户端报错(通常是网络问题)例如浏览器端出现500/超时但服务端日志中没有对应记录,通常指向网络中断、CDN 问题或请求根本没到达应用。
七、生产落盘背后的机制:drain pipeline
文件系统 drain 只是 evlog 众多 drain 适配器之一(还有 Axiom、OTLP、Sentry、Datadog、PostHog 等)。生产环境推荐用createDrainPipeline()包装任意 drain,获得批量发送、指数退避重试与缓冲区溢出保护。这在 drain-pipeline.md 中有完整参考,理解它能帮你判断"为什么日志会延迟出现"或"为什么某些事件丢失了"。
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, // 每批最大事件数(默认 50) intervalMs: 5000, // 批次未满时最大等待时间(默认 5000ms) }, retry: { maxAttempts: 3, // 总尝试次数含首次(默认 3) backoff: 'exponential', // 'exponential' | 'linear' | 'fixed'(默认 exponential) initialDelayMs: 1000, // 首次重试基础延迟(默认 1000ms) maxDelayMs: 30000, // 任意重试延迟上限(默认 30000ms) }, maxBufferSize: 1000, // 最大缓冲事件数,溢出丢弃最旧(默认 1000) onDropped: (events, error) => { // 溢出或重试耗尽时回调 console.error(`[evlog] Dropped ${events.length} events:`, error?.message) }, })7.1 工作原理(七步)
drain(ctx)把单个事件压入缓冲区buffer.length >= batch.size时立即批量 flush- 批次未满则启动定时器,
intervalMs到期后 flush 当前缓冲 - flush 时 drain 函数收到的总是数组
T[] - drain 抛出异常则按退避策略重试
maxAttempts次失败后调用onDropped并丢弃该批- 缓冲超过
maxBufferSize时丢弃最旧事件并调用onDropped
7.2 退避策略选择
| 策略 | 延迟模式 | 适用场景 |
|---|---|---|
exponential | 1s、2s、4s、8s... | 默认。适合需要恢复时间的瞬时故障 |
linear | 1s、2s、3s、4s... | 可预测的延迟增长 |
fixed | 1s、1s、1s、1s... | 有已知冷却时间的限流 API |
7.3 关键 API
const drain = pipeline(myDrainFn) drain(ctx) // 推送单个事件(同步、非阻塞) await drain.flush() // 强制 flush 所有缓冲事件 drain.pending // 当前缓冲的事件数(只读)最重要的实践:在服务端close钩子中调用drain.flush(),否则进程退出时缓冲事件会丢失。这也解释了为何本地.evlog/logs/中偶发缺少最后几条事件——很可能是进程未优雅关闭导致缓冲未落盘。
八、分析时的关键注意事项
- 每行都是完整的自包含事件:与传统日志不同,无需跨行关联——一行就包含一次请求的全部上下文。
why/fix是最高优先级信息:当error.data.why与error.data.fix存在时,它们是最可操作的内容,应直接用于向用户解释与建议。- duration 是带单位字符串:比较前先解析数字部分(如
"706ms"→706)。 source: "client"事件来自浏览器:它们经 transport 端点(如 client-logs.controller.ts 的POST /client)回传服务端后统一落盘,可用于区分端侧问题。- 日志文件已 gitignore:只存在于运行应用的机器上,属于本地排障素材,不进入版本库。
九、进阶:从"会读日志"到"写出好日志"
日志分析能力与日志生产质量互为表里。evlog 的 wide event 之所以好分析,是因为写日志时遵循了结构化约定——分析时可反向印证这些约定是否被遵守:
- 请求处理器应有
useLogger(event)/createRequestLogger(),并在请求结束自动emit()一次(参照 wide-events.md 的 request logger 模式); - 业务字段用分组对象而非扁平缩写(
{ user: { id, plan } }而不是{ uid, n }); - 错误用
createError()带why/fix,运营侧诊断放internal; - 敏感数据(密码、token、完整卡号、PII)绝不进日志,生产环境默认开启
redact自动脱敏(如4111111111111111→****1111,alice@example.com→a***@***.com)。
如果你在评审代码时发现console.log泛滥、throw new Error('...')无上下文、请求处理器完全没有日志,可参考仓库内的 review-logging-patterns SKILL 及其 code-review.md 检查清单,把"可读"的日志改造成"可分析"的日志——这样下次再排障时,error.data.why和fix就会直接告诉你答案。
十、小结
evlog 的 wide event 日志模型把一次请求的所有上下文压缩进一行 JSON,配合.evlog/logs/下的 NDJSON 文件与error.data.why/error.data.fix结构化错误字段,让错误排障、慢请求定位与请求链路追踪都变成"过滤 + 直读"的确定性工作。掌握本指南后,你可以:五分钟内在最新日志文件中定位某类错误或慢请求、按requestId还原完整请求上下文、用source字段区分端侧与服务端问题,并能解释文件日志为何存在或缺失(drain pipeline 的批处理、重试与 flush 语义)。这套方法论不局限于 Activepieces,任何接入 evlog 的 TypeScript 服务都可直接复用。
【免费下载链接】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),仅供参考