---
title: "ClickHouse Adapter"
description: "Insert wide events into ClickHouse over the HTTP interface — typed columns for what you aggregate, full JSON for everything else."
canonical_url: "https://www.evlog.dev/integrate/adapters/hybrid/clickhouse"
---
# ClickHouse Adapter

> Insert wide events into ClickHouse over the HTTP interface — typed columns for what you aggregate, full JSON for everything else.

The ClickHouse adapter inserts wide events over the [HTTP interface](https://clickhouse.com/docs/en/interfaces/http) in `JSONEachRow` format. It works with a local instance, a self-managed cluster, and ClickHouse Cloud.

The default schema gives you **typed columns** for the fields you filter and aggregate on, plus the complete wide event as JSON in `data`, so no field is ever lost, and adding a field to your events never requires a migration.

<prompt :actions="["copy","cursor","claude"]" description="Add the ClickHouse drain adapter" icon="i-simple-icons-clickhouse">

Add the ClickHouse drain adapter to store evlog wide events in ClickHouse.

1. Identify which framework I'm using and follow its evlog integration pattern
2. Install evlog if not already installed
3. Create the evlog_events table using the CREATE TABLE from the adapter docs
4. Import createClickHouseDrain from 'evlog/clickhouse'
5. Wire createClickHouseDrain() into my framework's drain configuration
6. Set CLICKHOUSE_ENDPOINT (e.g. [http://localhost:8123](http://localhost:8123)), plus CLICKHOUSE_USER / CLICKHOUSE_PASSWORD if authenticated
7. Test by triggering a request and running SELECT * FROM evlog_events ORDER BY timestamp DESC LIMIT 10

Adapter docs: [https://www.evlog.dev/integrate/adapters/hybrid/clickhouse](https://www.evlog.dev/integrate/adapters/hybrid/clickhouse)
Framework setup: [https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)

</prompt>

## Installation

The ClickHouse adapter comes bundled with evlog:

```typescript [src/index.ts]
import { createClickHouseDrain } from 'evlog/clickhouse'
```

## Create the table

Run this once before wiring the drain:

```sql [schema.sql]
CREATE TABLE IF NOT EXISTS evlog_events
(
  timestamp     DateTime64(3, 'UTC'),
  level         LowCardinality(String),
  service       LowCardinality(String),
  environment   LowCardinality(String),
  request_id    String,
  trace_id      String,
  span_id       String,
  method        LowCardinality(String),
  path          String,
  status        Nullable(UInt16),
  duration      String,
  duration_ms   Nullable(UInt32),
  error_name    String,
  error_message String,
  data          String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (service, environment, timestamp)
TTL toDateTime(timestamp) + INTERVAL 30 DAY;
```

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

`ORDER BY (service, environment, timestamp)` matches how you'll filter most often. Adjust the `TTL` to your retention policy, and drop the clause entirely to keep events forever.

</callout>

## Quick Start

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog.ts
import { createClickHouseDrain } from 'evlog/clickhouse'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain())
})
```

```typescript [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain())
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createClickHouseDrain(),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { handle, handleError } = createEvlogHooks({ drain: createClickHouseDrain() })
```

```typescript [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain())
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: createClickHouseDrain() }),
]
```

```typescript [Hono]src/index.ts
import { Hono } from 'hono'
import { evlog } from 'evlog/hono'
import { createClickHouseDrain } from 'evlog/clickhouse'

const app = new Hono()
app.use(evlog({ drain: createClickHouseDrain() }))
```

```typescript [Express]src/index.ts
import express from 'express'
import { evlog } from 'evlog/express'
import { createClickHouseDrain } from 'evlog/clickhouse'

const app = express()
app.use(evlog({ drain: createClickHouseDrain() }))
```

```typescript [Fastify]src/index.ts
import Fastify from 'fastify'
import { evlog } from 'evlog/fastify'
import { createClickHouseDrain } from 'evlog/clickhouse'

const app = Fastify()
await app.register(evlog, { drain: createClickHouseDrain() })
```

```typescript [Elysia]src/index.ts
import { Elysia } from 'elysia'
import { evlog } from 'evlog/elysia'
import { createClickHouseDrain } from 'evlog/clickhouse'

