Frameworks

SvelteKit

Source Code
Automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in SvelteKit applications.

The evlog/sveltekit adapter provides handle and handleError hooks that auto-create a request-scoped logger accessible via event.locals.log and useLogger(), emitting a wide event when the response completes.

Prompt
Set up evlog in my SvelteKit app.

- Install evlog: pnpm add evlog
- Add evlog/vite plugin to vite.config.ts with service name (handles auto-init, debug stripping)
- Export handle and handleError from evlog/sveltekit in hooks.server.ts
- Access the logger via event.locals.log or useLogger() in routes and services
- Use log.set() to accumulate context, throw createError() for structured errors
- Wide events are auto-emitted when each request completes

Docs: https://www.evlog.dev/frameworks/sveltekit
Adapters: https://www.evlog.dev/adapters

Quick Start

1. Install

Terminal
bun add evlog

2. Add the Vite plugin

vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite'
import evlog from 'evlog/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    sveltekit(),
    evlog({
      service: 'my-api',
    }),
  ],
})

See the Vite Plugin docs for all options.

3. Create hooks

src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'

export const { handle, handleError } = createEvlogHooks()

4. Type your locals

src/app.d.ts
import type { RequestLogger } from 'evlog'

declare global {
  namespace App {
    interface Locals {
      log: RequestLogger
    }
  }
}

export {}

Wide Events

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

src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'

export const GET: RequestHandler = async ({ locals, params }) => {
  locals.log.set({ user: { id: params.id } })

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

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

  return json({ 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 /api/users/usr_123 200 in 12ms
  ├─ orders: count=2 totalRevenue=6298
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...

useLogger()

Use useLogger() to access the request-scoped logger from anywhere in the call stack without passing locals through your service layer:

src/lib/services/user.ts
import { useLogger } from 'evlog/sveltekit'

export async function findUser(id: string) {
  const log = useLogger()
  log.set({ user: { id } })

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

  return user
}
src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'
import { findUser } from '$lib/services/user'
import type { RequestHandler } from './$types'

export const GET: RequestHandler = async ({ params }) => {
  const user = await findUser(params.id)
  return json(user)
}

Both event.locals.log and useLogger() return the same logger instance. useLogger() uses AsyncLocalStorage to propagate the logger across async boundaries.

Background work (log.fork)

Use locals.log.fork(label, fn) for a child wide event. See Wide events — After emit.

src/routes/api/orders/+server.ts
import { useLogger } from 'evlog/sveltekit'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ locals }) => {
  locals.log.fork!('process', async () => {
    const log = useLogger()
    log.set({ step: 'done' })
  })
  return new Response(JSON.stringify({ ok: true }))
}

Error Handling

Use createError for structured errors with why, fix, and link fields. The handleError hook captures thrown errors automatically:

src/routes/api/checkout/+server.ts
import { json } from '@sveltejs/kit'
import { createError } from 'evlog'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ locals, request }) => {
  const { cartId } = await request.json()
  locals.log.set({ cart: { id: cartId } })

  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] POST /api/checkout 402 in 3ms
  ├─ error: name=EvlogError message=Payment failed status=402
  ├─ cart: id=cart_456
  └─ 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 hooks options:

src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'

const userAgent = createUserAgentEnricher()

export const { handle, handleError } = createEvlogHooks({
  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:

src/hooks.server.ts
import type { DrainContext } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
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 { handle, handleError } = createEvlogHooks({ 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:

src/hooks.server.ts
export const { handle, handleError } = createEvlogHooks({
  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:

src/hooks.server.ts
export const { handle, handleError } = createEvlogHooks({
  include: ['/api/**'],
  exclude: ['/_internal/**', '/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})

Run Locally

Terminal
git clone https://github.com/hugorcd/evlog.git
cd evlog
bun install
bun run example:sveltekit

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

Source Code

Browse the complete SvelteKit example source on GitHub.

Next Steps

Deepen your SvelteKit integration:

  • Wide Events: Design comprehensive events with context layering
  • Adapters: Send logs to Axiom, Sentry, PostHog, and more
  • Sampling: Control log volume with head and tail sampling
  • Structured Errors: Throw errors with why, fix, and link fields