Custom Integration
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.
Build an evlog integration for my framework
Install
pnpm add evlog
bun add evlog
yarn add evlog
npm install evlog
What's in the Toolkit
| Export | Purpose |
|---|---|
defineFrameworkIntegration(spec) | Manifest factory — extract request, create logger, attach, run with ALS |
createMiddlewareLogger(opts) | Lower-level lifecycle (custom mode) |
BaseEvlogOptions | Base user-facing options — drain, enrich, keep, include, exclude, routes, plugins |
MiddlewareLoggerResult | Return 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 / composePlugins | Combine 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.
Manifest Mode (recommended)
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.
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:
- Normalizes headers (auto-detects
HeadersvsIncomingHttpHeaders). - Generates a
requestIdifextractRequestdoesn't return one. - Calls
createMiddlewareLoggerwith the merged options. - Calls
attachLogger(ctx, logger). - Attaches
log.fork()to the logger whenstorageis provided (so users can spawn correlated background work). - Exposes
runWith(fn)— runsfn()insidestorage.run(logger, …)if storage is configured, otherwise just callsfn().
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:
| Framework | Lines | Mode | Source |
|---|---|---|---|
| Hono | ~50 | manifest | hono/index.ts |
| Express | ~50 | manifest + ALS | express/index.ts |
| Fastify | ~70 | manifest + Fastify hooks | fastify/index.ts |
| Elysia | ~80 | manifest + custom ALS scoping | elysia/index.ts |
| NestJS | ~120 | custom (interceptor) | nestjs/ |
| SvelteKit | ~90 | custom (handle hook) | sveltekit/ |
Next Steps
- Toolkit reference: All toolkit primitives in one place
- 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, andlinkfields