Skip to content

Latest commit

 

History

History
303 lines (234 loc) · 8.33 KB

File metadata and controls

303 lines (234 loc) · 8.33 KB
title React Router
description Using evlog with React Router — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in React Router applications.
links
label icon to color variant
Source Code
i-simple-icons-github
neutral
subtle
navigation
title icon
React Router
i-simple-icons-reactrouter

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.

::callout{color="info" icon="i-lucide-info"} 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. ::

::code-collapse

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

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

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

3. Initialize and register the middleware

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

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"} Using Vite? The evlog/vite plugin 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:

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:

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:

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:

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:

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:

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:

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:

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

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:

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.

::card-group :::card

icon: i-simple-icons-github title: Source Code to: https://github.com/HugoRCD/evlog/tree/main/examples/react-router

Browse the complete React Router example source on GitHub. ::: ::