框架

React Router

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

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

React Router 有三种模式Framework(框架)、Data(数据)和 Declarative(声明式)。evlog/react-router 中间件需要使用中间件 API,该 API 仅在 FrameworkData 模式下可用。声明式模式不支持中间件:如果需要控制台日志记录,请使用 evlog/client;如果需要向服务器发送批量 HTTP 排水,请使用 evlog/http
提示
在 React Router 应用中设置 evlog。

- 安装 evlog:pnpm add evlog
- 在启动时调用 initLogger({ env: { service: 'my-api' } })
- 或者在 vite.config.ts 中使用 evlog/vite 插件进行自动初始化(替代 initLogger)
- 在 react-router.config.ts 中启用中间件:future: { v8_middleware: true }
- 导入 evlog 中间件和 loggerContext:import { evlog } from 'evlog/react-router'
- 将 evlog() 添加到根路由的中间件数组中
- 在加载器/操作函数中通过 context.get(loggerContext) 访问日志记录器
- 或者在不传递上下文的情况下在服务中使用 useLogger()
- 可选地向 evlog() 传递 drain、enrich、include 和 keep 选项

文档:https://www.evlog.dev/frameworks/react-router
适配器:https://www.evlog.dev/adapters/overview

快速开始

1. 安装

终端
bun add evlog react-router @react-router/node @react-router/serve

2. 启用中间件

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

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

3. 初始化并注册中间件

app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'

initLogger({
  env: { service: 'my-api' },
})

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

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

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

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 }
}
使用 Vite?evlog/vite插件 会用编译时自动初始化替代 initLogger(),从生产构建中剥离 log.debug(),并注入源代码位置。

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

广泛事件

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

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 }
}

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

终端输出
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()

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

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() 返回相同的日志记录器实例:

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 }
}

错误处理

使用 createError 创建带有 whyfixlink 字段的结构化错误:

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',
  })
}

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

终端输出
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、中间件选项、采样、静默模式等)。

排水与增强器

直接在中间件选项中配置排水适配器和增强器:

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 包装你的适配器以实现事件批处理和失败重试:

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 }),
]
在服务器关闭时调用 drain.flush() 以确保所有缓冲事件都已发送。请参阅管道文档了解所有选项。

尾部采样

使用 keep 强制保留特定事件,无论头部采样如何:

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

路由过滤

使用 includeexclude 控制哪些路由被记录:

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' },
    },
  }),
]

本地运行

终端
git clone https://github.com/hugorcd/evlog.git
cd evlog
bun install
bun run example:react-router

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

源代码

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

后续步骤

  • 广泛事件:使用上下文分层设计全面的事件
  • 适配器:将日志发送到 Axiom、Sentry、PostHog 等
  • 采样:通过头部和尾部采样控制日志量
  • 结构化错误:抛出带有 whyfixlink 字段的错误