---
title: "React Router"
description: "自动化的广泛事件、结构化错误、排水适配器、增强器以及 React Router 应用中的尾部采样。"
canonical_url: "https://evlog.zhcndoc.com/integrate/frameworks/react-router"
---
# React Router

> 自动化的广泛事件、结构化错误、排水适配器、增强器以及 React Router 应用中的尾部采样。

`evlog/react-router` 中间件会自动创建一个请求范围内的日志记录器，可以通过 `context.get(loggerContext)` 或 `useLogger()` 访问，并在响应完成时触发一个广泛事件。

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

React Router 有三种 [模式](https://reactrouter.com/start/modes)：**Framework**（框架）、**Data**（数据）和 **Declarative**（声明式）。`evlog/react-router` 中间件需要使用中间件 API，该 API 仅在 **Framework** 和 **Data** 模式下可用。声明式模式不支持中间件：如果需要控制台日志记录，请使用 `evlog/client`；如果需要向服务器发送批量 HTTP 排水，请使用 `evlog/http`。

</callout>

<prompt :actions="["copy","cursor","claude"]" description="在我的 React Router 应用中设置 evlog" icon="i-custom-reactrouter">

在我的 React Router 应用中设置 evlog。

- 安装 evlog：pnpm add evlog
- 创建 evlog.config.ts，其中包含 export default defineEvlog({ service: 'my-api' })
- 在启动时导入 evlog.config.ts，并调用 initLogger(toLoggerConfig(config))
- 在 react-router.config.ts 中启用中间件：future: { v8_middleware: true }
- 导入 evlog 中间件和 loggerContext：import { evlog } from 'evlog/react-router'
- 将 evlog() 添加到根路由的中间件数组中
- 在加载器/操作函数中通过 context.get(loggerContext) 访问日志记录器
- 或者在不传递上下文的情况下在服务中使用 useLogger()
- 将 drain、enrich、include 和 keep 放入 evlog.config.ts；toMiddlewareOptions(config) 会将它们传给 evlog()

文档：[https://www.evlog.dev/integrate/frameworks/react-router](https://www.evlog.dev/integrate/frameworks/react-router)
适配器：[https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## 快速开始

### 1. 安装

<code-group>

```bash [pnpm]
pnpm add evlog react-router @react-router/node @react-router/serve
```

```bash [bun]
bun add evlog react-router @react-router/node @react-router/serve
```

```bash [yarn]
yarn add evlog react-router @react-router/node @react-router/serve
```

```bash [npm]
npm install evlog react-router @react-router/node @react-router/serve
```

</code-group>

### 2. 创建 evlog.config.ts

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

export default defineEvlog({
  service: 'my-api',
})
```

所有选项都位于此文件中：采样、drain、`enrich` 和路由过滤器。`npx evlog init` 会为你写入它，以及下方的接入代码。请参阅[配置](https://evlog.zhcndoc.com/reference/configuration)。

### 3. 启用中间件

```typescript [react-router.config.ts]
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    v8_middleware: true,
  },
} satisfies Config
```

### 4. 初始化并注册中间件

```typescript [app/root.tsx]
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog } from 'evlog/react-router'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export const middleware: Route.MiddlewareFunction[] = [
  evlog(toMiddlewareOptions(config)),
]

export default function Root() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}
```

### 5. 在加载器中使用日志记录器

```typescript [app/routes/health.tsx]
import { loggerContext } from 'evlog/react-router'

export async function loader({ context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ route: 'health' })
  return { ok: true }
}
```

<callout color="info" icon="i-custom-vite">

**使用 Vite？** [`evlog/vite` 插件](https://evlog.zhcndoc.com/reference/vite-plugin) 会从生产构建中移除 `log.debug()`，并注入源代码位置。它不会读取 `evlog.config.ts`，因此保留 `initLogger(toLoggerConfig(config))` 调用：该调用会在插件自动初始化后运行，并替代它。

</callout>

`loggerContext` 允许在任意加载器或操作函数中通过 `context.get(loggerContext)` 获得类型化的 evlog 日志记录器。

## 广泛事件

在加载器中逐步构建上下文。一次请求 = 一个广泛事件：

```typescript [app/routes/users.$id.tsx]
import { loggerContext } from 'evlog/react-router'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  const userId = params.id

  log.set({ user: { id: userId } })

  const user = await db.findUser(userId)
  log.set({ user: { name: user.name, plan: user.plan } })

  const orders = await db.findOrders(userId)
  log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })

  return { user, orders }
}
```

所有字段都会合并到一个广泛事件中，并在请求完成时发出：

```bash [终端输出]
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...
```

## 使用 useLogger()

从任何服务器端函数访问日志记录器，而无需传递上下文：

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

export async function findUser(userId: string) {
  const log = useLogger()
  log.set({ db: { query: 'findUser', userId } })
  return await db.users.find(userId)
}
```

