Hono
The evlog/hono middleware auto-creates a request-scoped logger accessible via c.get('log') and emits a wide event when the response completes.
Set up evlog in my Hono 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 middleware and EvlogVariables type from 'evlog/hono'
- Add app.use(evlog()) and type the app with Hono<EvlogVariables>
- Access the logger via c.get('log') in route handlers
- Use log.set() to accumulate context throughout the request
- Optionally pass drain, enrich, include, and keep options to evlog()
Docs: https://www.evlog.dev/frameworks/hono
Adapters: https://www.evlog.dev/adapters
Quick Start
1. Install
bun add evlog hono @hono/node-server
2. Initialize and register the middleware
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
initLogger({
env: { service: 'my-api' },
})
const app = new Hono<EvlogVariables>()
app.use(evlog())
app.get('/health', (c) => {
c.get('log').set({ route: 'health' })
return c.json({ ok: true })
})
serve({ fetch: app.fetch, port: 3000 })
evlog/vite plugin replaces the initLogger() call with compile-time auto-initialization, strips log.debug() from production builds, and injects source locations.The EvlogVariables type gives you typed access to c.get('log') across all route handlers.
Wide Events
Build up context progressively through your handler. One request = one wide event:
app.get('/users/:id', async (c) => {
const log = c.get('log')
const userId = c.req.param('id')
log.set({ user: { id: userId } })
const user = await db.findUser(userId)
log.set({ user: { name: user.name, plan: user.plan } })
const orders = await db.findOrders(userId)
log.set({ orders: { count: orders.length, totalRevenue: sum(orders) } })
return c.json({ 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-...
Error Handling
Use createError for structured errors with why, fix, and link fields:
import { createError, parseError } from 'evlog'
app.get('/checkout', (c) => {
const log = c.get('log')
log.set({ cart: { items: 3, total: 9999 } })
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',
})
})
Handle errors globally with app.onError to return structured JSON responses:
app.onError((error, c) => {
c.get('log').error(error)
const parsed = parseError(error)
return c.json(
{
message: parsed.message,
why: parsed.why,
fix: parsed.fix,
link: parsed.link,
},
parsed.status,
)
})
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
├─ cart: items=3 total=9999
└─ 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 middleware options:
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
const userAgent = createUserAgentEnricher()
app.use(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())
app.use(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:
app.use(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:
app.use(evlog({
include: ['/api/**'],
exclude: ['/_internal/**', '/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
},
}))
Client-Side Logging
Use evlog/browser to send structured logs from any frontend to your Hono server. This works with any client framework (React, Vue, Svelte, vanilla JS).
Browser setup
import { initLogger, log } from 'evlog'
import { createBrowserLogDrain } from 'evlog/browser'
const drain = createBrowserLogDrain({
drain: { endpoint: '/v1/ingest' },
})
initLogger({ drain })
log.info({ action: 'page_view', path: location.pathname })
Ingest endpoint
Add a POST route to receive batched DrainContext[] from the browser:
import type { DrainContext } from 'evlog'
app.post('/v1/ingest', async (c) => {
const batch = await c.req.json<DrainContext[]>()
for (const ctx of batch) {
console.log('[BROWSER]', JSON.stringify(ctx.event))
}
return c.body(null, 204)
})
Run Locally
git clone https://github.com/HugoRCD/evlog.git
cd evlog
bun install
bun run example:hono
Open http://localhost:3000 to explore the interactive test UI.