---
title: "Grafana Loki Adapter"
description: "Push wide events to Grafana Loki — self-hosted, multi-tenant, or Grafana Cloud — with low-cardinality labels and full JSON querying."
canonical_url: "https://www.evlog.dev/integrate/adapters/hybrid/loki"
---
# Grafana Loki Adapter

> Push wide events to Grafana Loki — self-hosted, multi-tenant, or Grafana Cloud — with low-cardinality labels and full JSON querying.

The Loki adapter pushes wide events to [Grafana Loki](https://grafana.com/docs/loki/latest/) via its push API. It works with a self-hosted single-tenant instance, a multi-tenant deployment, and Grafana Cloud.

Each event is pushed as a **JSON log line** under a small, low-cardinality label set. That distinction matters in Loki: labels are indexed and billed by cardinality, while the log line is searched at query time. evlog labels only `service`, `environment`, and `level` by default, and leaves everything else (`requestId`, `path`, `user`, your custom fields) in the line, queryable with `| json`.

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

Add the Grafana Loki drain adapter to send evlog wide events to Loki.

1. Identify which framework I'm using and follow its evlog integration pattern
2. Install evlog if not already installed
3. Import createLokiDrain from 'evlog/loki'
4. Wire createLokiDrain() into my framework's drain configuration
5. Set LOKI_ENDPOINT (e.g. [http://localhost:3100](http://localhost:3100)). For Grafana Cloud also set LOKI_USER (instance ID) and LOKI_API_KEY
6. For multi-tenant self-hosted Loki, set LOKI_TENANT_ID instead
7. Test by triggering a request and querying {service="my-app"} in Grafana

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

</prompt>

## Installation

The Loki adapter comes bundled with evlog:

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

## Quick Start

Set `LOKI_ENDPOINT` and wire the drain into your framework.

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

## Configuration

### Environment variables

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

<tbody>
  <tr>
    <td>
      <code>
        LOKI_ENDPOINT
      </code>
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      Base URL of the Loki instance, without the push path (<code>
        LOKI_URL
      </code>
      
       also accepted)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_API_KEY
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      API token. Sent as Basic with <code>
        LOKI_USER
      </code>
      
      , otherwise as Bearer (<code>
        GRAFANA_API_KEY
      </code>
      
       also accepted)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_USER
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Grafana Cloud instance ID. Switches auth to Basic (<code>
        GRAFANA_USER
      </code>
      
       also accepted)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LOKI_TENANT_ID
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Tenant for multi-tenant self-hosted Loki, sent as <code>
        X-Scope-OrgID
      </code>
    </td>
  </tr>
</tbody>
</table>

### Priority

Configuration is resolved highest to lowest:

1. Overrides passed to `createLokiDrain()`
2. `runtimeConfig.evlog.loki` (Nitro)
3. `runtimeConfig.loki` (Nitro)
4. Environment variables

### 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>
      Base URL, without <code>
        /loki/api/v1/push
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        apiKey
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      API token
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        user
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Grafana Cloud instance ID (enables Basic auth)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tenantId
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      <code>
        X-Scope-OrgID
      </code>
      
       for multi-tenant Loki
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        labelFields
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      <code>
        ['service', 'environment', 'level']
      </code>
    </td>
    
    <td>
      Event fields promoted to Loki labels
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        labels
      </code>
    </td>
    
    <td>
      <code>
        Record<string, string>
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Static labels merged into every stream
    </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 on transient failures
    </td>
  </tr>
</tbody>
</table>

## Deployment

Loki runs either way, and the adapter is the same in both cases. Only
authentication differs.

### Self-hosted

A single-tenant instance needs nothing but the endpoint:

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

For a **multi-tenant** deployment, name the tenant, and it is sent as `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
```

If your instance sits behind an authenticating proxy, `apiKey` alone is sent as
`Authorization: Bearer`.

### Grafana Cloud

Grafana Cloud authenticates with your **instance ID** plus an access policy
token, sent together as 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` is the **numeric instance ID** of the Loki datasource. Find it under
*Connections → Data sources → Loki* in your Grafana Cloud stack. It is not your
account email. Using the wrong value is the usual cause of a `401`.

</callout>

### Which auth applies

<table>
<thead>
  <tr>
    <th>
      <code>
        user
      </code>
    </th>
    
    <th>
      <code>
        apiKey
      </code>
    </th>
    
    <th>
      <code>
        tenantId
      </code>
    </th>
    
    <th>
      Header sent
    </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>
      none — unauthenticated instance
    </td>
  </tr>
</tbody>
</table>

`tenantId` is independent and can be combined with either auth mode.

## Labels and cardinality

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

Never promote a high-cardinality field to a label. Loki creates one stream per unique label combination, so labelling `requestId` or `userId` will create millions of streams and degrade or break your instance.

</callout>

Add a label only for values you filter on and that have a bounded set:

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

Everything else stays in the JSON line and is fully queryable. See below.

## Querying in Grafana

Filter by label, then reach into the wide event with `| json`:

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

```logql
# Slow requests on one route
{service="checkout"} | json | path="/api/orders" | durationMs > 1000
```

```logql
# One request end to end
{service="checkout"} | json | requestId="4a8ff3a8-..."
```

```logql
# Error rate per service
sum by (service) (rate({environment="production", level="error"}[5m]))
```

## Batching

Pair the drain with a pipeline to push in batches rather than per request:

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

Events sharing a label set are grouped into a single Loki stream, and entries are sorted by timestamp, because Loki rejects out-of-order pushes within a stream.

## Verify it locally

Spin up Loki in Docker and push a real event, with no cloud account needed:

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

The suite is a **round-trip**: it pushes events, queries them back through
Loki's range API, and asserts the label set, the JSON log line and the
timestamp survived. Without `LOKI_ENDPOINT` it skips itself with a visible
label rather than passing silently.

To eyeball the result instead, point any Grafana at `http://localhost:3100`
and run `{service="evlog-e2e"}`.

## Troubleshooting

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

**Loki API error: 401.** On Grafana Cloud, check that `user` is the numeric **instance ID** of the Loki datasource, not your account email.

**Loki API error: 400 mentioning out-of-order entries.** Older Loki versions reject entries older than the most recent one in a stream. evlog sorts within each push; if you still hit this, enable `unordered_writes` in Loki or reduce the batch interval.

**Nothing appears in Grafana.** Confirm the label set you are querying. Run `{service=~".+"}` first to see which streams arrived.

## Direct API usage

Bypass the drain to push events yourself:

```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()`, and `resolveLokiPushUrl()` are also exported for custom transports.

## Next steps

- [Sampling](https://www.evlog.dev/learn/sampling): control log volume before it reaches Loki
- [Enrichers](https://www.evlog.dev/use-cases/enrichers): add derived fields to every event
- [Drain 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

---

- [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://www.evlog.dev/sitemap.md) for all pages.
