---
title: "Node.js 中的结构化日志记录"
description: "使用 evlog 为每个 Node.js 请求生成一个 JSON 宽事件：两分钟接入 node:http、Express、Fastify、Hono、NestJS 或 Nitro，然后处理错误和 drain。"
canonical_url: "https://evlog.zhcndoc.com/use-cases/structured-logging-nodejs"
---
# Node.js 中的结构化日志记录

> 使用 evlog 为每个 Node.js 请求生成一个 JSON 宽事件：两分钟接入 node:http、Express、Fastify、Hono、NestJS 或 Nitro，然后处理错误和 drain。

让每个请求都产生**一个结构化 JSON 事件**，也就是一个*宽事件*，而不是一串日志行。本指南会在大约两分钟内带你完成 Node.js 服务器的接入。

## 30 秒版本

<code-group>

```typescript [Before: 5 log lines]
console.log('checkout started', { requestId })
console.log('user loaded', { userId: 'u_42' })
console.log('cart loaded', { items: 3, total: 2999 })
console.log('charging card', { provider: 'stripe' })
console.log('checkout done', { status: 200, ms: 182 })
```

```typescript [After: 1 wide event]
log.set({ user: { id: 'u_42' } })
log.set({ cart: { items: 3, total: 2999 } })
log.set({ payment: { provider: 'stripe' } })
```

</code-group>

处理器调用 `log.set()`。evlog 会添加方法、路径、状态、耗时和请求 ID，并在响应发送时写入一个事件：

```json [Output]
{
  "level": "info",
  "method": "POST",
  "path": "/checkout",
  "status": 200,
  "durationMs": 182,
  "requestId": "4f1c…",
  "user": { "id": "u_42" },
  "cart": { "items": 3, "total": 2999 },
  "payment": { "provider": "stripe" }
}
```

请求失败时，你只需读取一条记录，而不是搜索五条日志；每个字段都可以查询：`payment.provider = 'stripe' AND status >= 500`。

## 为什么使用宽事件

<wide-event-collapse>



</wide-event-collapse>

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      多行日志
    </th>
    
    <th>
      一个宽事件
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      每个请求的记录数
    </td>
    
    <td>
      每次日志调用一条
    </td>
    
    <td>
      最后发出一条
    </td>
  </tr>
  
  <tr>
    <td>
      上下文
    </td>
    
    <td>
      每行重复，或在某些行中缺失
    </td>
    
    <td>
      收集一次，与结果放在一起
    </td>
  </tr>
  
  <tr>
    <td>
      调试失败
    </td>
    
    <td>
      找到请求 ID 对应的每一行，再重建过程
    </td>
    
    <td>
      读取一个事件：输入、结果、错误和耗时
    </td>
  </tr>
  
  <tr>
    <td>
      数量
    </td>
    
    <td>
      随调用次数增长
    </td>
    
    <td>
      每个操作固定一条，再通过<a href="https://evlog.zhcndoc.com/learn/sampling">
        采样
      </a>
      
      减少
    </td>
  </tr>
</tbody>
</table>

对于请求之外的内容（例如启动消息），仍然可以使用全局 `log` API 写入单独记录（[简单日志](https://evlog.zhcndoc.com/learn/simple-logging)）。[宽事件](https://evlog.zhcndoc.com/learn/wide-events)介绍了各自的适用场景。

## 两分钟完成设置

下面的每个代码块默认打开**独立模式**：一个基于 `node:http` 的普通 Node.js 脚本，不依赖框架，因此可以在 Node 能运行的任何地方运行。如果你使用框架，可以在任意代码块的菜单中选择它。这个选择会应用于本页面的所有代码块，并在文档中记住。

<steps level="3">

### 安装 evlog

<code-group sync="package-manager">

```bash [pnpm]
pnpm add evlog
```

```bash [npm]
npm install evlog
```

```bash [yarn]
yarn add evlog
```

```bash [bun]
bun add evlog
```

</code-group>

### 创建 `evlog.config.ts`

`evlog init` 会检测你的框架、写入此文件、接入集成，并运行 `evlog doctor` 检查结果（[evlog init](https://evlog.zhcndoc.com/cli/init)）。你也可以手动创建此文件。

<code-group sync="package-manager">

```bash [pnpm]
pnpm dlx evlog init
```

```bash [npm]
npx evlog init
```

```bash [yarn]
yarn dlx evlog init
```

```bash [bun]
bunx evlog init
```

</code-group>

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'checkout',
})
```

所有选项都位于此文件中：采样、drain、enricher 和脱敏。完整列表见[配置参考](https://evlog.zhcndoc.com/reference/configuration)。

### 接入服务器

<framework-tabs>

```typescript [Standalone]src/server.ts
import { createServer } from 'node:http'
import { initLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

createServer((req, res) => {
  // handlers create a request logger, see the next section
  res.end('ok')
}).listen(3000)
```

```typescript [Nuxt]nuxt.config.ts
// The module finds evlog.config.ts and loads it
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
})
```

```typescript [Nitro]nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

