evlog 的审计层不是一个并行系统。审计事件是带有保留 audit 字段的宽事件。每个现有的基础组件(排出器、enricher、redact、tail-sampling)都可以照常使用。只需添加 1 个 enricher + 1 个排出器包装器 + 1 个辅助函数,即可启用审计日志。
为我的应用添加审计日志
智能体技能
安装 evlog 技能目录,以便你的助手能够端到端地遵循 build-audit-logs:书面策略、框架接入、withAudit / log.audit、拒绝、脱敏、多租户隔离、防篡改排出器,以及基于 grep 的审查流程。如果你使用文件系统排出器处理审计日志或常规日志,analyze-logs 将教助手读取 .evlog/logs/ 下的 NDJSON。
终端
npx skills add https://www.evlog.dev
查看 智能体技能 以获取完整列表。仓库中的技能路径:skills/build-audit-logs、skills/analyze-logs。
为什么需要审计日志?
合规框架(SOC2、HIPAA、GDPR、PCI)要求知道谁在什么时间、从哪里、对哪个资源、做了什么,以及结果如何。evlog 无需第二个日志库就能覆盖这一需求。
**审计事件是关于意图的事实,而不是对操作的度量。**常规宽事件回答“此请求表现如何?”(延迟、状态、令牌)。审计事件回答“谁试图做什么,以及是否获准?”同一条管道,不同的问题,这也是模式被保留且事件在采样后仍被强制保留的原因。
tail-sample gate· keep rate 10%
incominggate decisionoutcome
#1POST/api/checkout
random 0.42 < 0.10
dropped
#2GET/api/users
random 0.91 < 0.10
dropped
#3POST/api/refund
audit · force
kept
#4GET/api/me
random 0.07 < 0.10
kept
#5POST/api/login
audit · force
kept
#6PATCH/api/cart
random 0.55 < 0.10
dropped
#7DELETE/api/account
audit · force
kept
#8GET/api/health
random 0.83 < 0.10
dropped
audit kept0 / 3 (100%)
regular kept0 / ~10%
dropped0
快速开始
你已经在使用 evlog 了。只需做三处改动即可添加审计日志:
server/plugins/evlog.ts
import { auditEnricher, auditOnly, signed } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createFsDrain } from 'evlog/fs'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('evlog:enrich', auditEnricher())
nitro.hooks.hook('evlog:drain', createAxiomDrain())
nitro.hooks.hook('evlog:drain', auditOnly(
signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' }),
{ await: true },
))
})
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const user = await requireUser(event)
const invoice = await refundInvoice(getRouterParam(event, 'id'))
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id, email: user.email },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
reason: 'Customer requested refund',
})
return { ok: true }
})
import { withEvlog, useLogger } from '@/lib/evlog'
export const POST = withEvlog(async (req, { params }) => {
const log = useLogger()
const user = await requireUser(req)
const invoice = await refundInvoice(params.id)
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id, email: user.email },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
reason: 'Customer requested refund',
})
return Response.json({ ok: true })
})
import type { EvlogVariables } from 'evlog/hono'
import { Hono } from 'hono'
const app = new Hono<EvlogVariables>()
app.post('/invoices/:id/refund', async (c) => {
const log = c.get('log')
const user = await requireUser(c)
const invoice = await refundInvoice(c.req.param('id'))
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id, email: user.email },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
reason: 'Customer requested refund',
})
return c.json({ ok: true })
})
import type { Request, Response } from 'express'
app.post('/invoices/:id/refund', async (req: Request, res: Response) => {
const log = req.log
const user = await requireUser(req)
const invoice = await refundInvoice(req.params.id)
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id, email: user.email },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
reason: 'Customer requested refund',
})
res.json({ ok: true })
})
import { audit } from 'evlog'
audit({
action: 'invoice.refund',
actor: { type: 'system', id: 'billing-worker' },
target: { type: 'invoice', id: 'inv_889' },
outcome: 'success',
reason: 'Auto-refund triggered by chargeback webhook',
})
{
"level": "info",
"service": "billing-api",
"method": "POST",
"path": "/api/invoices/inv_889/refund",
"status": 200,
"duration": "84ms",
"durationMs": 84,
"requestId": "a566ef91-7765-4f59-b6f0-b9f40ce71599",
"audit": {
"action": "invoice.refund",
"actor": { "type": "user", "id": "usr_42", "email": "[email protected]" },
"target": { "type": "invoice", "id": "inv_889" },
"outcome": "success",
"reason": "Customer requested refund",
"version": 1,
"idempotencyKey": "ak_8f3c4b2a1e5d6f7c",
"context": {
"requestId": "a566ef91-7765-4f59-b6f0-b9f40ce71599",
"ip": "203.0.113.7",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
}
}
}
就是这样。审计事件:
- 与其余日志一样,经过同一条宽事件管道
- 在尾部采样后始终保留
- 写入你的主排出器(Axiom),并且写入一个专用的、已签名的、仅追加排出器(FS journal)
- 通过
auditEnricher自动携带requestId、traceId、ip和userAgent
**为什么需要两个排出器?**主排出器(Axiom、Datadog……)将审计日志与其余遥测数据放在一起,这样仪表板和查询仍然可用。已签名的排出器是你的保险:如果主排出器发生中断、数据被清除,或管理员悄悄删除某一行,FS journal 仍然保留这条链。审计员两者都需要:快速查询和防篡改凭证。
drain pipeline· two drains, one event
active wide event
waiting for events…
main drain
createAxiomDrain()
0events ingested
audit drain
auditOnly(signed(fs))
0audits sealed
main drain · queryable audit drain · tamper-evident · long retention 0 non-audit events filtered out
组合方式
每一层都是可选接入并可替换的。除了 log.audit、auditEnricher 以及 auditOnly / signed 之外,其他所有节点都与常规宽事件共享。
audit pipeline · 1 event, 2 sinks·flowing
- log.audit / audit / withAudit audit
callsite
- set event.audit audit
reserved field
- force-keep tail-sample audit
never dropped
- auditEnricher() audit
+ requestId · ip · ua
- redact + auditRedactPreset shared
PII scrubbed
main drain
Axiom · Datadog · Sentry · …
auditOnly(signed(fsDrain))
hash-chain · WORM · 7y retention
audit-only layer shared with regular wide events 1 event · 2 sinks · 0 duplicates