Hono
Practical patterns for using evlog with 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.
Setup
1. Install dependencies
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 })
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', (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-...
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
},
}))
Unlike Nuxt/Nitro where you register hooks in plugins, Hono passes drain and enrich as options to the middleware. The behavior is the same — enrich runs first, then drain.
Pipeline (Batching & Retry)
Without a pipeline, the drain is called once per request with a single event. 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.