Fastify
The evlog/fastify plugin auto-creates a request-scoped logger accessible via request.log and useLogger(), emitting a wide event when the response completes.
Set up evlog in my Fastify app.
- Install evlog: pnpm add evlog
- Call initLogger({ env: { service: 'my-api' } }) at startup
- Alternatively, use evlog/vite plugin in vite.config.ts for auto-init (replaces initLogger)
- Import evlog from 'evlog/fastify' and register with app.register(evlog)
- Access the logger via request.log in route handlers or useLogger() anywhere
- Use log.set() to accumulate context throughout the request
- Optionally pass drain, enrich, include, and keep options when registering
Docs: https://www.evlog.dev/frameworks/fastify
Adapters: https://www.evlog.dev/adapters
Quick Start
1. Install
bun add evlog fastify
2. Initialize and register the plugin
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog } from 'evlog/fastify'
initLogger({
env: { service: 'my-api' },
})
const app = Fastify({ logger: false })
await app.register(evlog)
app.get('/health', async (request) => {
request.log.set({ route: 'health' })
return { ok: true }
})
await app.listen({ port: 3000 })
evlog/vite plugin replaces the initLogger() call with compile-time auto-initialization, strips log.debug() from production builds, and injects source locations.request.log is the evlog wide-event logger and shadows Fastify's built-in pino logger on the request. The pino logger remains accessible via fastify.log for server-level structured logging.
Wide Events
Build up context progressively through your handler. One request = one wide event:
app.get('/users/:id', async (request) => {
const { id } = request.params as { id: string }
request.log.set({ user: { id } })
const user = await db.findUser(id)
request.log.set({ user: { name: user.name, plan: user.plan } })
const orders = await db.findOrders(id)
request.log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })
return { user, orders }
})
All fields are merged into a single wide event emitted when the request completes:
14:58:15 INFO [my-api] GET /users/usr_123 200 in 12ms
├─ orders: count=2 totalRevenue=6298
├─ user: id=usr_123 name=Alice plan=pro
└─ requestId: 4a8ff3a8-...
useLogger()
Use useLogger() to access the request-scoped logger from anywhere in the call stack without passing the request object through your service layer:
import { useLogger } from 'evlog/fastify'
export async function findUser(id: string) {
const log = useLogger()
log.set({ user: { id } })
const user = await db.findUser(id)
log.set({ user: { name: user.name, plan: user.plan } })
return user
}
import { findUser } from './services/user'
app.get('/users/:id', async (request) => {
const { id } = request.params as { id: string }
const user = await findUser(id)
return user
})
Both request.log and useLogger() return the same logger instance. useLogger() uses AsyncLocalStorage to propagate the logger across async boundaries.
Error Handling
Use createError for structured errors with why, fix, and link fields. Fastify captures thrown errors via onError:
import { createError, parseError } from 'evlog'
app.get('/checkout', async (_request, reply) => {
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
})
app.setErrorHandler((error, _request, reply) => {
const parsed = parseError(error)
reply.status(parsed.status).send({
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
link: parsed.link,
})
})
The error is captured and logged with both the custom context and structured error fields:
14:58:20 ERROR [my-api] GET /checkout 402 in 3ms
├─ error: name=EvlogError message=Payment failed status=402
└─ requestId: 880a50ac-...
Configuration
See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).
Drain & Enrichers
Configure drain adapters and enrichers directly in the plugin options:
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
const userAgent = createUserAgentEnricher()
await app.register(evlog, {
drain: createAxiomDrain(),
enrich: (ctx) => {
userAgent(ctx)
ctx.event.region = process.env.FLY_REGION
},
})
Pipeline (Batching & Retry)
For production, wrap your adapter with createDrainPipeline to batch events and retry on failure:
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 50, intervalMs: 5000 },
retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())
await app.register(evlog, { drain })
drain.flush() on server shutdown to ensure all buffered events are sent. See the Pipeline docs for all options.Tail Sampling
Use keep to force-retain specific events regardless of head sampling:
await app.register(evlog, {
drain: createAxiomDrain(),
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
Route Filtering
Control which routes are logged with include and exclude patterns:
await app.register(evlog, {
include: ['/api/**'],
exclude: ['/_internal/**', '/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
},
})
Run Locally
git clone https://github.com/HugoRCD/evlog.git
cd evlog
bun install
bun run example:fastify
Open http://localhost:3000 to explore the interactive test UI.