---
title: "Grafana Loki 适配器"
description: "将宽事件推送到 Grafana Loki——自托管、多租户或 Grafana Cloud——使用低基数标签并支持完整的 JSON 查询。"
canonical_url: "https://evlog.zhcndoc.com/integrate/adapters/hybrid/loki"
---
# Grafana Loki 适配器

> 将宽事件推送到 Grafana Loki——自托管、多租户或 Grafana Cloud——使用低基数标签并支持完整的 JSON 查询。

Loki 适配器通过其推送 API 将宽事件推送到 [Grafana Loki](https://grafana.com/docs/loki/latest/)。它适用于自托管的单租户实例、多租户部署以及 Grafana Cloud。

每个事件都会作为**JSON 日志行**推送，并归属于一组小型、低基数的标签。在 Loki 中，这一区别非常重要：标签会按基数建立索引并计费，而日志行则在查询时进行搜索。evlog 默认仅为 `service`、`environment` 和 `level` 添加标签，并将其他所有内容（`requestId`、`path`、`user` 以及你的自定义字段）保留在日志行中，可通过 `| json` 进行查询。

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

添加 Grafana Loki drain 适配器，将 evlog 宽事件发送到 Loki。

1. 识别我使用的框架，并遵循该框架的 evlog 集成模式
2. 如果尚未安装 evlog，则安装它
3. 从 'evlog/loki' 导入 createLokiDrain
4. 将 createLokiDrain() 接入我所用框架的 drain 配置
5. 设置 LOKI_ENDPOINT（例如 [http://localhost:3100）。对于](undefined) Grafana Cloud，还需设置 LOKI_USER（实例 ID）和 LOKI_API_KEY
6. 对于多租户自托管 Loki，则设置 LOKI_TENANT_ID
7. 通过触发请求并在 Grafana 中查询 {service="my-app"} 进行测试

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

</prompt>

## 安装

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

```typescript [src/index.ts]
import { createLokiDrain } from 'evlog/loki'
```

## 快速开始

设置 `LOKI_ENDPOINT`，并将日志排出器接入你的框架。

<framework-tabs>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```typescript [Standalone]
import { initLogger } from 'evlog'
import { createLokiDrain } from 'evlog/loki'

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

</framework-tabs>

## 配置

### 环境变量

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

<tbody>
  <tr>
    <td>
      <code>
        LOKI_ENDPOINT
      </code>
    </td>
    
    <td>
      是
    </td>
    
    <td>
      Loki 实例的基础 URL，不包含推送路径（也接受 <code>
        LOKI_URL
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_API_KEY
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      API 令牌。设置了 <code>
        LOKI_USER
      </code>
      
       时以 Basic 方式发送，否则以 Bearer 方式发送（也接受 <code>
        GRAFANA_API_KEY
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_USER
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      Grafana Cloud 实例 ID。将身份验证切换为 Basic（也接受 <code>
        GRAFANA_USER
      </code>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_TENANT_ID
      </code>
    </td>
    
    <td>
      否
    </td>
    
    <td>
      多租户自托管 Loki 的租户，以 <code>
        X-Scope-OrgID
      </code>
      
       发送
    </td>
  </tr>
</tbody>
</table>

### 优先级

配置按从高到低的优先级解析：

1. 传递给 `createLokiDrain()` 的覆盖配置
2. `runtimeConfig.evlog.loki`（Nitro）
3. `runtimeConfig.loki`（Nitro）
4. 环境变量

### 选项

<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>
      基础 URL，不包含 <code>
        /loki/api/v1/push
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        apiKey
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      API 令牌
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        user
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Grafana Cloud 实例 ID（启用 Basic 身份验证）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tenantId
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      多租户 Loki 使用的 <code>
        X-Scope-OrgID
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        labelFields
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      <code>
        ['service', 'environment', 'level']
      </code>
    </td>
    
    <td>
      提升为 Loki 标签的事件字段
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        labels
      </code>
    </td>
    
    <td>
      <code>
        Record<string, string>
      </code>
    </td>
    
    <td>
      —
    </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>

## 部署

Loki 以这两种方式中的任一种运行，适配器在两种情况下都相同。区别仅在于身份验证。

### 自托管

单租户实例只需要端点：

<framework-tabs>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

EvlogModule.forRoot({ drain: createLokiDrain({ endpoint: 'http://localhost:3100' }) })
```

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

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

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

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

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

initLogger({ drain: createLokiDrain({ endpoint: 'http://localhost:3100' }) })
```

</framework-tabs>

```bash [.env]
LOKI_ENDPOINT=http://localhost:3100
```

对于**多租户**部署，请指定租户名称，该名称会作为 `X-Scope-OrgID` 发送：

<framework-tabs>

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

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }))
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }))
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

export const { handle, handleError } = createEvlogHooks({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }))
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createLokiDrain({
        endpoint: 'http://loki.internal:3100',
        tenantId: 'team-checkout',
      }),
    }),
]
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
}))
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
}))
```

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

await app.register(evlog, {
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
}))
```

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

EvlogModule.forRoot({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

const handler = withEvlog(new RPCHandler(router), {
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

export default withEvlog(handler, {
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

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

initLogger({
  drain: createLokiDrain({
    endpoint: 'http://loki.internal:3100',
    tenantId: 'team-checkout',
  }),
})
```

</framework-tabs>

```bash [.env]
LOKI_ENDPOINT=http://loki.internal:3100
LOKI_TENANT_ID=team-checkout
```

如果你的实例位于需要身份验证的代理之后，则仅发送 `apiKey`，格式为
`Authorization: Bearer`。

### Grafana Cloud

Grafana Cloud 使用你的**实例 ID**和访问策略令牌进行身份验证，并将二者一起作为 HTTP Basic 发送：

<framework-tabs>

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

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }))
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }))
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

