Frameworks

Custom Integration

Build your own evlog framework integration using the toolkit API — defineFrameworkIntegration, createMiddlewareLogger, 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 ~30 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.

Build an evlog integration for my framework

Install

pnpm add evlog

What's in the Toolkit

ExportPurpose
defineFrameworkIntegration(spec)Manifest factory — extract request, create logger, attach, run with ALS
createMiddlewareLogger(opts)Lower-level lifecycle (custom mode)
BaseEvlogOptionsBase user-facing options — drain, enrich, keep, include, exclude, routes, plugins
MiddlewareLoggerResultReturn type: { logger, finish, skipped }
extractSafeHeaders(headers)Filter sensitive headers from a Web API Headers object (used internally)
extractSafeNodeHeaders(headers)Filter sensitive headers from Node.js IncomingHttpHeaders (used internally)
createLoggerStorage(hint)Factory returning { storage, useLogger } backed by AsyncLocalStorage
defineEvlog(config)Canonical config object — works for initLogger and middleware options
definePlugin(plugin)Plugin contract — opt into any subset of setup, enrich, drain, keep, onRequestStart, onRequestFinish, onClientLog, extendLogger
composeEnrichers / composeDrains / composeKeep / composePluginsCombine multiple extensions into one
defineEnricher(spec)Build a single-field enricher with built-in error isolation
defineHttpDrain(spec)Build an HTTP drain — provide resolve() and encode(), get retries/timeout/error handling for free
httpPost(opts)The same retried POST helper used by every built-in adapter
resolveAdapterConfig(ns, fields, overrides)Standard config priority chain (overrides → runtimeConfig → env)

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

Most frameworks fit a (ctx, next) middleware shape. For those, write a manifest describing how to extract the request and attach the logger — defineFrameworkIntegration does the rest.

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

export type MyFrameworkEvlogOptions = BaseEvlogOptions

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

export { useLogger }

const integration = defineFrameworkIntegration<IncomingMessage>({
  name: 'my-framework',
  extractRequest: (req) => ({
    method: req.method || 'GET',
    path: req.url || '/',
    headers: req.headers,
    requestId: typeof req.headers['x-request-id'] === 'string'
      ? req.headers['x-request-id']
      : undefined,
  }),
  attachLogger: (req, logger) => {
    (req as IncomingMessage & { log: RequestLogger }).log = logger
  },
  storage,
})

export function evlog(options: MyFrameworkEvlogOptions = {}) {
  return async (req: IncomingMessage, res: ServerResponse, next: () => Promise<void>) => {
    const { skipped, finish, runWith } = integration.start(req, options)
    if (skipped) {
      await next()
      return
    }
    try {
      await runWith(() => 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, plugin lifecycle hooks, log.fork(), and duration tracking.

What defineFrameworkIntegration does

Given the manifest above, the helper:

  1. Normalizes headers (auto-detects Headers vs IncomingHttpHeaders).
  2. Generates a requestId if extractRequest doesn't return one.
  3. Calls createMiddlewareLogger with the merged options.
  4. Calls attachLogger(ctx, logger).
  5. Attaches log.fork() to the logger when storage is provided (so users can spawn correlated background work).
  6. Exposes runWith(fn) — runs fn() inside storage.run(logger, …) if storage is configured, otherwise just calls fn().

You're left with only the framework-specific glue: where to read the request from, where to attach the logger, and how to compute the response status.

Custom Mode

If your framework's lifecycle doesn't fit a clean (ctx, next) shape (NestJS interceptors, Next.js App Router, SvelteKit handle), drop one level lower and call createMiddlewareLogger directly:

import { createMiddlewareLogger, extractSafeNodeHeaders } from 'evlog/toolkit'

const { logger, finish, skipped } = createMiddlewareLogger({
  method,
  path,
  requestId,
  headers: extractSafeNodeHeaders(rawHeaders),
  ...options,
})

You'll be responsible for ALS wrapping (storage.run), log.fork() attachment (via attachForkToLogger), and finishing the lifecycle — but you keep the full pipeline (route filtering, sampling, emit, enrich, drain, plugins) for free.

Reference Implementations

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

FrameworkLinesModeSource
Hono~50manifesthono/index.ts
Express~50manifest + ALSexpress/index.ts
Fastify~70manifest + Fastify hooksfastify/index.ts
Elysia~80manifest + custom ALS scopingelysia/index.ts
NestJS~120custom (interceptor)nestjs/
SvelteKit~90custom (handle hook)sveltekit/
Built an integration for a framework we don't support? Open a PR — the community will thank you.

Next Steps