---
title: "排水器与完整性"
description: "auditEnricher 用于自动填充请求上下文，auditOnly 用于将审计路由到专用 sink，而 signed 用于提供可检测篡改的 HMAC 或哈希链完整性。"
canonical_url: "https://evlog.zhcndoc.com/use-cases/audit/pipeline"
---
# 排水器与完整性

> auditEnricher 用于自动填充请求上下文，auditOnly 用于将审计路由到专用 sink，而 signed 用于提供可检测篡改的 HMAC 或哈希链完整性。

三个构建模块：`auditEnricher` 填充上下文，`auditOnly` 将审计路由到专用 drain，而 `signed` 添加可检测篡改的完整性。每个都是可选启用且可替换的。

## `auditEnricher()`

`auditEnricher()` 会填充 `event.audit.context.{requestId, traceId, ip, userAgent, tenantId}`。如果你的策略不同，可以跳过它并提供自定义 enricher。

```typescript [server/plugins/evlog.ts]
import { auditEnricher } from 'evlog'

nitro.hooks.hook('evlog:enrich', auditEnricher())
```

对于多租户应用和自定义会话桥接，请传入选项：

```typescript
nitro.hooks.hook('evlog:enrich', auditEnricher({
  tenantId: ctx => ctx.event.tenant as string | undefined,
  bridge: { getSession: async ctx => readSessionActor(ctx.headers) },
}))
```

不使用 `auditEnricher` 时，`audit.context` 会保持为空，而审计人员和事件响应人员至少需要 `requestId` 和 `ip` 才能对记录的操作进行交叉定位。

## `auditOnly()`

<tip>

**为什么要将审计过滤到单独的 drain？** 有三个原因：**成本**（与产品遥测相比，审计量很小，因此将它们分开可以避免保留成本失控）、**权限**（审计数据集应对工程师只读，而应用只能写入）以及**保留期限**（审计通常需要保存 7 年以上，而产品日志很少保存超过 90 天）。

</tip>

`auditOnly(drain)` 只转发带有 `audit` 字段的事件。可与**任何** drain 组合：

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

// 将审计发送到专用的 Axiom 数据集：
nitro.hooks.hook('evlog:drain', auditOnly(
  createAxiomDrain({ dataset: 'audit', apiKey: process.env.AXIOM_AUDIT_API_KEY }),
))
```

设置 `await: true` 可使审计写入同步执行（审计不会 fire-and-forget，因此默认可安全应对崩溃）：

```typescript
auditOnly(createFsDrain({ dir: '.audit' }), { await: true })
```

`await: true` 标志会为每个记录审计的请求带来一次等待 drain 调用的延迟：承载审计的 drain 步骤会在写入完成后才结束，而不是触发后立即继续。设置了 `waitUntil` 时（Next.js、Cloudflare Workers、Vercel Edge），该步骤会在响应之后运行，运行时会一直存活到写入完成。在 `log.fork()` 中记录的审计会由子事件排空，因此当写入必须在处理程序返回前完成时，请等待 `await log.fork(...)`。对于合规级审计来说，这个权衡总是值得的。

## `signed()`

`signed(drain, opts)` 会添加可检测篡改的完整性。它有两种策略：

<table>
<thead>
  <tr>
    <th>
      策略
    </th>
    
    <th>
      它增加的内容
    </th>
    
    <th>
      使用场景
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'hmac'
      </code>
    </td>
    
    <td>
      <code>
        event.audit.signature
      </code>
      
      （规范化事件的 HMAC）
    </td>
    
    <td>
      单事件完整性检查（任何后续修改都会验证失败）。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'hash-chain'
      </code>
    </td>
    
    <td>
      <code>
        event.audit.prevHash
      </code>
      
       和 <code>
        event.audit.hash
      </code>
    </td>
    
    <td>
      可验证的链——删除和重排也会变得可检测。
    </td>
  </tr>
</tbody>
</table>

<tip>

**signed() 实际上能带来什么。** 检测，而非预防。任何拥有底层 drain 写入权限的人仍然可以删除文件或表，但该链可以在事后证明哪些事件被删除或修改过。如果你已经写入仅追加／WORM 存储（S3 Object Lock、具有行级不可变性的 Postgres、仅追加的 BigQuery 表），请跳过 `signed()`；重复添加完整性层只会增加延迟，却不会提高门槛。

</tip>

### HMAC

每个事件都会获得一个签名。篡改某一行会破坏该行的验证，但不会破坏后续行。

```typescript
import { signed } from 'evlog'

signed(drain, { strategy: 'hmac', secret: process.env.AUDIT_SECRET! })
```

### 哈希链

每个事件都引用前一个事件的哈希。删除任何一行都会破坏该点之后的链，因此验证器可以精确定位被篡改的那一行。

```typescript
signed(drain, {
  strategy: 'hash-chain',
  state: {
    load: () => fs.readFile('.audit/head', 'utf8').catch(() => null),
    save: (h) => fs.writeFile('.audit/head', h),
  },
})
```

对于跨进程或持久化链路，`state` 配置是必需的：在每个事件之前，从你自己的存储（Redis、Postgres、文件）中加载上一个 head hash，并在之后保存新的 head。

<hash-chain-tamper>



</hash-chain-tamper>

<note>

用于遍历并验证链的 CLI（`evlog audit verify`）已列入路线图。在此之前，请通过重新计算已存储事件的哈希，并将每个 `prevHash` 与前一个事件的 `hash` 进行比较来验证。
::。

</note>

---

- [记录](https://evlog.zhcndoc.com/use-cases/audit/recording)
- [合规](https://evlog.zhcndoc.com/use-cases/audit/compliance)


## Sitemap

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