Frameworks

Custom Integration

Build your own evlog framework integration using the toolkit API — createMiddlewareLogger, header extraction, AsyncLocalStorage, and the full drain/enrich/keep pipeline.

Don't see your framework listed? The evlog/toolkit package exposes the same building blocks that power every built-in integration (Hono, Express, Fastify, Elysia, NestJS, SvelteKit). Build a full-featured evlog middleware for any HTTP framework in ~50 lines of code.

The toolkit API is marked as beta. The surface is stable (used by all built-in integrations) but may evolve based on community feedback.

Install

pnpm add evlog

What's in the Toolkit

ExportPurpose
createMiddlewareLogger(opts)Full pipeline: logger creation, route filtering, tail sampling, emit, enrich, drain
BaseEvlogOptionsBase user-facing options type with drain, enrich, keep, include, exclude, routes
MiddlewareLoggerOptionsInternal options extending BaseEvlogOptions with method, path, requestId, headers
MiddlewareLoggerResultReturn type: { logger, finish, skipped }
extractSafeHeaders(headers)Filter sensitive headers from a Web API Headers object (Hono, Elysia, Deno, Bun)
extractSafeNodeHeaders(headers)Filter sensitive headers from Node.js IncomingHttpHeaders (Express, Fastify, NestJS)
createLoggerStorage(hint)Factory returning { storage, useLogger } backed by AsyncLocalStorage
extractErrorStatus(error)Extract HTTP status from any error shape (status or statusCode)
shouldLog(path, include, exclude)Route filtering logic (glob patterns)
getServiceForPath(path, routes)Resolve per-route service name

Types like RequestLogger, DrainContext, EnrichContext, WideEvent, and TailSamplingContext are exported from the main evlog package.

Architecture

Every evlog framework integration follows the same 5-step pattern:

Request → createMiddlewareLogger() → store logger → handle request → finish()
  1. Extract method, path, requestId, and headers from the framework request
  2. Call createMiddlewareLogger() with those fields + user options
  3. Check skipped — if true, the route is filtered out, skip to next middleware
  4. Store the logger in the framework's idiomatic context (req.log, c.set('log'), etc.)
  5. Call finish({ status }) on success or finish({ error }) on failure

createMiddlewareLogger handles everything else: route filtering, service overrides, duration tracking, tail sampling, event emission, enrichment, and draining.

Minimal Example

Here's a complete integration for a generic Node.js HTTP framework:

my-framework-evlog.ts
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { RequestLogger } from 'evlog'
import {
  createMiddlewareLogger,
  extractSafeNodeHeaders,
  createLoggerStorage,
  type BaseEvlogOptions,
} from 'evlog/toolkit'

export type MyFrameworkEvlogOptions = BaseEvlogOptions

const { storage, useLogger } = createLoggerStorage(
  'middleware context. Make sure evlog middleware is registered before your routes.',
)

export { useLogger }

export function evlog(options: MyFrameworkEvlogOptions = {}) {
  return async (req: IncomingMessage, res: ServerResponse, next: () => Promise<void>) => {
    const { logger, finish, skipped } = createMiddlewareLogger({
      method: req.method || 'GET',
      path: req.url || '/',
      requestId: (req.headers['x-request-id'] as string) || crypto.randomUUID(),
      headers: extractSafeNodeHeaders(req.headers),
      ...options,
    })

    if (skipped) {
      await next()
      return
    }

    ;(req as IncomingMessage & { log: RequestLogger }).log = logger

    try {
      await storage.run(logger, () => next())
      await finish({ status: res.statusCode })
    } catch (error) {
      await finish({ error: error as Error })
      throw error
    }
  }
}

That's it. This middleware gets every feature for free: route filtering, drain adapters, enrichers, tail sampling, error capture, and duration tracking.

Key Rules

  1. Always use createMiddlewareLogger — never call createRequestLogger directly
  2. Use the right header extractorextractSafeHeaders for Web API Headers (Hono, Elysia, Deno), extractSafeNodeHeaders for Node.js IncomingHttpHeaders (Express, Fastify)
  3. Spread user options...options passes drain, enrich, keep, include, exclude to the pipeline automatically
  4. Call finish() in both paths — success ({ status }) and error ({ error }) — it handles emit + enrich + drain
  5. Re-throw errors after finish() so framework error handlers still work
  6. Export useLogger() — consumers expect it for accessing the logger from service functions
  7. Export your options type extending BaseEvlogOptions — for IDE completion on drain, enrich, keep

Usage

Once built, your integration is used like any other:

import { initLogger } from 'evlog'
import { evlog, useLogger } from './my-framework-evlog'
import { createAxiomDrain } from 'evlog/axiom'

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

app.use(evlog({
  include: ['/api/**'],
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    ctx.event.region = process.env.FLY_REGION
  },
  keep: (ctx) => {
    if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
  },
}))

app.get('/api/users', (req, res) => {
  req.log.set({ users: { count: 42 } })
  res.json({ users: [] })
})

// Access logger from anywhere in the call stack
function findUsers() {
  const log = useLogger()
  log.set({ db: { query: 'SELECT * FROM users' } })
}

Reference Implementations

Study these built-in integrations for framework-specific patterns:

FrameworkLinesPatternSource
Hono~40Web API Headers, c.set(), try/catchhono/index.ts
Express~60Node.js headers, req.log, res.on('finish')express/index.ts
Elysia~70Plugin API, derive(), onAfterHandle/onErrorelysia/index.ts
Fastify~70Plugin, decorateRequest, onRequest/onResponse hooksfastify/index.ts
Built an integration for a framework we don't support? Open a PR — the community will thank you.