---
title: "为什么从 evlog 开始"
description: "添加结构化日志最省成本的时机是在第一次请求之前。在第 0 天选择 evlog，系统的其余部分都会继承它。"
canonical_url: "https://evlog.zhcndoc.com/start/why-evlog"
---
# 为什么从 evlog 开始

> 添加结构化日志最省成本的时机是在第一次请求之前。在第 0 天选择 evlog，系统的其余部分都会继承它。

添加结构化日志最省成本的时机是在**第一次请求之前**。等你拥有 200 个路由、40 个后台任务，并且每个文件里都有一个 `console.log` 时，你就要开始为一个从未真正做出过的决定支付利息了。evlog 专为第 0 天的选择而设计：只需选择一次，系统的其余部分就会继承结构化日志、结构化错误、类型化目录、AI SDK 遥测、审计轨迹，以及无需日后自行构建的 drain 管道。

<callout color="neutral" icon="i-lucide-arrow-right">

已经在使用 `console.log` 或 pino 发布了吗？evlog 依然更胜一筹，但理由有所不同。参见 [evlog vs pino、winston、consola](https://evlog.zhcndoc.com/reference/vs-other-loggers)。

</callout>

## 从第一天起你就能得到什么

evlog 不是一个供你包装的小型原语。它是一个单一依赖，默认提供结构化接口、集成生态和有明确设计取舍的 drain 管道。

### 日志基础原语

<card-group>
<card icon="i-lucide-shield" title="自动脱敏">

PII（邮箱、银行卡、IP、电话号码、JWT、Bearer 令牌、IBAN）会在控制台输出之前以及任何 drain 之前被掩码。参见 [自动脱敏](https://evlog.zhcndoc.com/learn/redaction)。

</card>

<card icon="i-lucide-shield-alert" title="结构化错误">

每个错误都已经携带 `why`、`fix` 和 `link`，可直接给值班人员（以及你未来的 AI 代理）使用。参见 [结构化错误](https://evlog.zhcndoc.com/learn/structured-errors)。

</card>

<card icon="i-lucide-layers" title="宽事件">

在一次操作过程中累积上下文，并在操作结束时发出一个类型化事件，这是让请求、任务和工作流能够端到端查询的可观测性模式。参见 [宽事件](https://evlog.zhcndoc.com/learn/wide-events)。

</card>

<card icon="i-lucide-book-marked" title="类型化目录">

`defineErrorCatalog` 和 `defineAuditCatalog` 为你提供类似枚举、可安全重构的代码和动作。可以从两个条目开始，扩展到一个已发布的包。参见 [目录](https://evlog.zhcndoc.com/learn/catalogs)。

</card>

<card icon="i-lucide-percent" title="头采样 + 尾采样">

在发出时丢弃低重要性事件，对慢请求和错误强制保留。配置一次，长期受益。参见 [采样](https://evlog.zhcndoc.com/learn/sampling)。

</card>

<card icon="i-lucide-database" title="Drain 管道">

内置批处理、带指数退避的重试，以及向多个目标进行扇出。无需自行编写 drain 管道。

</card>
</card-group>

### 不止是 logger

这些原语只是基本要求，每个现代 logger 都具备其中某种形式。evlog 在第 1 天就能占据一席之地，靠的是围绕它们连接起来的所有能力，用来解决那些你尚未遇到的问题。

**想象一下你给应用添加 AI。** 迟早你会把 Vercel AI SDK 接入某个路由。令牌成本让你感到意外，模型在流式传输中途卡住，工具返回垃圾结果。借助 [AI SDK 集成](https://evlog.zhcndoc.com/use-cases/ai-sdk/overview)，每次模型调用都会自动成为一个包含提示词、工具、令牌、延迟和成本的宽事件。而且如果你使用 [Better Auth](https://evlog.zhcndoc.com/use-cases/better-auth/overview)，evlog 会为你将操作者身份关联到这些事件，这样无需编写任何管道代码，你就能回答“刚才是哪个用户在一次对话中花掉了 14 美元？”

**想象一下你的技术栈横跨多个框架。** 一个 Nuxt 前端、一个 Hono 内部服务、一个 AWS Lambda webhook：大多数团队最终都会形成这样的组合。evlog 拥有 [13+ 个框架集成](https://evlog.zhcndoc.com/integrate/frameworks/overview)，每个集成都提供相同的日志原语（`useLogger`、`log.set`、`createError`）。处理器本身仍然保持框架的形态，这部分由你负责，但每次跨越一个运行时，你无需重新学习一个 logger，它们背后的 drain 管道也始终保持一致。

**想象一下你希望开发环境中的日志比生产环境更嘈杂。** 在本地开发期间，你会添加一些 `log.debug` 调用（完整的请求体、每次重试、每个守卫），以便真正看清发生了什么。这些内容不应该被发布到线上。[Vite 插件](https://evlog.zhcndoc.com/reference/vite-plugin) 会在构建时移除选定的日志级别，因此开发环境拥有你想要的详细上下文，而生产环境保持简洁。除此之外，每个保留下来的调用都会自动注入其源代码位置（`file.ts:42`），因此当事件进入你的仪表板时，你能准确知道是哪一行发出的。

**想象一下用户从浏览器报告了一个 bug。** 错误发生在用户的会话中，深藏在一个你无法复现的 fetch 内部。evlog 的[浏览器 logger](https://evlog.zhcndoc.com/use-cases/client-logging) 会将客户端事件发送到你的服务器，在那里它们会与请求的其余部分合并成同一个宽事件：无论错误从哪里产生，最终都是一个类型化事件。将它与[内置增强器](https://evlog.zhcndoc.com/use-cases/enrichers)结合使用，你还可以免费获得附加的 UA、GeoIP 和 W3C trace context。

**想象一下你的技术栈更换了供应商。** 你最初使用 stdout，后来签约 Axiom 进行查询，然后你的团队又希望使用 Sentry 处理错误、使用 PostHog 进行产品分析。借助 [9+ 个 drain 适配器](https://evlog.zhcndoc.com/integrate/adapters/overview)和内置扇出，这些事件会并行发送到所有地方，而应用代码无需改动。通过[文件系统](https://evlog.zhcndoc.com/integrate/adapters/self-hosted/fs)或 [NuxtHub](https://evlog.zhcndoc.com/integrate/adapters/self-hosted/nuxthub)适配器，也可以直接切换为自行托管。

这些都不是“v2 功能”。从第 1 天起，它们就是同一个包、同一个 `log` API 的一部分。

## 目录会随着你一起成长

最小且有用的目录只需要两项：

```typescript [src/errors.ts]
import { defineErrorCatalog } from 'evlog'

export const errors = defineErrorCatalog('billing', {
  PAYMENT_DECLINED: { status: 402, message: '支付被拒绝' },
  INVOICE_NOT_FOUND: { status: 404, message: '未找到发票' },
})
```

六个月后，它会有三十项条目，类型增强会让你在任何地方对 `createError({ code })` 拥有自动补全，而你会把它作为一个私有 npm 包发布到整个 monorepo。**同一种模式，无需重写。**

审计目录（`defineAuditCatalog`）也是如此：从一个动作开始，逐步成长为你的合规地图。参见[目录](https://evlog.zhcndoc.com/learn/catalogs)。

## 为 AI 编码代理时代而构建

越来越多的应用是使用 AI 编码代理构建的：Cursor、Codex、Claude Code、Copilot。它们擅长编写处理器，却不太擅长调试处理器。**它们需要的是上下文。**

- **带有 why / fix / link 的结构化错误。** 模糊的 `Error: failed` 让人无法理解；`createError({ message, why, fix, link })` 则是代理可以读取、总结或呈现给用户的内容，而无需你连接一个翻译层。
- **作为每个请求单一事实来源的宽事件。** 代理可以端到端推理一个类型化事件，而不是在日志行之间进行 grep。
- **作为类似枚举接口的类型化目录。** 代理不会自行发明错误代码，而是从 `errors.PAYMENT_DECLINED`、`audit.INVOICE_REFUND` 等选项中选择，并通过目录获得自动补全。
- **每次 LLM 调用都具备 AI SDK 遥测。** 当代理自己的模型调用失败、产生幻觉或消耗预算时，宽事件会告诉你使用了哪个提示词、哪些工具、多少令牌以及花费了多少成本。
- **内置代理技能。** evlog 提供了[代理技能](https://evlog.zhcndoc.com/reference/agent-skills)，因此 Cursor / Claude 已经知道如何接入，无需手动进行提示词工程。

<callout color="info" icon="i-lucide-bot">

如果你团队的效率来自 AI 编码代理，那么日志的质量 *就是* 它们上下文窗口的质量。第 0 天就是你设定这个上限的时候。

</callout>

## 审计与合规：现在便宜，以后昂贵

每个产品最终都会遇到以下之一：GDPR 数据导出请求、SOC 2 准备、医疗健康领域的 HIPAA、支付领域的 PCI，或者仅仅是一场事故复盘，有人会问 **“是谁删了那个？”**。获取一条审计轨迹有两种方式：

1. **第 1000 天的方式。** 搭建一个并行系统。确定一个架构。尽可能回填数据。从请求头中反向推断操作者身份。在截止期限的压力下发布。希望没有遗漏任何内容。
2. **第 0 天的方式。** 添加 `auditEnricher()`，并在任何会修改状态的处理器中调用 `log.audit({ action, actor, target })`。在你原本就会发出的宽事件之上，evlog 提供哈希链完整性、保留策略，以及超越采样机制的强制保留。

evlog 的审计层**不是一个并行系统**。它就是你已经在使用的同一个 `log`，只是带有一个保留的 `audit` 字段。参见[审计日志](https://evlog.zhcndoc.com/use-cases/audit/overview)。

<callout color="success" icon="i-lucide-shield-check">

受监管行业（金融科技、健康科技、B2B SaaS）中的团队应将第 0 天的审计日志视为基本要求，而不是 v2 功能。

</callout>

## 从第一天起就与 drain 无关

你的应用代码从不依赖某个供应商。它只向 drain 管道发出事件。第 0 天，这在开发环境中是 stdout，在 CI 中则是[文件系统 drain](https://evlog.zhcndoc.com/integrate/adapters/self-hosted/fs)。当你决定查询日志的那一天：

<framework-tabs>

```typescript [Nuxt]nuxt.config.ts
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export default defineNuxtConfig({
  evlog: {
    drain: {
      adapters: [
        createAxiomDrain({ apiKey: '...', dataset: 'app' }),
        createSentryDrain({ dsn: '...' }),
      ],
    },
  },
})
```

```typescript [Nitro]nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export default defineConfig({
  modules: [evlog({
    drain: composeDrains([
      createAxiomDrain({ apiKey: '...', dataset: 'app' }),
      createSentryDrain({ dsn: '...' }),
    ]),
  })],
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export const { handle, handleError } = createEvlogHooks({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [TanStack Start]nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export default defineConfig({
  modules: [evlog({
    drain: composeDrains([
      createAxiomDrain({ apiKey: '...', dataset: 'app' }),
      createSentryDrain({ dsn: '...' }),
    ]),
  })],
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    drain: composeDrains([
      createAxiomDrain({ apiKey: '...', dataset: 'app' }),
      createSentryDrain({ dsn: '...' }),
    ]),
  }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

app.use(evlog({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

app.use(evlog({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

await app.register(evlog, {
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

app.use(evlog({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

EvlogModule.forRoot({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

const handler = withEvlog(new RPCHandler(router), {
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

export default withEvlog(handler, {
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { composeDrains } from 'evlog/toolkit'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

initLogger({
  drain: composeDrains([
    createAxiomDrain({ apiKey: '...', dataset: 'app' }),
    createSentryDrain({ dsn: '...' }),
  ]),
})
```

</framework-tabs>

无需修改任何处理器。同样的事件会通过[扇出](https://evlog.zhcndoc.com/integrate/adapters/overview)进入 [Axiom](https://evlog.zhcndoc.com/integrate/adapters/cloud/axiom)、[Datadog](https://evlog.zhcndoc.com/integrate/adapters/cloud/datadog)、[PostHog](https://evlog.zhcndoc.com/integrate/adapters/cloud/posthog)、[Sentry](https://evlog.zhcndoc.com/integrate/adapters/cloud/sentry)、[Better Stack](https://evlog.zhcndoc.com/integrate/adapters/cloud/better-stack)、[HyperDX](https://evlog.zhcndoc.com/integrate/adapters/hybrid/hyperdx)或 [OTLP](https://evlog.zhcndoc.com/integrate/adapters/hybrid/otlp)，也可以进入它们全部。

## “以后”到底要付出什么成本

当团队先用 `console.log` 发版，然后打算“等需要的时候”再补上正规日志时，账单就会找上门：

- **事后统一字段命名约定。** 是 `userId`、`user_id`、`uid`，还是 `actor.id`？一旦有三十个服务都用不同方式记录，你就只能在观测平台里写迁移脚本了。
- **事故后再补脱敏。** 当还没有日志时，自动脱敏很简单；但如果六个月的日志里已经包含了 PII，那就是一次 P1 级审计事件。
- **在压力下选择日志落地端。** 当你已经着火了，再去比较 Datadog、Axiom、Sentry，根本不是评估供应商的最佳时机。
- **事后补上可操作的错误上下文。** 你写过的每一个 `throw new Error('failed')`，都少了一个 `why`、少了一个 `fix`、少了一个你的值班工程师（或者 AI 代理）可以使用的链接。

evlog 完全消除了“以后”：从第 1 天起，结构化接口、宽事件生命周期和 drain 管道就全部可用。

<table>
<thead>
  <tr>
    <th>
      决策日
    </th>
    
    <th>
      采用 evlog 的成本
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      第 0 天（新项目）
    </td>
    
    <td>
      添加框架模块。完成。
    </td>
  </tr>
  
  <tr>
    <td>
      第 30 天（小型应用）
    </td>
    
    <td>
      切换日志接口——大约一天工作量。
    </td>
  </tr>
  
  <tr>
    <td>
      第 365 天（生产应用）
    </td>
    
    <td>
      通读代码库替换日志器，统一字段命名约定，补入审计与脱敏。成本与在任意两个结构化日志器之间迁移相同。
    </td>
  </tr>
</tbody>
</table>

这种不对称正是关键。**一开始就用 evlog，因为成本为零。继续用 evlog，因为离开它的成本比自己把这些能力都搭出来还高。**

## 第 0 天，实践中

<prompt :actions="["copy","cursor","claude"]" description="从第一次提交开始就接入 evlog，开启一个新项目" icon="i-lucide-rocket">

我正在开始一个新项目。从第一次提交开始就接入 evlog，这样我就再也不用事后补日志了。

- 检测我的框架，并安装匹配的 evlog 集成（Nuxt、Next.js、SvelteKit、Hono、Express 等）
- 将 evlog.env.service 设置为我的应用名称，并启用 redact: true
- 在一个路由处理器中添加一个 useLogger(event) 调用，并使用 log.set({ user, action })，这样我从一开始就拥有一个可用的宽事件
- 针对无效输入的情况抛出一个 createError({ message, status, why, fix })
- 使用 defineErrorCatalog('app', { ... }) 创建一个小型 errors 目录，从两个条目开始，随着时间推移逐步扩展
- 如果项目使用 Vercel AI SDK，接入 AI SDK 中间件，使每次 LLM 调用都记录令牌、工具和成本
- 如果项目使用 Better Auth，安装 Better Auth 插件，使身份验证事件从第 1 天起就具备类型
- 为本地开发配置文件系统 drain（.evlog/logs），并保留注释掉的云 drain，随时可以启用
- 如果我的项目属于受监管行业（金融科技 / 健康科技 / B2B SaaS），还要使用 defineAuditCatalog 接入审计增强器

文档：[https://www.evlog.dev/start/why-evlog](https://www.evlog.dev/start/why-evlog)
框架：[https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)
目录：[https://www.evlog.dev/learn/catalogs](https://www.evlog.dev/learn/catalogs)
AI SDK：[https://www.evlog.dev/use-cases/ai-sdk/overview](https://www.evlog.dev/use-cases/ai-sdk/overview)
审计：[https://www.evlog.dev/use-cases/audit/overview](https://www.evlog.dev/use-cases/audit/overview)

</prompt>

## 下一步

- [安装](https://evlog.zhcndoc.com/start/installation)：选择你的框架
- [快速开始](https://evlog.zhcndoc.com/start/quick-start)：在 2 分钟内使用 `useLogger`、`createLogger`、`createError`
- [目录](https://evlog.zhcndoc.com/learn/catalogs)：类型化错误和审计动作，从第 0 天扩展到 monorepo 规模
- [审计日志](https://evlog.zhcndoc.com/use-cases/audit/overview)：第 0 天的合规状态
- [AI SDK](https://evlog.zhcndoc.com/use-cases/ai-sdk/overview)：令牌使用情况、工具调用、流式传输指标
- [Better Auth](https://evlog.zhcndoc.com/use-cases/better-auth/overview)：一行安装即可获得身份验证事件
- [最佳实践](https://evlog.zhcndoc.com/reference/best-practices)：不应记录的内容、脱敏、采样
- [evlog vs pino、winston、consola](https://evlog.zhcndoc.com/reference/vs-other-loggers)：功能对等矩阵和迁移片段

---

- [安装](https://evlog.zhcndoc.com/start/installation)
- [审计与合规](https://evlog.zhcndoc.com/use-cases/audit/overview)
- [vs pino、winston、consola](https://evlog.zhcndoc.com/reference/vs-other-loggers)


## Sitemap

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