然后从你的加载器调用该服务：`useLogger()` 返回相同的日志记录器实例：

```typescript [app/routes/users.$id.tsx]
import { loggerContext } from 'evlog/react-router'
import { findUser } from '~/services/user.server'

export async function loader({ params, context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ user: { id: params.id } })

  const user = await findUser(params.id!)
  return { user }
}
```

## 后台工作 (`log.fork`)

`loggerContext` 中的日志记录器支持用于创建子广泛事件的 `fork`。请参阅广泛事件中的[发出后](https://evlog.zhcndoc.com/learn/wide-events#after-emit-sealing-and-background-work)。

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

export async function action({ context }: Route.ActionArgs) {
  const log = context.get(loggerContext)
  log.fork!('background', async () => {
    const child = useLogger()
    child.set({ step: 'complete' })
  })
  return { ok: true }
}
```

## 错误处理

使用 `createError` 创建带有 `why`、`fix` 和 `link` 字段的结构化错误：

```typescript [app/routes/checkout.tsx]
import { loggerContext } from 'evlog/react-router'
import { createError } from 'evlog'

export async function loader({ context }: Route.LoaderArgs) {
  const log = context.get(loggerContext)
  log.set({ cart: { items: 3, total: 9999 } })

  throw createError({
    message: 'Payment failed',
    status: 402,
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
    link: 'https://docs.example.com/payments/declined',
  })
}
```

该错误会被捕获并记录，包含自定义上下文和结构化错误字段：

```bash [终端输出]
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  ├─ cart: items=3 total=9999
  └─ requestId: 880a50ac-...
```

## 配置

有关所有可用选项（`initLogger`、中间件选项、采样、静默模式等），请参见 [配置参考](https://evlog.zhcndoc.com/reference/configuration)。所有选项都位于 `evlog.config.ts` 中。

## 排水与增强器

在 `evlog.config.ts` 中设置 `drain` 和 `enrich`，它们会通过 `toMiddlewareOptions(config)` 到达应用。也可以直接传入中间件选项；内联键会替换文件中的同名键，而不是与其合并：

```typescript [app/root.tsx]
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    drain: createAxiomDrain(),
    enrich: (ctx) => {
      userAgent(ctx)
      ctx.event.region = process.env.FLY_REGION
    },
  }),
]
```

### 管道（批处理与重试）

对于生产环境，使用 `createDrainPipeline` 包装你的适配器以实现事件批处理和失败重试：

```typescript [app/root.tsx]
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain }),
]
```

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

在服务器关闭时调用 `drain.flush()` 以确保发送所有已缓冲事件。有关所有选项，请参见 [管道文档](https://evlog.zhcndoc.com/extend/drain-pipeline)。

</callout>

## 尾部采样

使用 `keep` 强制保留特定事件，无论头部采样如何：

```typescript [app/root.tsx]
export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    drain: createAxiomDrain(),
    keep: (ctx) => {
      if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    },
  }),
]
```

## 路由过滤

使用 `include` 和 `exclude` 控制哪些路由被记录：

```typescript [app/root.tsx]
export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    include: ['/api/**'],
    exclude: ['/_internal/**', '/health'],
    routes: {
      '/api/auth/**': { service: 'auth-service' },
      '/api/payment/**': { service: 'payment-service' },
    },
  }),
]
```

## 本地运行

```bash [终端]
git clone https://github.com/evloghq/evlog.git
cd evlog
pnpm install
pnpm example react-router
```

打开 [http://localhost:5173](http://localhost:5173) 探索交互式测试界面。

<card-group>
<card icon="i-simple-icons-github" title="源代码" to="https://github.com/evloghq/evlog/tree/main/examples/react-router">

## 在 GitHub 上浏览完整的 React Router 示例源代码。

</card>

## 后续步骤

- [宽事件](https://evlog.zhcndoc.com/learn/wide-events)：设计具有上下文分层的完整事件
- [适配器](https://evlog.zhcndoc.com/integrate/adapters/overview)：将日志发送到 Axiom、Sentry、PostHog 等
- [采样](https://evlog.zhcndoc.com/learn/sampling)：通过头部和尾部采样控制日志量
- [结构化错误](https://evlog.zhcndoc.com/learn/structured-errors)：抛出带有 `why`、`fix` 和 `link` 字段的错误。

</card-group>

---

- [源代码](https://github.com/evloghq/evlog/tree/main/examples/react-router)


## Sitemap

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