export const { handle, handleError } = createEvlogHooks({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }))
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createLokiDrain({
        endpoint: 'https://logs-prod-eu-west-0.grafana.net',
        user: '123456',
        apiKey: process.env.GRAFANA_API_KEY,
      }),
    }),
]
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
}))
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
}))
```

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

await app.register(evlog, {
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

app.use(evlog({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
}))
```

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

EvlogModule.forRoot({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

const handler = withEvlog(new RPCHandler(router), {
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

export default withEvlog(handler, {
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

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

initLogger({
  drain: createLokiDrain({
    endpoint: 'https://logs-prod-eu-west-0.grafana.net',
    user: '123456',
    apiKey: process.env.GRAFANA_API_KEY,
  }),
})
```

</framework-tabs>

```bash [.env]
LOKI_ENDPOINT=https://logs-prod-eu-west-0.grafana.net
LOKI_USER=123456
LOKI_API_KEY=glc_xxx
```

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

`user` 是 Loki 数据源的**数字实例 ID**。请在 Grafana Cloud 堆栈中的
*Connections → Data sources → Loki* 下查找它。它不是你的账户邮箱。使用错误的值通常会导致 `401`

</callout>

### 应用哪种身份验证

<table>
<thead>
  <tr>
    <th>
      <code>
        user
      </code>
    </th>
    
    <th>
      <code>
        apiKey
      </code>
    </th>
    
    <th>
      <code>
        tenantId
      </code>
    </th>
    
    <th>
      发送的请求头
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      ✓
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        Authorization: Basic base64(user:apiKey)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        Authorization: Bearer <apiKey>
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      
    </td>
    
    <td>
      
    </td>
    
    <td>
      ✓
    </td>
    
    <td>
      <code>
        X-Scope-OrgID: <tenantId>
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      
    </td>
    
    <td>
      
    </td>
    
    <td>
      
    </td>
    
    <td>
      无——未进行身份验证的实例
    </td>
  </tr>
</tbody>
</table>

`tenantId` 独立存在，可以与任一身份验证模式结合使用。

## 标签与基数

<callout color="warning" icon="i-lucide-triangle-alert">

绝不要将高基数字段提升为标签。Loki 会为每种唯一的标签组合创建一个流，因此为 `requestId` 或 `userId` 添加标签会创建数百万个流，并导致你的实例性能下降或无法运行

</callout>

仅为你会进行筛选且取值集合有界的字段添加标签：

<framework-tabs>

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

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }))
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }))
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

export const { handle, handleError } = createEvlogHooks({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }))
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createLokiDrain({
        // `region` has a handful of values — safe
        labelFields: ['service', 'environment', 'level', 'region'],
        // Static labels for everything in this deployment
        labels: { cluster: 'prod-eu' },
      }),
    }),
]
```

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

app.use(evlog({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
}))
```

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

app.use(evlog({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
}))
```

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

await app.register(evlog, {
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

app.use(evlog({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
}))
```

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

EvlogModule.forRoot({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

const handler = withEvlog(new RPCHandler(router), {
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

export default withEvlog(handler, {
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

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

initLogger({
  drain: createLokiDrain({
    // `region` has a handful of values — safe
    labelFields: ['service', 'environment', 'level', 'region'],
    // Static labels for everything in this deployment
    labels: { cluster: 'prod-eu' },
  }),
})
```

</framework-tabs>

其他所有内容都会保留在 JSON 日志行中，并且完全可查询。请参见下文。

## Grafana 中的查询

按标签筛选，然后使用 `| json` 深入查看宽事件：

```logql
{service="checkout", environment="production"}
  | json
  | status >= 500
```

```logql
# 某个路由上的慢请求
{service="checkout"} | json | path="/api/orders" | durationMs > 1000
```

```logql
# 端到端的单个请求
{service="checkout"} | json | requestId="4a8ff3a8-..."
```

```logql
# 每个服务的错误率
sum by (service) (rate({environment="production", level="error"}[5m]))
```

## 批处理

将 drain 与管道结合使用，以批量推送，而不是逐个请求推送：

<framework-tabs>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</framework-tabs>

共享同一标签集的事件会被分组到单个 Loki 流中，并按时间戳排序，因为 Loki 会拒绝流内时间顺序不正确的推送。

## 在本地验证

在 Docker 中启动 Loki 并推送真实事件，无需云账户：

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

该测试套件是一次**往返测试**：它推送事件，通过 Loki 的范围 API 将其查询回来，并断言标签集、JSON 日志行和时间戳均得以保留。如果未设置 `LOKI_ENDPOINT`，它会显示一个明确的标签并跳过自身，而不是静默通过。

如果想直接查看结果，可以将任意 Grafana 指向 `http://localhost:3100`，然后运行 `{service="evlog-e2e"}`。

## 故障排查

**Missing endpoint。** 未设置 `LOKI_ENDPOINT`，且没有传递 `endpoint`。drain 会记录一次 `[evlog/loki] Missing endpoint`，随后不再执行任何操作，因此请求永远不会被阻塞或失败。

**Loki API error: 401。** 在 Grafana Cloud 中，请检查 `user` 是否为 Loki 数据源的数字**实例 ID**，而不是你的账户邮箱。

**提及时间顺序错误条目的 Loki API error: 400。** 较旧版本的 Loki 会拒绝早于流中最近条目的条目。evlog 会在每次推送内进行排序；如果仍然遇到此问题，请在 Loki 中启用 `unordered_writes`，或缩短批处理间隔。

**Grafana 中没有显示任何内容。** 确认你查询的标签集。先运行 `{service=~".+"}`，查看哪些流已到达。

## 直接使用 API

绕过 drain，自行推送事件：

```typescript [scripts/backfill.ts]
import { sendBatchToLoki, sendToLoki } from 'evlog/loki'

await sendToLoki(event, { endpoint: 'http://localhost:3100' })
await sendBatchToLoki(events, { endpoint: 'http://localhost:3100' })
```

`buildLokiPayload()`、`toLokiLabels()` 和 `resolveLokiPushUrl()` 也已导出，可用于自定义传输。

## 后续步骤

- [采样](https://evlog.zhcndoc.com/learn/sampling)：在日志到达 Loki 之前控制日志量
- [Enrichers](https://evlog.zhcndoc.com/use-cases/enrichers)：为每个事件添加派生字段
- [Drain 管道](https://evlog.zhcndoc.com/extend/drain-pipeline)：批处理、重试和扇出
- [适配器概览](https://evlog.zhcndoc.com/integrate/adapters/overview)：所有可用的目标站点

---

- [Loki HTTP API](https://grafana.com/docs/loki/latest/reference/loki-http-api/)
- [LogQL](https://grafana.com/docs/loki/latest/query/)


## Sitemap

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