Examples

Hono

Source Code
Using evlog with Hono — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in Hono applications.

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

src/index.ts
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:

src/index.ts
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:

Terminal output
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:

src/index.ts
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:

src/index.ts
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:

Terminal output
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:

src/index.ts
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:

src/index.ts
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 }))
Call 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:

src/index.ts
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:

src/index.ts
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

client.ts
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:

src/index.ts
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)
})
See the full Browser Drain adapter docs for batching, retry, sendBeacon fallback, and authentication options.

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.

Source Code

Browse the complete Hono example source on GitHub.