---
title: "Hono"
description: "One middleware puts a typed logger on `c.get('log')`, defers the event until a streamed body closes, and uses `waitUntil` where the runtime has it."
canonical_url: "https://www.evlog.dev/integrate/frameworks/hono"
---
# Hono

> One middleware puts a typed logger on `c.get('log')`, defers the event until a streamed body closes, and uses `waitUntil` where the runtime has it.

The `evlog/hono` middleware auto-creates a request-scoped logger accessible via `c.get('log')`, or `useLogger()` anywhere deeper in the call stack, and emits a wide event when the response completes.

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my Hono app" icon="i-simple-icons-hono">

Set up evlog in my Hono app.

- Install evlog: pnpm add evlog
- Create evlog.config.ts with export default defineEvlog({ service: 'my-api' })
- Call initLogger(toLoggerConfig(config)) at startup, importing config from evlog.config.ts
- Import evlog middleware and EvlogVariables type from 'evlog/hono'
- Add app.use(evlog(toMiddlewareOptions(config))) and type the app with Hono<evlog-variables>



</evlog-variables>
- Access the logger via c.get('log') in route handlers, or useLogger() from 'evlog/hono' in nested functions
- Use log.set() to accumulate context throughout the request
- Put drain, enrich, include and keep in evlog.config.ts; toMiddlewareOptions(config) passes them to evlog()

Docs: [https://www.evlog.dev/integrate/frameworks/hono](https://www.evlog.dev/integrate/frameworks/hono)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog hono @hono/node-server
```

```bash [bun]
bun add evlog hono @hono/node-server
```

```bash [yarn]
yarn add evlog hono @hono/node-server
```

```bash [npm]
npm install evlog hono @hono/node-server
```

</code-group>

### 2. Create evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-api',
})
```

Every option lives in this file: sampling, drain, `enrich`, route filters. `npx evlog init` writes it, and the wiring below, for you. See [Configuration](https://www.evlog.dev/reference/configuration).

### 3. Initialize and register the middleware

```typescript [src/index.ts]
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { initLogger, toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

const app = new Hono<EvlogVariables>()

app.use(evlog(toMiddlewareOptions(config)))

app.get('/health', (c) => {
  c.get('log').set({ route: 'health' })
  return c.json({ ok: true })
})

serve({ fetch: app.fetch, port: 3000 })
```

<callout color="info" icon="i-custom-vite">

**Using Vite?** The [`evlog/vite` plugin](https://www.evlog.dev/reference/vite-plugin) strips `log.debug()` from production builds and injects source locations. It does not read `evlog.config.ts`, so keep the `initLogger(toLoggerConfig(config))` call: it runs after the plugin's auto-init and replaces it.

</callout>

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:

```typescript [src/index.ts]
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:

```bash [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-...
```

## Accessing the logger deeper in the stack

`c.get('log')` is the idiomatic accessor inside a route handler. Once you are a few
layers down (a service, a repository), threading `c` everywhere gets noisy. `useLogger()`
resolves the same request logger without it:

```typescript [src/services/payment.ts]
import { useLogger } from 'evlog/hono'

export async function chargeCard(amount: number) {
  const log = useLogger()
  log.set({ payment: { amount } })
}
```

Both return the same logger, so pick whichever reads better at the call site.

<callout color="info" icon="i-lucide-info">

`useLogger()` is backed by `AsyncLocalStorage`. On Cloudflare Workers this requires the
`nodejs_compat` (or `nodejs_als`) compatibility flag in `wrangler.toml`. `c.get('log')`
works with or without it, so deployments that cannot enable the flag keep using it.

</callout>

### Background work

**log.fork()** runs work under a forked logger that emits its own wide event, correlated
to the request via `_parentRequestId`:

```typescript [src/index.ts]
app.post('/api/orders', (c) => {
  c.get('log').fork('send-receipt', async () => {
    await sendReceipt()
    useLogger().set({ email: { sent: true } })
  })
  return c.json({ ok: true })
})
```

If you schedule async work after the response without forking, post-emit **[evlog] warnings**
help you notice stale `set()` calls. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

## Error Handling

Use `createError` for structured errors with `why`, `fix`, and `link` fields:

```typescript [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:

```typescript [src/index.ts]
import type { ContentfulStatusCode } from 'hono/utils/http-status'

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 as ContentfulStatusCode,
  )
})
```

`parseError()` types `status` as a `number`, while Hono’s `c.json()` second argument expects `ContentfulStatusCode`. The cast matches what you already return at runtime and satisfies TypeScript.

The error is captured and logged with both the custom context and structured error fields:

```bash [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-...
```

## Configuration

Every option goes in `evlog.config.ts`. See the [Configuration reference](https://www.evlog.dev/reference/configuration) for the full list.

## Drain & Enrichers

Set `drain` and `enrich` in `evlog.config.ts` so they reach the app through `toMiddlewareOptions(config)`. You can also pass them inline in the middleware options; an inline key replaces the one from the file instead of merging with it:

```typescript [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
  },
}))
```

### Pipeline (Batching & Retry)

For production, wrap your adapter with `createDrainPipeline` to batch events and retry on failure:

```typescript [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 }))
```

<callout color="info" icon="i-lucide-info">

Call `drain.flush()` on server shutdown to ensure all buffered events are sent. See the [Pipeline docs](https://www.evlog.dev/extend/drain-pipeline) for all options.

</callout>

## Tail Sampling

Use `keep` to force-retain specific events regardless of head sampling:

```typescript [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:

```typescript [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/http` to send structured logs from any frontend to your Hono server. This works with any client framework (React, Vue, Svelte, vanilla JS).

### Browser setup

```typescript [client.ts]
import { initLogger, log } from 'evlog'
import { createHttpLogDrain } from 'evlog/http'

const drain = createHttpLogDrain({
  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:

```typescript [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)
})
```

<callout color="neutral" icon="i-lucide-globe">

See the full [HTTP drain](https://www.evlog.dev/extend/drain-pipeline) adapter docs for batching, retry, sendBeacon fallback, and authentication options.

</callout>

## Run Locally

```bash [Terminal]
git clone https://github.com/evloghq/evlog.git
cd evlog
pnpm install
pnpm example hono
```

Open [http://localhost:3000](http://localhost:3000) to explore the interactive test UI.

<card-group>
<card icon="i-simple-icons-github" title="Source Code" to="https://github.com/evloghq/evlog/tree/main/examples/hono">

Browse the complete Hono example source on GitHub.

</card>
</card-group>

## Next Steps

Deepen your **Hono** integration:

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields

---

- [Source Code](https://github.com/evloghq/evlog/tree/main/examples/hono)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
