Activepieces 审计日志(Audit Logs)全解:事件模型、捕获架构与生产级查询索引实践
【免费下载链接】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 开源仓库中的审计日志模块为核心,完整解析其事件模型、基于事件总线的捕获架构、平台管理员查询 API,以及从生产事故中沉淀出的索引设计与事件发射最佳实践。读完本文,你将掌握audit_event表的字段与索引设计、ApplicationEvent判别联合类型、GET /v1/audit-events的过滤与游标分页用法,以及如何避免"事件漏记"与"大表查询超时"这两类高发问题。
一、审计日志的定位与启用条件
Activepieces 的审计日志(Audit Logs)用于记录安全相关的用户与系统操作,为合规审计(compliance)与取证(forensics)提供依据。所有事件被持久化到audit_event表,并可由平台管理员(platform admin)查询。该功能仅对Enterprise / Cloud版本开放,由平台套餐字段platform.plan.auditLogEnabled控制。
在服务端,这一门控是在路由注册时通过 Fastify 的preHandler钩子强制执行的。参见 audit-event-module.ts:
export const auditEventModule: FastifyPluginAsyncZod = async (app) => { auditLogService(app.log).setup() app.addHook('preHandler', platformMustHaveFeatureEnabled((platform) => platform.plan.auditLogEnabled)) await app.register(auditEventController, { prefix: '/v1/audit-events' }) }这意味着:当套餐未开启auditLogEnabled时,/v1/audit-events相关请求会被直接拦截;同时前端平台管理页面的审计日志入口也会基于同一字段禁用(见下文 React Query hooks 中的enabled: platform.plan.auditLogEnabled)。
二、数据模型:ApplicationEvent 联合类型与 audit_event 表
2.1 ApplicationEvent:可审计事件的判别联合
所有可审计的事件类型被建模为一个discriminated union(判别联合),定义在 packages/core/shared/src/lib/ee/audit-events/index.ts。该联合由 20 个 Zod schema 组合而成(AgentAuditEvent、ConnectionEvent、VariableEvent、FlowCreatedEvent、FlowDeletedEvent、FlowUpdatedEvent、FlowPiecesUpgradedEvent、FlowPiecesRevertedEvent、FlowPublishedEvent、FlowActivatedEvent、FlowDeactivatedEvent、FlowRunEvent、AuthenticationEvent、FolderEvent、SignUpEvent、SigningKeyEvent、ProjectRoleEvent、ProjectReleaseEvent、ProjectReplacedEvent、FlowApprovalEvent),以action字段作为判别键,每个事件类型都复用一组公共信封字段:
const BaseAuditEventProps = { ...BaseModelSchema, // id / created / updated platformId: z.string(), projectId: z.string().optional(), projectDisplayName: z.string().optional(), userId: z.string().optional(), userEmail: z.string().optional(), ip: z.string().optional(), }公共信封 + 类型化的data载荷,既保证了入库数据的结构一致,也让消费端(摘要生成、前端渲染、测试)可以按事件类型安全地访问载荷。
2.2 ApplicationEventName:39 个事件名枚举
ApplicationEventName是事件的字符串枚举。当前源码中共有39 个值(早期内部笔记记录为 27 个,仓库已持续扩充),按领域可归纳为下表:
| 领域 | 事件名 |
|---|---|
| 流程生命周期 | flow.created、flow.deleted、flow.updated、flow.published、flow.activated、flow.deactivated |
| 流程运行 | flow.run.started、flow.run.finished、flow.run.resumed、flow.run.retried |
| 流程维护 | flow.pieces.upgraded、flow.pieces.reverted、flow.approval.requested、flow.approval.granted、flow.approval.rejected、flow.approval.withdrawn |
| 文件夹 | folder.created、folder.updated、folder.deleted |
| 连接 | connection.upserted、connection.deleted |
| 变量 | variable.upserted、variable.deleted、variable.value.revealed |
| Agent | agent.created、agent.updated、agent.deleted、agent.published、agent.unpublished |
| 认证与用户 | user.signed.up、user.signed.in、user.password.reset、user.email.verified |
| 平台治理 | signing.key.created、project.role.created、project.role.updated、project.role.deleted、project.release.created、project.replaced |
值得注意的细节:variable.value.revealed(变量值被查看)与signing.key.created(签名密钥创建)属于安全敏感事件,说明审计范围不止覆盖业务流程,还覆盖凭证类操作。而project.replaced事件的数据载荷中包含了 flows / tables / folders / connections 各自的 created / updated / deleted / unchanged 计数与outcome、durationMs等字段,为项目替换类批量操作提供了完整的事后统计(见 index.ts)。
2.3 audit_event 表结构与索引
audit_event表以 TypeORMEntitySchema定义,见 audit-event-entity.ts。字段如下:
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
id/created/updated | — | 否 | BaseColumnSchemaPart提供的公共列 |
platformId | String | 否 | 所属平台,外键关联platform,级联删除 |
projectId | String | 是 | 所属项目(后台/系统级事件可无项目) |
action | String | 否 | 事件名,对应ApplicationEventName |
userEmail | String | 是 | 操作者邮箱(系统事件可空) |
projectDisplayName | String | 是 | 项目显示名快照 |
data | jsonb | 否 | 类型化事件载荷,JSONB 存储 |
ip | String | 是 | 客户端真实 IP(请求来源时填充) |
userId | String | 是 | 操作者用户 ID |
表上定义了 4 个复合索引(audit-event-entity.ts):
audit_event_platform_id_project_id_user_id_action_idx:(platformId, projectId, userId, action),最窄组合,覆盖最常见的过滤场景;audit_event_platform_id_user_id_action_idx:(platformId, userId, action);audit_event_platform_id_action_idx:(platformId, action);audit_event_platform_id_created_id_desc_idx:(platformId, created, id)——这个索引与分页排序强相关,详见第五节。
data采用 jsonb 而非拆列,使事件载荷可以随业务演进自由扩展,无需为每个新事件类型做 DDL 迁移。
三、事件捕获架构:基于事件总线的解耦监听
审计日志的核心设计思想是通过事件总线(applicationEvents)解耦捕获:业务代码只负责"发事件",审计模块只负责"听事件",二者互不感知。
auditLogService.setup()在模块初始化时调用(audit-event-service.ts):
export const auditLogService = (log: FastifyBaseLogger) => ({ setup(): void { applicationEvents(log).registerListeners(log, { userEvent: (log) => async (params) => { rejectedPromiseHandler(auditLogRepo().save(params), log) }, workerEvent: (log) => async (_projectId, params) => { rejectedPromiseHandler(auditLogRepo().save(params), log) }, }) }, // ... })两个监听器分别对应两类事件来源,均为 fire-and-forget(异步落库、异常不抛出):
userEvent:由 HTTP 请求触发的用户操作(如登录、编辑流程),事件参数中已携带完整的userEmail、projectDisplayName、ip等上下文;workerEvent:由后台 Worker 触发的操作(如流程运行、项目替换),以(projectId, params)形式传入,事件总线会补上id、created、updated等信封字段。
总线实现在 application-events.ts:sendUserEvent()会对请求来源做上下文丰富(enrichAuditEventParam),包括解析出真实用户 ID、项目、项目显示名、用户邮箱与真实客户端 IP,再把事件分发给所有userEventListeners;sendWorkerEvent()则直接构造事件并广播给workerEventListeners。rejectedPromiseHandler保证落库失败不会反噬主业务流程。
3.1 上下文如何被丰富
对于请求来源的事件,extractMetaInformation()会从 Fastify 请求中提取:
platformId:取principal.platform.id;projectId:取request.projectId ?? principal.projectId;userId:经authenticationUtils.extractUserIdFromRequest()解析;ip:经networkUtils.extractClientRealIp()从CLIENT_REAL_IP_HEADER指定的请求头提取。
随后事件总线再依据userId反查用户邮箱、依据projectId反查项目显示名,把"裸事件"丰富成可读、可审计的完整记录(application-events.ts)。这也是为什么审计日志能直接展示"谁、在哪个项目、什么 IP、做了什么"——这些信息在事件源头发送时可能并不完整。
四、查询 API 与前端集成
4.1 GET /v1/audit-events
路由挂载于packages/server/api/src/app/ee/audit-logs/audit-event-module.ts,前缀/v1/audit-events,仅平台管理员可访问(securityAccess.platformAdminOnly([PrincipalType.SERVICE, PrincipalType.USER]))。控制器将查询参数透传给auditLogService.list(),返回SeekPage<ApplicationEvent>,按created倒序排列。
请求参数由 Zod schemaListAuditEventsRequest定义(index.ts):
| 参数 | 类型 | 说明 |
|---|---|---|
limit | number(可选) | 每页条数,未传时服务端默认 20 |
cursor | string(可选) | 游标分页令牌 |
action | string[](可选,数组) | 按事件名过滤,支持多个 |
projectId | string[](可选,数组) | 按项目过滤,支持多个 |
userId | string(可选) | 按用户 ID 过滤 |
createdBefore | string(可选) | 只返回该时间点之前的事件 |
createdAfter | string(可选) | 只返回该时间点之后的事件 |
服务端的过滤实现(audit-event-service.ts)以platformId为强制过滤条件(平台数据隔离),再按userId等值、action/projectId的IN列表、created的区间逐层叠加andWhere,最后交给buildPaginator完成游标分页。集成测试 audit-event.test.ts 验证了两点:管理员能列出本平台全部事件;非 owner(PlatformRole.MEMBER)请求返回403 FORBIDDEN。
4.2 前端:API client、React Query hooks 与管理页面
前端数据链路清晰分层:
- audit-events-api.ts:封装
GET /v1/audit-events请求; - audit-log-hooks.ts:React Query 的
useAuditLogs()hook,把 URL 查询参数(cursor、limit、action[]、projectId[]、userId、createdBefore/After)映射为 API 参数,并设置enabled: platform.plan.auditLogEnabled—— 套餐未开启时直接不发起请求; - UI 页面位于 packages/web/src/app/routes/platform/security/audit-logs/,供平台管理员在"安全"菜单下按事件类型筛选、按时间区间查询。
五、从生产事故中沉淀的实战陷阱(Gotchas)
该模块的知识库笔记记录了几条极具工程价值的生产教训,以下逐条展开。
5.1 事件必须由"执行操作的服务"发出,而非调用方
事故 #14591 的根因:flow 类事件当初在flow.controller.ts中发射,导致所有绕过 controller 直接调用 flow 服务代码的路径——包括 15 个 MCP flow 工具、app-connection.handler.ts、worker-rpc-service.ts、project-state-helper.ts、platform-teardown-jobs.ts——在修改流程时完全没有留下审计记录。而 flowruns从未出现该问题,因为运行事件经由flow-run-service.ts内的flowRunSideEffects发出。
由此形成两条硬性规范:
- 把
*-side-effects.ts钩子放进执行操作的服务内部,默认开启事件发射,而不是放在 controller 层或让每个调用方各自发射; - 批量/系统路径若确实需要静默(如 project release apply、platform teardown),必须显式传
emitEvents: false退出,让"这次不记审计"成为一个可审查的显式决策,而非意外遗漏。
此外,schema 中ip是可选的——正确做法是把它作为 controller 传入的一个可选参数向下传递,而不是为了让事件拿到 IP 就把发射逻辑留在 controller 层。仓库中新增的 mcp-flow-audit-trail.test.ts 正是针对 MCP 工具链审计追踪的回归测试。
5.2 排序与索引:分页必须覆盖排序列
列表接口的排序规则是created DESC, id DESC——Paginator会自动追加id作为平局决胜键(withIdTiebreaker),因此索引必须同时覆盖这两列。
- 只有
(platformId, created DESC):查询计划会在索引之上叠加 Incremental Sort 节点; - 只有
(platformId, created DESC, id DESC):是纯索引扫描(plain index scan),即当前实体中定义的audit_event_platform_id_created_id_desc_idx; - 若两者都没有:Postgres 只能借助
platformId开头的action索引读完整平台的所有行,再全量排序后只返回一页(如 11 条),导致语句超时、页面 500(对应事故 GIT-1705)。
文档记录的生产规模数据可佐证问题的严重性:Cloud 生产环境(2026 年 8 月)audit_event已达约 3.62 亿行 / 475 GB,单个平台约 650 万行,错误查询计划代价高达 740 万且永不结束。同时该表从不清理(GIT-1574),因此任何新增查询形态都需要设计覆盖排序的索引,而不只是覆盖过滤条件。
5.3 在生产大表上建索引是"运维操作",不是"迁移步骤"
在 475 GB 的表上,CREATE INDEX CONCURRENTLY需要运行数小时。而项目迁移在main.ts中、服务开始监听之前执行——启动期构建索引永远无法及时通过健康检查,部署会被回滚到一个只建了一半的索引上。因此规范做法是:
- 在部署前手工构建索引,让迁移里的
IF NOT EXISTS直接跳过(no-op); CREATE INDEX CONCURRENTLY同样受statement_timeout约束,构建会话中需先SET statement_timeout = 0;若角色级超时已设置,启动期路径会直接失败;- 被中断的 CONCURRENTLY 构建会留下
indisvalid = false的索引,普通IF NOT EXISTS重试会跳过它并"报告成功",但查询规划器永远不会使用这个索引——这也是 1820 号迁移要检查pg_index.indisvalid并在重建前删除无效残留的原因。
5.4 游标分页的演进:同一秒内的事件不再被跳过
如果读到过"该分页器用DATE_TRUNC('second', created)生成游标"的旧资料,那是过时信息。当前实现改为选择created::text,并生成复合游标(created < c) OR (created = c AND id < i),彻底修复了"同一秒内的事件跨页被跳过"的旧缺陷。也就是说,分页在created与id两层上都是稳定、唯一的。
六、摘要生成与测试基建
summarizeApplicationEvent()(index.ts)将每个事件渲染成一句话的人类可读摘要。其中flow.updated最复杂:它依据FlowOperationType(ADD_ACTION、UPDATE_ACTION、DELETE_ACTION、CHANGE_NAME、LOCK_AND_PUBLISH、MOVE_ACTION、ADD/DUPLICATE/DELETE/MOVE_BRANCH、ADD/UPDATE/DELETE_NOTE 等 20 余种操作)生成精确的细节描述,例如Added action "Send Email" to "Order Flow" Flow.或Deleted actions "step_1, step_2" from "Order Flow" Flow.。运行类事件则输出Flow run <id> is started / finished / retried from a failed step,审批类事件附带rejectionReason。
配套的buildMockEvent()(mock-event-builder.ts)为每个事件名生成一个带完整合法载荷的 typed mock(统一使用ip: '127.0.0.1'等固定测试数据),既用于事件目的地(event-destination)测试投递,也是后续新增事件类型时"一键产出样例数据"的基座。
七、关键文件索引
- 服务端模块:packages/server/api/src/app/ee/audit-logs/(含 audit-event-module.ts、audit-event-service.ts、audit-event-entity.ts)
- 事件总线:packages/server/api/src/app/helper/application-events.ts
- 事件类型与工具:packages/core/shared/src/lib/ee/audit-events/(
ApplicationEvent联合、ApplicationEventName枚举、summarizeApplicationEvent()、buildMockEvent()) - 前端:audit-events-api.ts、audit-log-hooks.ts、审计日志管理页面
- 集成测试:packages/server/api/test/integration/cloud/audit-event/(含 audit-event.test.ts 与 mcp-flow-audit-trail.test.ts)
- 面向用户的文档:docs/admin-guide/security/audit-logs/(每种事件类型一个文档页)
结语
Activepieces 的审计日志模块是一套"事件模型 + 事件总线 + 门控查询"三位一体的实现:判别联合的事件类型保证了数据结构的可扩展性,applicationEvents总线实现了业务与审计的解耦,而auditLogEnabled门控与platformAdminOnly权限控制保证了只有合规范围内的人员能访问。真正值得借鉴的是那些踩坑记录——把事件发射放在服务内部、为created + id排序设计复合索引、在超大规模表上以运维流程而非启动迁移的方式建索引——这些实践同样适用于任何需要长期累积、按时间倒序查询的海量审计表。
【免费下载链接】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),仅供参考