框架

TanStack Start

源代码
自动广泛事件、结构化错误记录以及 TanStack Start API 路由和服务器函数中的日志记录。

TanStack Start 使用 Nitro v3 作为其服务器层,因此 evlog 通过 evlog/nitro/v3 模块进行集成。相同的基于插件的钩子系统也适用。

TanStack Router 与 TanStack Start:TanStack Router 是一个前端路由,并不需要服务器端日志记录。本页介绍 TanStack Start,全栈框架。如果你在 SPA 模式下使用 TanStack Router,请参考 客户端日志记录
提示
在 TanStack Start 应用中设置 evlog。

- 安装 evlog:pnpm add evlog
- 创建 nitro.config.ts 并引入 evlog/nitro/v3 模块,启用 experimental.asyncContext
- 配置 env.service 作为你的应用名称
- 在根路由添加 evlogErrorHandler 中间件以获取结构化的错误响应
- 在路由处理器中通过 useRequest().context.log 访问记录器
- 使用 log.set() 累加上下文,使用 throw createError() 抛出结构化错误

文档:https://www.evlog.dev/frameworks/tanstack-start
适配器:https://www.evlog.dev/adapters

快速开始

从一个使用 npm create @tanstack/start@latest 创建的 TanStack Start 项目开始:

1. 安装

终端
bun add evlog

2. 添加 nitro.config.ts

在项目根目录创建一个 nitro.config.ts 以注册 evlog 模块。你的 vite.config.ts 已经通过 CLI 添加了 nitro() 插件,因此无需修改。

nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: {
    asyncContext: true,
  },
  modules: [
    evlog({
      env: { service: 'my-app' },
    }),
  ],
})

启用 asyncContext 允许你通过 useRequest() 在调用栈任意位置访问请求作用域内的记录器。

3. 错误处理中间件

TanStack Start 拥有自己的错误处理层,会在 Nitro 之前运行。为确保 throw createError() 返回包含 whyfixlink 的正确 JSON 响应,请将 evlogErrorHandler 中间件添加到根路由:

src/routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'

export const Route = createRootRoute({
  server: {
    middleware: [createMiddleware().server(evlogErrorHandler)],
  },
  // ... head, shellComponent, etc.
})

至此完成。evlog 会自动捕获每个请求作为一个广泛事件,包含方法、路径、状态和持续时间。

使用 Vite? TanStack Start 基于 Vite。内置插件 evlog/vite 可在生产构建中剥离 log.debug() 并注入源码位置,请将其添加到 vite.config.ts 以及 TanStack Start 插件中。

广泛事件

启用 experimental.asyncContext: true 后,可通过 useRequest()nitro/context 访问请求作用域记录器并逐步构建上下文:

src/routes/api/hello.ts
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'

export const Route = createFileRoute('/api/hello')({
  server: {
    handlers: {
      GET: async () => {
        const req = useRequest()
        const log = req.context.log as RequestLogger

        log.set({ user: { id: 'user_123', plan: 'pro' } })
        log.set({ action: 'fetch_profile' })
        log.set({ cache: { hit: true, ttl: 3600 } })

        return Response.json({ ok: true })
      },
    },
  },
})

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

终端输出
14:58:15 INFO [my-app] GET /api/hello 200 in 52ms
  ├─ cache: hit=true ttl=3600
  ├─ action: fetch_profile
  ├─ user: id=user_123 plan=pro
  └─ requestId: 4a8ff3a8-...
useRequest() 是 Nitro v3 的实验性特性,由 AsyncLocalStorage 提供支持。它在 Node.js 和 Bun 运行时中有效。

错误处理

使用 createError 来抛出包含 whyfixlink 字段的结构化错误:

src/routes/api/checkout.ts
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import { createError } from 'evlog'
import type { RequestLogger } from 'evlog'

export const Route = createFileRoute('/api/checkout')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const req = useRequest()
        const log = req.context.log as RequestLogger
        const body = await request.json()

        log.set({ user: { id: body.userId, plan: body.plan } })
        log.set({ cart: { items: body.items, total: body.total } })

        const result = await chargeCard(body)

        if (!result.success) {
          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',
          })
        }

        return Response.json({ success: true, orderId: result.orderId })
      },
    },
  },
})

错误会被捕获并连同自定义上下文和结构化错误字段一起记录:

终端输出
14:58:20 ERROR [my-app] POST /api/checkout 402 in 104ms
  ├─ error: name=EvlogError message=Payment failed status=402
  ├─ cart: items=3 total=9999
  ├─ user: id=user_123 plan=pro
  └─ requestId: 880a50ac-...

在客户端解析错误

使用 parseError 从任意错误响应中提取结构化字段:

src/routes/checkout.tsx
import { parseError } from 'evlog'

try {
  const res = await fetch('/api/checkout', {
    method: 'POST',
    body: JSON.stringify({ userId: 'user_123' }),
  })
  if (!res.ok) throw { data: await res.json(), status: res.status }
} catch (error) {
  const { message, status, why, fix, link } = parseError(error)
}

配置

请参考 配置参考文档 查看所有可用选项(initLogger、中间件选项、采样、静默模式等)。

路由过滤

通过模块选项中的 includeexclude 控制哪些路由被记录:

nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  experimental: { asyncContext: true },
  modules: [
    evlog({
      env: { service: 'my-app' },
      include: ['/api/**'],
      exclude: ['/_internal/**', '/health'],
      routes: {
        '/api/auth/**': { service: 'auth-service' },
        '/api/payment/**': { service: 'payment-service' },
      },
    }),
  ],
})

排放与增强器

由于 TanStack Start 使用 Nitro v3,可通过 Nitro 插件配置排放和增强器。创建 server/plugins/ 目录并注册钩子:

server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createAxiomDrain } from 'evlog/axiom'

export default definePlugin((nitroApp) => {
  const axiom = createAxiomDrain()

  nitroApp.hooks.hook('evlog:drain', axiom)
})
server/plugins/evlog-enrich.ts
import { definePlugin } from 'nitro'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'

export default definePlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]

  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})
请参考 适配器增强器 文档获取所有可用的排放适配器与增强器。

管道(批量与重试)

在生产环境中,使用 createDrainPipeline 包装你的排放器以实现事件批量发送和失败重试:

server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

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

  nitroApp.hooks.hook('evlog:drain', drain)
})
在服务器关闭时调用 drain.flush() 以确保所有缓冲事件被发送。更多选项请参考 管道文档

采样

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

server/plugins/evlog-keep.ts
import { definePlugin } from 'nitro'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
    if (ctx.status && ctx.status >= 500) ctx.shouldKeep = true
  })
})

本地运行

终端
git clone https://github.com/hugorcd/evlog.git
cd evlog/examples/tanstack-start
bun install
bun run dev

打开 http://localhost:3000 并访问 evlog 演示页面以测试 API 端点。

源代码

在 GitHub 上浏览完整的 TanStack Start 示例源代码。

::

后续步骤

深入集成 TanStack Start

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