const app = new Elysia().use(evlog({ drain: createClickHouseDrain() }))
```

```typescript [NestJS]src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
import { createClickHouseDrain } from 'evlog/clickhouse'

@Module({
  imports: [EvlogModule.forRoot({ drain: createClickHouseDrain() })],
})
export class AppModule {}
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createClickHouseDrain } from 'evlog/clickhouse'

const handler = withEvlog(new RPCHandler(router), { drain: createClickHouseDrain() })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default withEvlog(handler, { drain: createClickHouseDrain() })
```

```typescript [Standalone]
import { initLogger } from 'evlog'
import { createClickHouseDrain } from 'evlog/clickhouse'

initLogger({
  env: { service: 'my-app' },
  drain: createClickHouseDrain(),
})
```

</framework-tabs>

## Configuration

### Environment variables

<table>
<thead>
  <tr>
    <th>
      Variable
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        CLICKHOUSE_ENDPOINT
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      HTTP interface URL (<code>
        CLICKHOUSE_URL
      </code>
      
       also accepted)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_USER
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Username. Default <code>
        default
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_PASSWORD
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Password. Omit for an unauthenticated local instance
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_DATABASE
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Database. Default <code>
        default
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_TABLE
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Table. Default <code>
        evlog_events
      </code>
    </td>
  </tr>
</tbody>
</table>

### Options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        endpoint
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      HTTP interface URL
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        database
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      Target database
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        table
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        evlog_events
      </code>
    </td>
    
    <td>
      Target table
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        username
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      Username
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        password
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Password
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        asyncInsert
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Batch inserts server-side
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        waitForAsyncInsert
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Wait for the flush before responding
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        transform
      </code>
    </td>
    
    <td>
      <code>
        (event) => Record<string, unknown>
      </code>
    </td>
    
    <td>
      <code>
        toClickHouseRow
      </code>
    </td>
    
    <td>
      Map an event to a row
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timeout
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        5000
      </code>
    </td>
    
    <td>
      Request timeout in ms
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        retries
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      Retry attempts
    </td>
  </tr>
</tbody>
</table>

## Deployment

The adapter is identical either way. Only the endpoint and credentials change.

### Self-hosted

A local or self-managed instance, with or without auth:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createClickHouseDrain } from 'evlog/clickhouse'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({ endpoint: 'http://localhost:8123' }))
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({ endpoint: 'http://localhost:8123' }))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { handle, handleError } = createEvlogHooks({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({ endpoint: 'http://localhost:8123' }))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) }))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) }))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createClickHouseDrain } from 'evlog/clickhouse'

await app.register(evlog, { drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) }))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createClickHouseDrain } from 'evlog/clickhouse'

EvlogModule.forRoot({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createClickHouseDrain } from 'evlog/clickhouse'

const handler = withEvlog(new RPCHandler(router), { drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default withEvlog(handler, { drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createClickHouseDrain } from 'evlog/clickhouse'

initLogger({ drain: createClickHouseDrain({ endpoint: 'http://localhost:8123' }) })
```

</framework-tabs>

```bash [.env]
CLICKHOUSE_ENDPOINT=http://localhost:8123
# Only if your instance requires auth:
CLICKHOUSE_USER=evlog
CLICKHOUSE_PASSWORD=your-password
```

A fresh container runs as `default` with no password, which is why `username`
defaults to `default` and `password` is optional.

### ClickHouse Cloud

Cloud services always require credentials and speak HTTPS on port 8443:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createClickHouseDrain } from 'evlog/clickhouse'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }))
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { handle, handleError } = createEvlogHooks({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createClickHouseDrain({
        endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
        password: process.env.CLICKHOUSE_PASSWORD,
        database: 'logs',
      }),
    }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createClickHouseDrain } from 'evlog/clickhouse'

