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 ~50 lines of code.
Install
pnpm add evlog
npm install evlog
bun add evlog
What's in the Toolkit
| Export | Purpose |
|---|---|
createMiddlewareLogger(opts) | Full pipeline: logger creation, route filtering, tail sampling, emit, enrich, drain |
BaseEvlogOptions | Base user-facing options type with drain, enrich, keep, include, exclude, routes |
MiddlewareLoggerOptions | Internal options extending BaseEvlogOptions with method, path, requestId, headers |
MiddlewareLoggerResult | Return 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()
- Extract
method,path,requestId, andheadersfrom the framework request - Call
createMiddlewareLogger()with those fields + user options - Check
skipped— iftrue, the route is filtered out, skip to next middleware - Store the
loggerin the framework's idiomatic context (req.log,c.set('log'), etc.) - Call
finish({ status })on success orfinish({ 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:
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
- Always use
createMiddlewareLogger— never callcreateRequestLoggerdirectly - Use the right header extractor —
extractSafeHeadersfor Web APIHeaders(Hono, Elysia, Deno),extractSafeNodeHeadersfor Node.jsIncomingHttpHeaders(Express, Fastify) - Spread user options —
...optionspassesdrain,enrich,keep,include,excludeto the pipeline automatically - Call
finish()in both paths — success ({ status }) and error ({ error }) — it handles emit + enrich + drain - Re-throw errors after
finish()so framework error handlers still work - Export
useLogger()— consumers expect it for accessing the logger from service functions - Export your options type extending
BaseEvlogOptions— for IDE completion ondrain,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:
| Framework | Lines | Pattern | Source |
|---|---|---|---|
| Hono | ~40 | Web API Headers, c.set(), try/catch | hono/index.ts |
| Express | ~60 | Node.js headers, req.log, res.on('finish') | express/index.ts |
| Elysia | ~70 | Plugin API, derive(), onAfterHandle/onError | elysia/index.ts |
| Fastify | ~70 | Plugin, decorateRequest, onRequest/onResponse hooks | fastify/index.ts |