Frameworks

Custom Integration

Build your own evlog framework integration using the toolkit API with 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 extractor - extractSafeHeaders 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.