// The module finds evlog.config.ts and loads it
export default defineConfig({
  modules: [evlog()],
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import config from '../evlog.config'

// Wrap route handlers with withEvlog; the other exports are request-aware
export const { withEvlog, useLogger, log, createError } = createEvlog(config)
```

```typescript [SvelteKit]src/hooks.server.ts
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export const { handle, handleError } = createEvlogHooks(toMiddlewareOptions(config))
```

```typescript [TanStack Start]nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

// The module finds evlog.config.ts; asyncContext lets handlers call useRequest()
export default defineConfig({
  experimental: { asyncContext: true },
  modules: [evlog()],
})
```

```typescript [React Router]app/root.tsx
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/react-router'
import config from '../evlog.config'
import type { Route } from './+types/root'

initLogger(toLoggerConfig(config))

// Needs future.v8_middleware: true in react-router.config.ts
export const middleware: Route.MiddlewareFunction[] = [
  evlog(toMiddlewareOptions(config)),
]
```

```typescript [Hono]src/server.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = new Hono<EvlogVariables>()
app.use(evlog(toMiddlewareOptions(config)))

serve({ fetch: app.fetch, port: 3000 })
```

```typescript [Express]src/server.ts
import express from 'express'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/express'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = express()
app.use(evlog(toMiddlewareOptions(config)))

app.listen(3000)
```

```typescript [Fastify]src/server.ts
import Fastify from 'fastify'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/fastify'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = Fastify({ logger: false })
await app.register(evlog, toMiddlewareOptions(config))

await app.listen({ port: 3000 })
```

```typescript [Elysia]src/server.ts
import { Elysia } from 'elysia'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/elysia'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export const app = new Elysia()
  .use(evlog(toMiddlewareOptions(config)))
  .listen(3000)
```

```typescript [NestJS]src/main.ts
import 'reflect-metadata'
import { Module } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { EvlogModule } from 'evlog/nestjs'
import { CheckoutController } from './checkout.controller'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

@Module({
  // forRoot() registers a global middleware for every route
  controllers: [CheckoutController],
  imports: [EvlogModule.forRoot(toMiddlewareOptions(config))],
})
class AppModule {}

const app = await NestFactory.create(AppModule)
await app.listen(3000)
```

```typescript [Cloudflare Workers]src/worker.ts
import { toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

// withEvlog emits the event and keeps drains alive with waitUntil
export default withEvlog(async (request, _env, _ctx, log) => {
  return Response.json({ ok: true })
}, toMiddlewareOptions(config))
```

```typescript [Astro]src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { createRequestLogger, initLogger, parseError, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

// Type locals.log as RequestLogger in src/env.d.ts
export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
  const log = createRequestLogger({ method: request.method, path: new URL(request.url).pathname })
  locals.log = log
  try {
    const response = await next()
    log.emit({ status: response.status })
    return response
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    log.emit({ status: parseError(error).status }) // 500 unless the error carries one
    throw error
  }
})
```

```typescript [AWS Lambda]src/handler.ts
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
import { initLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

// Once per cold start, outside the handler
initLogger(toLoggerConfig(config))

export async function handler(event: APIGatewayProxyEventV2) {
  // one request logger per invocation, see the next section
  return { statusCode: 200, body: 'ok' }
}
```

</framework-tabs>
</steps>

<callout color="neutral" icon="i-lucide-layers">

**使用 oRPC？**它运行在上面的某个服务器中，其[集成](https://evlog.zhcndoc.com/integrate/frameworks/orpc)会为每个事件标记过程的 `operation`。所有指南都列在[框架](https://evlog.zhcndoc.com/integrate/frameworks/overview)中。

</callout>

## 记录你的第一个宽事件

随着处理器获取信息，使用 `log.set()` 添加上下文。嵌套对象会合并，因此可以分两步补充 `payment`。使用框架集成时，方法、路径、状态、耗时和请求 ID 会自动记录，响应结束时发出事件。

<framework-tabs>

```typescript [Standalone]src/server.ts
import { randomUUID } from 'node:crypto'
import { createServer } from 'node:http'
import { createRequestLogger, initLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

createServer((req, res) => {
  const log = createRequestLogger({ method: req.method, path: new URL(req.url ?? '/', 'http://localhost').pathname, requestId: randomUUID() })

  // Without a framework, you emit the event yourself: once, after the response is sent
  let emitted = false
  const emit = () => {
    if (emitted) return
    emitted = true
    log.emit({ status: res.statusCode })
  }
  res.once('finish', emit)
  res.once('close', emit)

  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  res.statusCode = 200
  res.end(JSON.stringify({ orderId: 'order-123' }))
}).listen(3000)
```

```typescript [Nuxt]server/api/checkout.post.ts
export default defineEventHandler((event) => {
  const log = useLogger(event) // auto-imported
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return { orderId: 'order-123' }
})
```

```typescript [Nitro]routes/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler((event) => {
  const log = useLogger(event)
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return { orderId: 'order-123' }
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { useLogger, withEvlog } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  const log = useLogger()
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return Response.json({ orderId: 'order-123' })
})
```

```typescript [SvelteKit]src/routes/api/checkout/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'

// Type locals.log as RequestLogger in src/app.d.ts
export const POST: RequestHandler = async ({ locals }) => {
  locals.log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  locals.log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return json({ orderId: 'order-123' })
}
```

```typescript [TanStack Start]src/routes/api/checkout.ts
import { createFileRoute } from '@tanstack/react-router'
import type { RequestLogger } from 'evlog'
import { useRequest } from 'nitro/context'

export const Route = createFileRoute('/api/checkout')({
  server: {
    handlers: {
      POST: async () => {
        const log = useRequest().context?.log as RequestLogger
        log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
        log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
        return Response.json({ orderId: 'order-123' })
      },
    },
  },
})
```

```typescript [React Router]app/routes/checkout.tsx
import { loggerContext } from 'evlog/react-router'
import type { Route } from './+types/checkout'

export async function action({ context }: Route.ActionArgs) {
  const log = context.get(loggerContext)
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return { orderId: 'order-123' }
}
```

```typescript [Hono]src/server.ts
app.post('/checkout', (c) => {
  const log = c.get('log')
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return c.json({ orderId: 'order-123' })
})
```

```typescript [Express]src/server.ts
import { useLogger } from 'evlog/express'

app.post('/checkout', (req, res) => {
  const log = useLogger() // the same logger as req.log
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  res.json({ orderId: 'order-123' })
})
```

```typescript [Fastify]src/server.ts
import { useLogger } from 'evlog/fastify'

app.post('/checkout', async () => {
  const log = useLogger() // the same logger as request.log
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return { orderId: 'order-123' }
})
```

```typescript [Elysia]src/server.ts
app.post('/checkout', ({ log }) => {
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return { orderId: 'order-123' }
})
```

```typescript [NestJS]src/checkout.controller.ts
import { Controller, Post } from '@nestjs/common'
import { useLogger } from 'evlog/nestjs'

@Controller('checkout')
export class CheckoutController {
  @Post()
  pay() {
    const log = useLogger()
    log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
    log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
    return { orderId: 'order-123' }
  }
}
```

```typescript [Cloudflare Workers]src/worker.ts
import { toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default withEvlog(async (request, _env, _ctx, log) => {
  log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return Response.json({ orderId: 'order-123' })
}, toMiddlewareOptions(config))
```

```typescript [Astro]src/pages/api/checkout.ts
import type { APIRoute } from 'astro'

export const POST: APIRoute = async ({ locals }) => {
  locals.log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
  locals.log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
  return Response.json({ orderId: 'order-123' })
}
```

```typescript [AWS Lambda]src/handler.ts
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
import { createRequestLogger, initLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export async function handler(event: APIGatewayProxyEventV2) {
  const { method, path } = event.requestContext.http
  const log = createRequestLogger({ method, path, requestId: event.requestContext.requestId })
  let statusCode = 500
  try {
    log.set({ orderId: 'order-123', payment: { amount: 2999, currency: 'EUR' } })
    log.set({ payment: { chargeId: 'ch_123' }, outcome: 'paid' })
    statusCode = 200
    return { statusCode, body: JSON.stringify({ orderId: 'order-123' }) }
  } finally {
    // Lambda has no response lifecycle to hook: emit before the handler returns
    log.emit({ status: statusCode })
  }
}
```

</framework-tabs>

在另一个终端发送请求：

```bash [Terminal]
curl -X POST http://localhost:3000/checkout
```

开发环境中，事件会以易读的树形结构打印。生产环境中，它是一行 JSON，并包含以下字段以及环境元数据：

```json [Output]
{
  "level": "info",
  "service": "checkout",
  "method": "POST",
  "path": "/checkout",
  "status": 200,
  "orderId": "order-123",
  "payment": { "amount": 2999, "currency": "EUR", "chargeId": "ch_123" },
  "outcome": "paid"
}
```

<callout color="neutral" icon="i-lucide-lightbulb">

记录调试此操作时真正需要的信息：ID、结果、金额、数量和耗时。不要记录不需要的凭据和个人数据。

</callout>

<collapsible close-text="隐藏：" name="logging from service code" open-text="深入了解：">

处理器下方的代码不需要显式传入 logger。在 Next.js、SvelteKit、React Router、Hono、Express、Fastify、Elysia 和 NestJS 中，`useLogger()` 通过 `AsyncLocalStorage` 返回同一个请求 logger。Nuxt 和 Nitro 会将 logger 绑定到 event，因此服务代码接收 `event`；TanStack Start 则从 `useRequest()` 读取它。在 Cloudflare Workers、Astro 和 AWS Lambda 中，请像独立模式标签页那样传入 logger。

<framework-tabs>

```typescript [Standalone]src/payments.ts
import type { RequestLogger } from 'evlog'

// No framework: pass the request logger to the code that needs it
export async function charge(log: RequestLogger, amount: number) {
  log.set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Nuxt]server/utils/payments.ts
import type { H3Event } from 'h3'

export async function charge(event: H3Event, amount: number) {
  useLogger(event).set({ payment: { provider: 'stripe', amount } }) // auto-imported
}
```

```typescript [Nitro]utils/payments.ts
import type { H3Event } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export async function charge(event: H3Event, amount: number) {
  useLogger(event).set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Next.js]lib/payments.ts
import { useLogger } from '@/lib/evlog'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [SvelteKit]src/lib/server/payments.ts
import { useLogger } from 'evlog/sveltekit'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [TanStack Start]src/server/payments.ts
import type { RequestLogger } from 'evlog'
import { useRequest } from 'nitro/context'

export async function charge(amount: number) {
  const log = useRequest().context?.log as RequestLogger
  log.set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [React Router]app/services/payments.server.ts
import { useLogger } from 'evlog/react-router'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Hono]src/payments.ts
import { useLogger } from 'evlog/hono'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Express]src/payments.ts
import { useLogger } from 'evlog/express'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Fastify]src/payments.ts
import { useLogger } from 'evlog/fastify'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [Elysia]src/payments.ts
import { useLogger } from 'evlog/elysia'

export async function charge(amount: number) {
  useLogger().set({ payment: { provider: 'stripe', amount } })
}
```

```typescript [NestJS]src/payments.service.ts
import { Injectable } from '@nestjs/common'
import { useLogger } from 'evlog/nestjs'

@Injectable()
export class PaymentsService {
  async charge(amount: number) {
    useLogger().set({ payment: { provider: 'stripe', amount } })
  }
}
```

</framework-tabs>

响应结束后才完成的工作无法再修改请求事件，因为 logger 发出事件时就会封存。后台工作请使用 `log.fork()`（[发出事件之后](https://evlog.zhcndoc.com/learn/wide-events#after-emit-sealing-and-background-work)）。[日志生命周期](https://evlog.zhcndoc.com/learn/lifecycle)展示了从创建 logger 到 drain 的每个阶段。

</collapsible>

## 能自我说明的错误

<structured-error-context>



</structured-error-context>

`createError()` 为错误提供 `status`、`why` 和 `fix`。evlog 会将它们与失败前设置的上下文一起记录到请求事件中，`parseError()` 再读取它们来构建 HTTP 响应。

<framework-tabs>

```typescript [Standalone]src/server.ts
import { createError, parseError } from 'evlog'

// inside the request handler from the previous section, after the listeners
try {
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
} catch (error) {
  log.error(error instanceof Error ? error : new Error(String(error)))
  const parsed = parseError(error)
  res.statusCode = parsed.status
  res.end(JSON.stringify({ message: parsed.message, why: parsed.why, fix: parsed.fix }))
}
// the 'finish' listener emits the event with status 402
```

```typescript [Nuxt]server/api/checkout.post.ts
// Import createError from evlog: h3's version drops why and fix
import { createError } from 'evlog'

export default defineEventHandler((event) => {
  useLogger(event).set({ orderId: 'order-123' })

  // Nuxt's error handler records it and returns why and fix as JSON
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})
```

```typescript [Nitro]routes/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { createError, useLogger } from 'evlog/nitro/v3'

export default defineHandler((event) => {
  useLogger(event).set({ orderId: 'order-123' })

  // Nitro's error handler records it and builds the response
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { createError, useLogger, withEvlog } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  useLogger().set({ orderId: 'order-123' })

  // withEvlog records it and returns why and fix as JSON
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})
```

```typescript [SvelteKit]src/routes/api/checkout/+server.ts
import { createError } from 'evlog'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ locals }) => {
  locals.log.set({ orderId: 'order-123' })

  // The handleError hook records it on the event
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
}
```

```typescript [TanStack Start]src/routes/api/checkout.ts
import { createFileRoute } from '@tanstack/react-router'
import { createError } from 'evlog'

// For a JSON response with why and fix, add the evlogErrorHandler
// middleware from 'evlog/nitro/v3' to the root route
export const Route = createFileRoute('/api/checkout')({
  server: {
    handlers: {
      POST: async () => {
        throw createError({
          message: 'Payment declined',
          status: 402,
          why: 'The card issuer declined the charge',
          fix: 'Try another payment method',
        })
      },
    },
  },
})
```

```typescript [React Router]app/routes/checkout.tsx
import { createError } from 'evlog'
import { loggerContext } from 'evlog/react-router'
import type { Route } from './+types/checkout'

export async function action({ context }: Route.ActionArgs) {
  context.get(loggerContext).set({ orderId: 'order-123' })

  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
}
```

```typescript [Hono]src/server.ts
import type { ContentfulStatusCode } from 'hono/utils/http-status'
import { createError, parseError } from 'evlog'

app.post('/checkout', () => {
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})

app.onError((error, c) => {
  c.get('log').error(error)
  const parsed = parseError(error)
  return c.json({ message: parsed.message, why: parsed.why, fix: parsed.fix }, parsed.status as ContentfulStatusCode)
})
```

```typescript [Express]src/server.ts
import type { ErrorRequestHandler } from 'express'
import { createError, parseError } from 'evlog'

app.post('/checkout', () => {
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})

const onError: ErrorRequestHandler = (err, req, res, _next) => {
  req.log?.error(err)
  const parsed = parseError(err)
  res.status(parsed.status).json({ message: parsed.message, why: parsed.why, fix: parsed.fix })
}
app.use(onError)
```

```typescript [Fastify]src/server.ts
import { createError, parseError } from 'evlog'

app.post('/checkout', async () => {
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
})

// The plugin records the error on the event through Fastify's onError hook
app.setErrorHandler((error, _request, reply) => {
  const parsed = parseError(error)
  reply.status(parsed.status).send({ message: parsed.message, why: parsed.why, fix: parsed.fix })
})
```

```typescript [Elysia]src/server.ts
import { createError, parseError } from 'evlog'

app
  .post('/checkout', () => {
    throw createError({
      message: 'Payment declined',
      status: 402,
      why: 'The card issuer declined the charge',
      fix: 'Try another payment method',
    })
  })
  .onError(({ error, set }) => {
    const parsed = parseError(error)
    set.status = parsed.status
    return { message: parsed.message, why: parsed.why, fix: parsed.fix }
  })
```

```typescript [NestJS]src/evlog-exception.filter.ts
import { Catch } from '@nestjs/common'
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common'
import { parseError } from 'evlog'
import { useLogger } from 'evlog/nestjs'

// Register with app.useGlobalFilters(new EvlogExceptionFilter()) in src/main.ts
@Catch()
export class EvlogExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    useLogger().error(exception instanceof Error ? exception : new Error(String(exception)))
    const parsed = parseError(exception)
    host.switchToHttp().getResponse().status(parsed.status).json({ message: parsed.message, why: parsed.why, fix: parsed.fix })
  }
}
```

```typescript [Cloudflare Workers]src/worker.ts
import { createError, parseError, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default withEvlog(async (request, _env, _ctx, log) => {
  try {
    throw createError({
      message: 'Payment declined',
      status: 402,
      why: 'The card issuer declined the charge',
      fix: 'Try another payment method',
    })
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    const parsed = parseError(error)
    return Response.json({ message: parsed.message, why: parsed.why, fix: parsed.fix }, { status: parsed.status })
  }
}, toMiddlewareOptions(config))
```

```typescript [Astro]src/pages/api/checkout.ts
import type { APIRoute } from 'astro'
import { createError, parseError } from 'evlog'

export const POST: APIRoute = async ({ locals }) => {
  const error = createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
  locals.log.error(error)
  const parsed = parseError(error)
  return Response.json({ message: parsed.message, why: parsed.why, fix: parsed.fix }, { status: parsed.status })
}
```

```typescript [AWS Lambda]src/handler.ts
import { createError, parseError } from 'evlog'

// inside the handler from the previous section
try {
  throw createError({
    message: 'Payment declined',
    status: 402,
    why: 'The card issuer declined the charge',
    fix: 'Try another payment method',
  })
} catch (error) {
  log.error(error instanceof Error ? error : new Error(String(error)))
  const parsed = parseError(error)
  statusCode = parsed.status
  return { statusCode, body: JSON.stringify({ message: parsed.message, why: parsed.why, fix: parsed.fix }) }
} finally {
  log.emit({ status: statusCode })
}
```

</framework-tabs>

被拒付的请求会发出一个 `status: 402` 的 `error` 事件，其中包含失败前设置的上下文，以及错误的 message、`why` 和 `fix`。[结构化错误](https://evlog.zhcndoc.com/learn/structured-errors)介绍了 `link`、错误代码，以及用于反复出现的失败的[目录](https://evlog.zhcndoc.com/learn/catalogs)。

## 将日志发送到某处

<drain-fan-out>



</drain-fan-out>

本地环境中，事件会打印到终端。生产环境中，**drain** 会将事件发送到后端，而且无需修改任何处理器。将它添加到配置文件后，本页面的每个集成都能使用它：

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'checkout',
  drain: createAxiomDrain(), // reads AXIOM_API_KEY and AXIOM_DATASET
})
```

Axiom 只是其中一个选项。Datadog、Sentry、PostHog、Better Stack、Loki、ClickHouse、OTLP 和文件系统都在[适配器概览](https://evlog.zhcndoc.com/integrate/adapters/overview)中。文件中的 `enrich` 函数会为每个事件添加字段，例如用户代理或部署版本（[Enricher](https://evlog.zhcndoc.com/use-cases/enrichers)）。

<collapsible close-text="隐藏：" name="batching and flushing" open-text="深入了解：">

管道会批处理事件，并重试失败的发送。进程退出前必须 flush 已缓冲的事件，这对脚本、worker 和无服务器函数尤其重要：

```typescript [evlog.config.ts]
import type { DrainContext } from 'evlog'
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
export const drain = pipeline(createAxiomDrain())

export default defineEvlog({ service: 'checkout', drain })
```

```typescript [src/shutdown.ts]
import type { Server } from 'node:http'
import { drain } from '../evlog.config'

// Stop accepting connections, let in-flight requests finish, then flush.
// Node exits on its own once nothing is left to do.
export function flushOnShutdown(server: Server) {
  process.once('SIGTERM', () => {
    server.close(async () => {
      await drain.flush()
    })
  })
}
```

批处理大小、间隔和重试策略请参见 [Drain Pipeline](https://evlog.zhcndoc.com/extend/drain-pipeline)。

</collapsible>

## 生产环境检查清单

- **按结果采样。**保留一部分成功的 `info` 事件，并使用 `keep` 规则始终保留错误和慢请求（[采样](https://evlog.zhcndoc.com/learn/sampling)）。```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'checkout',
  sampling: { rates: { info: 10 }, keep: [{ status: 400 }, { duration: 1000 }] },
})
```
- **发送前脱敏。**生产环境默认启用脱敏。为领域特有的字段添加路径（[脱敏](https://evlog.zhcndoc.com/learn/redaction)）。
- **退出时 flush。**脚本、worker 和无服务器函数必须在进程停止前等待待发送内容完成交付（[独立 TypeScript](https://evlog.zhcndoc.com/integrate/frameworks/standalone)）。
- **检查接入。**没有日志出现时运行 `evlog doctor`，并使用 [`evlog map`](https://evlog.zhcndoc.com/cli/map) 找出仍未发出事件的处理器。<code-group sync="package-manager">

```bash [pnpm]
pnpm evlog doctor
```

```bash [npm]
npx evlog doctor
```

```bash [yarn]
yarn evlog doctor
```

```bash [bun]
bunx evlog doctor
```

</code-group>

## 常见问题

<accordion type="single">
<accordion-item label="How is this different from pino or winston?">

Pino 和 winston 每次调用都会写入一条记录。evlog 的请求 logger 会在请求过程中收集字段，并在最后写入一个包含结果、错误和耗时的事件。它还内置结构化错误、采样、脱敏和 drain。[Pino](https://evlog.zhcndoc.com/compare/pino)、[Winston](https://evlog.zhcndoc.com/compare/winston) 以及[所有日志器](https://evlog.zhcndoc.com/reference/vs-other-loggers)的对比页面会详细介绍差异。

</accordion-item>

<accordion-item label="它会拖慢我的应用吗？">

一个请求只构建一个内存对象并序列化一次，而不是写入多条记录。事件发出后 drain 才运行，[管道](https://evlog.zhcndoc.com/extend/drain-pipeline)会批处理网络发送。成本仍取决于你记录的内容和发送位置，因此请测量自己的配置：[性能](https://evlog.zhcndoc.com/reference/performance)列出了基准测试及运行方式，[采样](https://evlog.zhcndoc.com/learn/sampling)可以减少数量。

</accordion-item>

<accordion-item label="迁移期间可以保留 console.log 吗？">

可以。evlog 不会替换或修改 `console`，现有调用会继续工作。可以一次迁移一个路由到请求 logger，准备好后再用全局 `log` API 替换一次性消息（[简单日志](https://evlog.zhcndoc.com/learn/simple-logging)）。

</accordion-item>

<accordion-item label="如何避免记录密钥或个人数据？">

首先不要把它们放进上下文：记录 ID，而不是整个对象。生产环境默认启用脱敏并遮盖常见敏感值；在 `evlog.config.ts` 中添加自己的路径和模式（[脱敏](https://evlog.zhcndoc.com/learn/redaction)）。

</accordion-item>

<accordion-item label="它支持无服务器函数和边缘环境吗？">

支持。函数返回前要等待交付完成：在 Cloudflare Workers 中向 logger 传入 `waitUntil`，在短生命周期进程中对管道 drain 调用 `flush()`。参见 [AWS Lambda](https://evlog.zhcndoc.com/integrate/frameworks/aws-lambda)、[Cloudflare Workers](https://evlog.zhcndoc.com/integrate/frameworks/cloudflare-workers) 和[独立 TypeScript](https://evlog.zhcndoc.com/integrate/frameworks/standalone)。

</accordion-item>

<accordion-item label="它支持 OpenTelemetry 吗？">

支持。[OTLP 适配器](https://evlog.zhcndoc.com/integrate/adapters/hybrid/otlp)可以将事件导出到任意 OpenTelemetry Collector 或后端，你也可以将活动 span 的 `traceId` 和 `spanId` 添加到事件中。[evlog 与 OpenTelemetry](https://evlog.zhcndoc.com/compare/opentelemetry)介绍了设置方法。

</accordion-item>

<accordion-item label="如何关联多个服务之间的日志？">

每个请求事件都有 `requestId`。Hono、Express、Fastify 和 NestJS 集成会在收到 `x-request-id` 标头时复用它，因此转发该标头即可关联服务之间的事件。对于分布式追踪，`createTraceContextEnricher()` 会将收到的 `traceparent` 添加到每个事件（[Enricher](https://evlog.zhcndoc.com/use-cases/enrichers)）。

</accordion-item>
</accordion>

为了让各处理器中的字段名称保持一致，请继续阅读[类型化字段](https://evlog.zhcndoc.com/learn/typed-fields)。框架的完整选项请参见[框架指南](https://evlog.zhcndoc.com/integrate/frameworks/overview)。

---

- [独立 TypeScript](https://evlog.zhcndoc.com/integrate/frameworks/standalone)
- [快速开始](https://evlog.zhcndoc.com/start/quick-start)


## Sitemap

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