---
title: "审计日志"
description: "作为宽事件薄层的审计日志。由一个 enrichment 器、一个排出器包装器和一个辅助函数构成的防篡改轨迹。"
canonical_url: "https://evlog.zhcndoc.com/use-cases/audit/overview"
---
# 审计日志

> 作为宽事件薄层的审计日志。由一个 enrichment 器、一个排出器包装器和一个辅助函数构成的防篡改轨迹。

evlog 的审计层**不是一个并行系统**。审计事件是带有保留 `audit` 字段的宽事件。每个现有的基础组件（排出器、enricher、redact、tail-sampling）都可以照常使用。只需添加 **1 个 enricher + 1 个排出器包装器 + 1 个辅助函数**，即可启用审计日志。

<prompt :actions="["copy","cursor","claude"]" description="为我的应用添加审计日志" icon="i-lucide-shield-check">

在 evlog 之上为我的应用添加一份可防篡改的审计日志。

- 识别我的框架，并遵循其 evlog 集成模式
- 在 evlog:enrich 钩子上注册 auditEnricher()（或者在 initLogger.enrichers 中注册）
- 在主排出器旁边，注册一个独立的 auditOnly(signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' })) 排出器
- 在审计排出器上使用 { await: true }，以确保审计事件在响应返回前被刷新
- 对每个安全敏感操作都调用 log.audit({ action, actor, target, outcome, reason })（登录、角色变更、退款、数据导出、删除）
- 审计事件会强制保留，跳过采样，并通过哈希链签名以实现防篡改证明
- 与 Better Auth 集成结合后，actor.id / actor.email 会自动填充

文档：[https://www.evlog.dev/use-cases/audit/overview](https://www.evlog.dev/use-cases/audit/overview)
适配器：[https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## 智能体技能

安装 evlog 技能目录，以便你的助手能够端到端地遵循 **build-audit-logs**：书面策略、框架接入、`withAudit` / `log.audit`、拒绝、脱敏、多租户隔离、防篡改排出器，以及基于 grep 的审查流程。如果你使用文件系统排出器处理审计日志或常规日志，**analyze-logs** 将教助手读取 `.evlog/logs/` 下的 NDJSON。

```bash [终端]
npx skills add https://www.evlog.dev
```

查看 [智能体技能](https://evlog.zhcndoc.com/reference/agent-skills) 以获取完整列表。仓库中的技能路径：`skills/build-audit-logs`、`skills/analyze-logs`。

## 为什么需要审计日志？

合规框架（SOC2、HIPAA、GDPR、PCI）要求知道**谁在什么时间、从哪里、对哪个资源、做了什么，以及结果如何**。evlog 无需第二个日志库就能覆盖这一需求。

<tip>

**审计事件是关于意图的事实，而不是对操作的度量。**常规宽事件回答“此请求表现如何？”（延迟、状态、令牌）。审计事件回答“谁试图做什么，以及是否获准？”同一条管道，不同的问题，这也是模式被保留且事件在采样后仍被强制保留的原因。

</tip>

<audit-force-keep>



</audit-force-keep>

## 快速开始

你已经在使用 evlog 了。只需做三处改动即可添加审计日志：

```typescript [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 },
  ))
})
```

<framework-tabs>

```typescript [Nuxt]
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 }
})
```

```typescript [Nitro]
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
export default defineHandler(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 }
})
```

```typescript [Next.js]
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 })
})
```

```typescript [Hono]src/index.ts
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 })
})
```

```typescript [Express]src/index.ts
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 })
})
```

```typescript [Standalone]scripts/refund-job.ts
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',
})
```

</framework-tabs>

```json [输出 — 宽事件]
{
  "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": "demo@example.com" },
    "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`

<tip>

**为什么需要两个排出器？**主排出器（Axiom、Datadog……）将审计日志与其余遥测数据放在一起，这样仪表板和查询仍然可用。已签名的排出器是你的保险：如果主排出器发生中断、数据被清除，或管理员悄悄删除某一行，FS journal 仍然保留这条链。审计员两者都需要：快速查询*和*防篡改凭证。

</tip>

<audit-dual-sink>



</audit-dual-sink>

## 组合方式

每一层都是**可选接入并可替换**的。除了 `log.audit`、`auditEnricher` 以及 `auditOnly` / `signed` 之外，其他所有节点都与常规宽事件共享。

<audit-composition-flow>



</audit-composition-flow>

## 接下来去哪

<card-group>
<card icon="i-lucide-file-text" title="模式" to="https://evlog.zhcndoc.com/use-cases/audit/schema">

`AuditFields` 类型、动作命名约定、参与者类型，以及幂等性。

</card>

<card icon="i-lucide-pen-line" title="记录事件" to="https://evlog.zhcndoc.com/use-cases/audit/recording">

`log.audit`、`log.audit.deny`、独立的 `audit()`、`withAudit`、`defineAuditAction`、`defineAuditCatalog` 和 `auditDiff`。

</card>

<card icon="i-lucide-link" title="数据管道与完整性" to="https://evlog.zhcndoc.com/use-cases/audit/pipeline">

`auditEnricher`、`auditOnly` 和 `signed`（HMAC 和哈希链）数据管道包装器。

</card>

<card icon="i-lucide-shield-check" title="合规" to="https://evlog.zhcndoc.com/use-cases/audit/compliance">

完整性、脱敏预设、GDPR 与仅追加、保留策略，以及常见陷阱。

</card>

<card icon="i-lucide-book-open" title="配方" to="https://evlog.zhcndoc.com/use-cases/audit/recipes">

FS、Axiom 和 Postgres 配方，以及使用 `mockAudit` 进行测试和 API 参考。

</card>

:::

</card-group>

---

- [模式](https://evlog.zhcndoc.com/use-cases/audit/schema)
- [记录](https://evlog.zhcndoc.com/use-cases/audit/recording)
- [排出器与完整性](https://evlog.zhcndoc.com/use-cases/audit/pipeline)
- [合规](https://evlog.zhcndoc.com/use-cases/audit/compliance)
- [配方](https://evlog.zhcndoc.com/use-cases/audit/recipes)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
