Frameworks

React Router

Source Code
Using evlog with React Router — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in React Router applications.

The evlog/react-router middleware auto-creates a request-scoped logger accessible via context.get(loggerContext) or useLogger() and emits a wide event when the response completes.

React Router has three modes: Framework, Data, and Declarative. The evlog/react-router middleware requires the middleware API, which is available in Framework and Data modes only. Declarative mode does not support middleware — use evlog/browser for client-side logging instead.
Prompt
Set up evlog in my React Router app.

- Install evlog: pnpm add evlog
- Call initLogger({ env: { service: 'my-api' } }) at startup
- Alternatively, use evlog/vite plugin in vite.config.ts for auto-init (replaces initLogger)
- Enable middleware in react-router.config.ts: future: { v8_middleware: true }
- Import evlog middleware and loggerContext from 'evlog/react-router'
- Add evlog() to root route's middleware array
- Access logger via context.get(loggerContext) in loaders/actions
- Or use useLogger() from services without passing context
- Optionally pass drain, enrich, include, and keep options to evlog()

Docs: https://www.evlog.dev/frameworks/react-router
Adapters: https://www.evlog.dev/adapters/overview

Quick Start

1. Install

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

2. Enable middleware

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

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

3. Initialize and register the middleware

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. Use the logger in loaders

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 }
}
Using Vite? The evlog/viteplugin replaces the initLogger() call with compile-time auto-initialization, strips log.debug() from production builds, and injects source locations.

The loggerContext provides typed access to the evlog logger in any loader or action via context.get(loggerContext).

Wide Events

Build up context progressively through your loader. One request = one wide event:

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

All fields are merged into a single wide event emitted when the request completes:

Terminal output
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()

Access the logger from any server-side function without passing context:

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

Then call the service from your loader — useLogger() returns the same logger instance:

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

Error Handling

Use createError for structured errors with why, fix, and link fields:

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

The error is captured and logged with both the custom context and structured error fields:

Terminal output
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-...

Configuration

See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).

Drain & Enrichers

Configure drain adapters and enrichers directly in the middleware options:

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

Pipeline (Batching & Retry)

For production, wrap your adapter with createDrainPipeline to batch events and retry on failure:

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 }),
]
Call drain.flush() on server shutdown to ensure all buffered events are sent. See the Pipeline docs for all options.

Tail Sampling

Use keep to force-retain specific events regardless of head sampling:

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

Route Filtering

Control which routes are logged with include and exclude patterns:

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

Run Locally

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

Open http://localhost:5173 to explore the interactive test UI.

Source Code

Browse the complete React Router example source on GitHub.