审计日志

参考配方与 API 参考

用于审计日志的文件系统、Axiom 和 Postgres 配方,以及用于测试的 mockAudit 和完整的 API 参考。

选择与你的 sink 匹配的配方,把它放进去,你就拥有了一个防篡改的审计日志。每个配方都在不同的 drain 之上组合了相同的原语(auditOnlysigned、可选的 await: true)。

磁盘上的审计日志

import { auditOnly, signed } from 'evlog'
import { createFsDrain } from 'evlog/fs'

nitro.hooks.hook('evlog:drain', auditOnly(
  signed(createFsDrain({ dir: '.audit', maxFiles: 30 }), { strategy: 'hash-chain' }),
  { await: true },
))

每一行的 prevHash 都与前一行的 hash 相匹配。对任意一行进行篡改都会破坏该点之后的链条——验证器会重新计算这些哈希并报告第一个不匹配项。

写入专用 Axiom 数据集的审计日志

import { auditOnly } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

nitro.hooks.hook('evlog:drain', createAxiomDrain({ dataset: 'logs' }))
nitro.hooks.hook('evlog:drain', auditOnly(
  createAxiomDrain({ dataset: 'audit', apiKey: process.env.AXIOM_AUDIT_API_KEY }),
))

拆分数据集意味着审计数据集可以拥有更长的保留期(7 年)、更严格的访问控制,以及单独的计费项——而无需影响流水线的其余部分。

Postgres 中的审计日志

import { auditOnly } from 'evlog'
import type { DrainContext } from 'evlog'

const postgresAudit = async (ctx: DrainContext) => {
  await db.insert(auditEvents).values({
    id: ctx.event.audit!.idempotencyKey,
    timestamp: new Date(ctx.event.timestamp),
    payload: ctx.event,
  }).onConflictDoNothing()
}

nitro.hooks.hook('evlog:drain', auditOnly(postgresAudit, { await: true }))

确定性的 idempotencyKey 让重试变得安全——重复插入会通过 ON CONFLICT DO NOTHING 合并掉。没有它的话,重试期间的一次短暂网络抖动会创建重复的审计行,而这正是你不想要的。

测试审计

mockAudit() 在发出时捕获每个审计事件——包括独立的 audit()log.audit()log.set({ audit })

import { mockAudit } from 'evlog'

it('退款并记录审计', async () => {
  const captured = mockAudit()

  await refundInvoice({ id: 'inv_889' }, { actor: { type: 'user', id: 'u1' } })

  captured.assertAudit({
    action: 'invoice.refund',
    target: { type: 'invoice', id: 'inv_889' },
    outcome: 'success',
  })

  captured.restore()
})

当缺失审计应当以可读消息使测试失败时,优先使用 assertAudit()。当你需要在 expect(...) 中使用布尔值时,使用 toIncludeAuditOf()

始终在 afterEach 中调用 captured.restore()(或用 fixture 包裹),这样失败的断言就不会泄漏到下一个测试中。

API 参考

符号类型说明
AuditFields类型宽事件上的保留字段
defineAuditAction(name, opts?)工厂类型化的动作注册表,推断 target 形状
defineAuditCatalog(prefix, map)工厂共享前缀的类型化审计动作集合
log.audit(fields)方法log.set({ audit }) 的语法糖 + 强制保留
log.audit.deny(reason, fields)方法记录被拒绝的动作
audit(fields)函数供脚本 / 作业独立使用
withAudit({ action, target })(fn)包装器自动发出 success / failure / denied
auditDiff(before, after)助手面向 redact 的 JSON Patch,用于 changes
mockAudit()测试工具在发出时捕获审计;使用 assertAudit()toIncludeAuditOf()
auditEnricher(opts?)enrichr自动填充请求 / 运行时 / 租户上下文
auditOnly(drain, { await? })包装器仅路由包含 audit 字段的事件
signed(drain, opts)包装器通用完整性包装器(hmac / hash-chain)
auditRedactPreset配置审计事件的严格 PII 处理

所有内容都从主 evlog 入口导出。