await app.register(evlog, {
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createClickHouseDrain } from 'evlog/clickhouse'

EvlogModule.forRoot({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createClickHouseDrain } from 'evlog/clickhouse'

const handler = withEvlog(new RPCHandler(router), {
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default withEvlog(handler, {
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createClickHouseDrain } from 'evlog/clickhouse'

initLogger({
  drain: createClickHouseDrain({
    endpoint: 'https://abc123.eu-west-1.aws.clickhouse.cloud:8443',
    password: process.env.CLICKHOUSE_PASSWORD,
    database: 'logs',
  }),
})
```

</framework-tabs>

```bash [.env]
CLICKHOUSE_ENDPOINT=https://abc123.eu-west-1.aws.clickhouse.cloud:8443
CLICKHOUSE_PASSWORD=your-service-password
CLICKHOUSE_DATABASE=logs
```

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

Copy the endpoint from *Connect → HTTPS* in the ClickHouse Cloud console,
including the `:8443` port. Cloud services idle-suspend, so the first insert
after a pause can take a few seconds; raise `timeout` if you see aborts.

</callout>

<callout color="success" icon="i-lucide-shield-check">

Credentials are sent as `X-ClickHouse-User` / `X-ClickHouse-Key` **headers**,
never as query parameters, so they never reach `system.query_log` or an
intermediate proxy's access log.

</callout>

## Verify it locally

Spin up ClickHouse in Docker, create the table and push a real event:

```bash [Terminal]
docker compose -f packages/evlog/test/e2e/docker-compose.yml up -d
CLICKHOUSE_ENDPOINT=http://localhost:8123 pnpm run test:e2e
docker compose -f packages/evlog/test/e2e/docker-compose.yml down -v
```

The suite is a **round-trip**: it inserts events, reads them back with
`SELECT`, and asserts the typed columns and the `data` JSON survived. The
compose file creates `evlog_events` from the schema above on first boot.
Without `CLICKHOUSE_ENDPOINT` it skips itself with a visible label.

To *see* the events rather than assert on them, seed the sandbox and open the
provisioned dashboard. The stack ships a Grafana with the ClickHouse
datasource already wired up:

```bash [Terminal]
pnpm run sandbox:up
pnpm run sandbox:seed
# → http://localhost:3001/d/evlog-wide-events
```

ClickHouse's built-in `/play` page at `http://localhost:8123/play` also works
for raw SQL, but it is deliberately minimal, and the dashboard is the better
starting point.

## Async inserts

Log ingestion means many small inserts, and one MergeTree part per request would quickly degrade a table. The adapter therefore enables [asynchronous inserts](https://clickhouse.com/docs/en/optimize/asynchronous-inserts) by default, and does **not** wait for the flush:

```text
async_insert=1&wait_for_async_insert=0
```

ClickHouse buffers rows server-side and writes them in batches. Draining never blocks a request on disk writes.

Set `waitForAsyncInsert: true` if you need the insert acknowledged as durable before the drain resolves, at the cost of latency. Set `asyncInsert: false` to insert synchronously, which only makes sense when you already batch client-side with `evlog/pipeline`.

## Querying

Typed columns are indexed by the sort key; everything else lives in `data` and is reachable with ClickHouse's JSON functions:

```sql
-- Error rate per service over the last hour
SELECT service, countIf(level = 'error') / count() AS error_rate
FROM evlog_events
WHERE timestamp > now() - INTERVAL 1 HOUR
GROUP BY service;
```

```sql
-- Slowest routes
SELECT path, count() AS hits, avg(duration_ms) AS avg_ms, quantile(0.95)(duration_ms) AS p95_ms
FROM evlog_events
WHERE timestamp > now() - INTERVAL 1 DAY AND method = 'GET' AND duration_ms IS NOT NULL
GROUP BY path
ORDER BY avg_ms DESC
LIMIT 20;
```

```sql
-- One request end to end
SELECT timestamp, level, path, status, data
FROM evlog_events
WHERE request_id = '4a8ff3a8-...'
ORDER BY timestamp;
```

```sql
-- Reach into a custom field kept only in data
SELECT JSONExtractString(data, 'user', 'plan') AS plan, count()
FROM evlog_events
WHERE timestamp > now() - INTERVAL 7 DAY
GROUP BY plan;
```

## Custom schema

Pass `transform` to target a table of your own shape:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createClickHouseDrain } from 'evlog/clickhouse'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }))
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const { handle, handleError } = createEvlogHooks({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createClickHouseDrain } from 'evlog/clickhouse'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createClickHouseDrain({
        table: 'app_logs',
        transform: event => ({
          ts: event.timestamp,
          lvl: event.level,
          svc: event.service,
          payload: JSON.stringify(event),
        }),
      }),
    }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createClickHouseDrain } from 'evlog/clickhouse'

await app.register(evlog, {
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createClickHouseDrain } from 'evlog/clickhouse'

app.use(evlog({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createClickHouseDrain } from 'evlog/clickhouse'

EvlogModule.forRoot({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createClickHouseDrain } from 'evlog/clickhouse'

const handler = withEvlog(new RPCHandler(router), {
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createClickHouseDrain } from 'evlog/clickhouse'

export default withEvlog(handler, {
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createClickHouseDrain } from 'evlog/clickhouse'

initLogger({
  drain: createClickHouseDrain({
    table: 'app_logs',
    transform: event => ({
      ts: event.timestamp,
      lvl: event.level,
      svc: event.service,
      payload: JSON.stringify(event),
    }),
  }),
})
```

</framework-tabs>

Keys must match your column names, because ClickHouse rejects a `JSONEachRow` insert containing unknown columns.

## Batching

Pair with a pipeline to insert in larger batches:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog.ts
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', pipeline(createClickHouseDrain()))
})
```

```typescript [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', pipeline(createClickHouseDrain()))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: pipeline(createClickHouseDrain()),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
export const { handle, handleError } = createEvlogHooks({ drain: pipeline(createClickHouseDrain()) })
```

```typescript [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', pipeline(createClickHouseDrain()))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: pipeline(createClickHouseDrain()) }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
app.use(evlog({ drain: pipeline(createClickHouseDrain()) }))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
app.use(evlog({ drain: pipeline(createClickHouseDrain()) }))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
await app.register(evlog, { drain: pipeline(createClickHouseDrain()) })
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
app.use(evlog({ drain: pipeline(createClickHouseDrain()) }))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
EvlogModule.forRoot({ drain: pipeline(createClickHouseDrain()) })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
const handler = withEvlog(new RPCHandler(router), { drain: pipeline(createClickHouseDrain()) })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
export default withEvlog(handler, { drain: pipeline(createClickHouseDrain()) })
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createDrainPipeline } from 'evlog/pipeline'
import { createClickHouseDrain } from 'evlog/clickhouse'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 500, intervalMs: 5000 },
})
initLogger({ drain: pipeline(createClickHouseDrain()) })
```

</framework-tabs>

## Troubleshooting

**Missing endpoint.** `CLICKHOUSE_ENDPOINT` is unset and no `endpoint` was passed. The drain logs `[evlog/clickhouse] Missing endpoint` once and then does nothing, so requests are never blocked or failed.

**ClickHouse API error: 401.** Check `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD`. Credentials are sent as `X-ClickHouse-User` / `X-ClickHouse-Key` headers, never in the query string, so they never reach `system.query_log`.

**ClickHouse API error: 400 mentioning unknown column.** Your table does not match the row shape. Either create the table from the DDL above, or supply a `transform` matching your columns.

**Cannot parse input.** The table exists but a column type disagrees. `status` is `Nullable(UInt16)` because it is absent on non-HTTP events.

## Direct API usage

```typescript [scripts/backfill.ts]
import { sendBatchToClickHouse, sendToClickHouse } from 'evlog/clickhouse'

await sendToClickHouse(event, { endpoint: 'http://localhost:8123' })
await sendBatchToClickHouse(events, { endpoint: 'http://localhost:8123' })
```

`toClickHouseRow()`, `toJSONEachRow()`, and `resolveClickHouseUrl()` are also exported.

## Next steps

- [Sampling](https://www.evlog.dev/learn/sampling): control volume before it reaches ClickHouse
- [Enrichers](https://www.evlog.dev/use-cases/enrichers): add derived fields to every event
- [Pipeline](https://www.evlog.dev/extend/drain-pipeline): batching, retries, and fan-out
- [Adapters Overview](https://www.evlog.dev/integrate/adapters/overview): all available destinations

---

- [HTTP Interface](https://clickhouse.com/docs/en/interfaces/http)
- [Async Inserts](https://clickhouse.com/docs/en/optimize/asynchronous-inserts)


## Sitemap

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