---
title: "ClickHouse 适配器"
description: "通过 HTTP 接口将宽事件写入 ClickHouse——对需要聚合的内容使用类型化列，其余内容完整保留为 JSON。"
canonical_url: "https://evlog.zhcndoc.com/integrate/adapters/hybrid/clickhouse"
---
# ClickHouse 适配器

> 通过 HTTP 接口将宽事件写入 ClickHouse——对需要聚合的内容使用类型化列，其余内容完整保留为 JSON。

ClickHouse 适配器通过[HTTP 接口](https://clickhouse.com/docs/en/interfaces/http)，以 `JSONEachRow` 格式写入宽事件。它支持本地实例、自管理集群以及 ClickHouse Cloud。

默认架构会为你进行筛选和聚合的字段提供**类型化列**，并将完整的宽事件以 JSON 形式存储在 `data` 中，因此不会丢失任何字段，而且向事件中添加字段也永远不需要迁移。

<prompt :actions="["copy","cursor","claude"]" description="添加 ClickHouse drain 适配器" icon="i-simple-icons-clickhouse">

添加 ClickHouse drain 适配器，将 evlog 宽事件存储到 ClickHouse 中。

1. 识别我正在使用的框架，并遵循其 evlog 集成模式
2. 如果尚未安装 evlog，则进行安装
3. 使用适配器文档中的 CREATE TABLE 创建 evlog_events 表
4. 从 'evlog/clickhouse' 导入 createClickHouseDrain
5. 将 createClickHouseDrain() 接入我框架的 drain 配置
6. 设置 CLICKHOUSE_ENDPOINT（例如 [http://localhost:8123）；如果需要身份验证，同时设置](undefined) CLICKHOUSE_USER / CLICKHOUSE_PASSWORD
7. 通过触发一个请求并运行 SELECT * FROM evlog_events ORDER BY timestamp DESC LIMIT 10 进行测试

适配器文档：[https://www.evlog.dev/integrate/adapters/hybrid/clickhouse](https://www.evlog.dev/integrate/adapters/hybrid/clickhouse)
框架设置：[https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)

</prompt>

## 安装

ClickHouse 适配器已随 evlog 一起提供：

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

## 创建表

在接入排空程序之前运行一次：

```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)` 与你最常用的筛选方式相匹配。根据你的保留策略调整 `TTL`，如果要永久保留事件，则完全删除该子句。

</callout>

## 快速开始

<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>

## 配置

### 环境变量

<table>
<thead>
  <tr>
    <th>
      变量
    </th>
    
    <th>
      必填
    </th>
    
    <th>
      描述
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        CLICKHOUSE_ENDPOINT
      </code>
    </td>
    
    <td>
      是
    </td>
    
    <td>
      HTTP 接口 URL（也接受 <code>
        CLICKHOUSE_URL
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_USER
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      用户名。默认为 <code>
        default
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_PASSWORD
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      密码。未经身份验证的本地实例可省略
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_DATABASE
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      数据库。默认为 <code>
        default
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CLICKHOUSE_TABLE
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      表。默认为 <code>
        evlog_events
      </code>
    </td>
  </tr>
</tbody>
</table>

### 选项

<table>
<thead>
  <tr>
    <th>
      选项
    </th>
    
    <th>
      类型
    </th>
    
    <th>
      默认值
    </th>
    
    <th>
      描述
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        endpoint
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      HTTP 接口 URL
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        database
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      目标数据库
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        table
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        evlog_events
      </code>
    </td>
    
    <td>
      目标表
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        username
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      用户名
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        password
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      密码
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        asyncInsert
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      在服务器端批量插入
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        waitForAsyncInsert
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      响应前等待刷新完成
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        transform
      </code>
    </td>
    
    <td>
      <code>
        (event) => Record<string, unknown>
      </code>
    </td>
    
    <td>
      <code>
        toClickHouseRow
      </code>
    </td>
    
    <td>
      将事件映射为一行
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timeout
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        5000
      </code>
    </td>
    
    <td>
      请求超时时间（毫秒）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        retries
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      重试次数
    </td>
  </tr>
</tbody>
</table>

## 部署

无论采用哪种方式，适配器都相同。只有端点和凭据会发生变化。

### 自托管

本地或自行管理的实例，可选择启用或不启用身份验证：

<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
# 仅当你的实例需要身份验证时：
CLICKHOUSE_USER=evlog
CLICKHOUSE_PASSWORD=your-password
```

全新的容器以 `default` 用户运行且没有密码，这就是为什么
`username` 默认为 `default`，而 `password` 是可选的。

### ClickHouse Cloud

云服务始终需要凭据，并通过 HTTPS 的 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">

从 ClickHouse Cloud 控制台的 *Connect → HTTPS* 中复制端点，
包括 `:8443` 端口。云服务会在空闲时暂停，因此暂停后的第一次插入
可能需要几秒钟；如果看到中止错误，请增大 `timeout`。

</callout>

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

凭据通过 `X-ClickHouse-User` / `X-ClickHouse-Key` **请求头**发送，
绝不会作为查询参数发送，因此不会出现在 `system.query_log` 或
中间代理的访问日志中。

</callout>

## 在本地验证

在 Docker 中启动 ClickHouse，创建表并推送一个真实事件：

```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
```

该测试套件是一个**往返测试**：它插入事件，使用
`SELECT` 将其读回，并断言类型化列和 `data` JSON 均得以保留。
compose 文件会在首次启动时根据上述架构创建 `evlog_events`。
如果没有设置 `CLICKHOUSE_ENDPOINT`，测试会显示醒目标记并跳过自身。

若要*查看*事件而不是对其进行断言，请填充沙盒并打开
已配置的仪表板。该技术栈附带一个已连接 ClickHouse
数据源的 Grafana：

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

ClickHouse 内置的 `/play` 页面 `http://localhost:8123/play` 也可以
用于执行原始 SQL，但它有意保持简洁，而仪表板是更好的起点。

## 异步插入

日志摄取意味着会有许多小型插入操作，而每个请求对应一个 MergeTree 分区会很快导致表性能下降。因此，适配器默认启用[异步插入](https://clickhouse.com/docs/en/optimize/asynchronous-inserts)，并且**不会**等待刷新完成：

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

ClickHouse 会在服务器端缓冲行，并批量写入。排空操作不会因磁盘写入而阻塞请求。

如果需要在 drain 解析前确认插入已持久化，请设置 `waitForAsyncInsert: true`，代价是增加延迟。设置 `asyncInsert: false` 可进行同步插入，但这只有在你已经通过 `evlog/pipeline` 在客户端完成批处理时才有意义。

## 查询

按排序键对类型化列建立索引；其他所有内容都存储在 `data` 中，并可通过 ClickHouse 的 JSON 函数访问：

```sql
-- 过去一小时内每个服务的错误率
SELECT service, countIf(level = 'error') / count() AS error_rate
FROM evlog_events
WHERE timestamp > now() - INTERVAL 1 HOUR
GROUP BY service;
```

```sql
-- 最慢的路由
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
-- 一个请求的端到端记录
SELECT timestamp, level, path, status, data
FROM evlog_events
WHERE request_id = '4a8ff3a8-...'
ORDER BY timestamp;
```

```sql
-- 访问仅保存在 data 中的自定义字段
SELECT JSONExtractString(data, 'user', 'plan') AS plan, count()
FROM evlog_events
WHERE timestamp > now() - INTERVAL 7 DAY
GROUP BY plan;
```

## 自定义架构

传入 `transform` 以针对自定义结构的表：

<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>

键必须与列名匹配，因为 ClickHouse 会拒绝包含未知列的 `JSONEachRow` 插入。

## 批处理

与管道配合使用，以更大的批次插入：

<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>

## 故障排除

**Missing endpoint。** 未设置 `CLICKHOUSE_ENDPOINT`，且未传入 `endpoint`。排空程序会记录一次 `[evlog/clickhouse] Missing endpoint`，之后不再执行任何操作，因此请求永远不会被阻塞或失败。

**ClickHouse API error: 401。** 检查 `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD`。凭据通过 `X-ClickHouse-User` / `X-ClickHouse-Key` 请求头发送，绝不会出现在查询字符串中，因此不会进入 `system.query_log`。

**提及未知列的 ClickHouse API error: 400。** 你的表结构与行数据形状不匹配。请根据上面的 DDL 创建表，或提供与列匹配的 `transform`。

**Cannot parse input。** 表已存在，但某一列的类型不匹配。`status` 是 `Nullable(UInt16)`，因为非 HTTP 事件中没有该字段。

## 直接使用 API

```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()` 和 `resolveClickHouseUrl()` 也已导出。

## 后续步骤

- [Sampling](https://evlog.zhcndoc.com/learn/sampling)：在数据到达 ClickHouse 之前控制数据量
- [Enrichers](https://evlog.zhcndoc.com/use-cases/enrichers)：为每个事件添加派生字段
- [Pipeline](https://evlog.zhcndoc.com/extend/drain-pipeline)：批处理、重试和扇出
- [Adapters Overview](https://evlog.zhcndoc.com/integrate/adapters/overview)：所有可用目标 亚洲欧美

---

- [HTTP 接口](https://clickhouse.com/docs/en/interfaces/http)
- [异步插入](https://clickhouse.com/docs/en/optimize/asynchronous-inserts)


## Sitemap

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