---
title: "Adapters Overview"
description: "Send your logs to external services with evlog adapters. Built-in support for popular observability platforms and custom destinations."
canonical_url: "https://www.evlog.dev/integrate/adapters/overview"
---
# Adapters Overview

> Send your logs to external services with evlog adapters. Built-in support for popular observability platforms and custom destinations.

Adapters let you send logs to external observability platforms. evlog provides built-in adapters for popular services, and you can create custom adapters for any destination.

<drain-fan-out>



</drain-fan-out>

## Send events without naming a destination in your code

Adapters receive a `DrainContext` after each request completes and send the data to an external service. The drain runs in **fire-and-forget** mode, meaning it never blocks the HTTP response.

How you wire an adapter depends on your framework:

<framework-tabs>

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

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

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

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

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

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

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

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

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

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

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

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

```typescript [Hono]src/index.ts
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({ drain: createAxiomDrain() }))
```

```typescript [Express]src/index.ts
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({ drain: createAxiomDrain() }))
```

```typescript [Fastify]src/index.ts
import { createAxiomDrain } from 'evlog/axiom'

await app.register(evlog, { drain: createAxiomDrain() })
```

```typescript [Elysia]src/index.ts
import { createAxiomDrain } from 'evlog/axiom'

app.use(evlog({ drain: createAxiomDrain() }))
```

```typescript [NestJS]src/app.module.ts
import { createAxiomDrain } from 'evlog/axiom'

EvlogModule.forRoot({ drain: createAxiomDrain() })
```

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

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

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

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

```typescript [Standalone]
import { createAxiomDrain } from 'evlog/axiom'

initLogger({ drain: createAxiomDrain() })
```

</framework-tabs>

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

**Serverless Support:** On Cloudflare Workers and Vercel Edge, evlog automatically uses `waitUntil()` to ensure drains complete before the runtime terminates. No additional configuration needed.

</callout>

## Pick a destination

<card-group>
<card icon="i-custom-axiom" title="Axiom" to="https://www.evlog.dev/integrate/adapters/cloud/axiom">

Send logs to Axiom, which indexes every field so you can query on any of them.

</card>

<card icon="i-simple-icons-posthog" title="PostHog" to="https://www.evlog.dev/integrate/adapters/cloud/posthog">

Send logs to PostHog Logs for structured logging and observability.

</card>

<card icon="i-simple-icons-sentry" title="Sentry" to="https://www.evlog.dev/integrate/adapters/cloud/sentry">

Send structured logs to Sentry Logs for high-cardinality querying.

</card>

<card icon="i-simple-icons-betterstack" title="Better Stack" to="https://www.evlog.dev/integrate/adapters/cloud/better-stack">

Send logs to Better Stack for log management and alerting.

</card>

<card icon="i-simple-icons-datadog" title="Datadog" to="https://www.evlog.dev/integrate/adapters/cloud/datadog">

Send logs to Datadog Logs via the native HTTP intake API.

</card>

<card icon="i-custom-sazabi" title="Sazabi" to="https://www.evlog.dev/integrate/adapters/cloud/sazabi">

Send logs to a Sazabi log stream over OTLP/HTTP, configured with the stream's intake URL.

</card>

<card icon="i-simple-icons-grafana" title="Grafana Loki" to="https://www.evlog.dev/integrate/adapters/hybrid/loki">

Push logs to self-hosted, multi-tenant, or Grafana Cloud Loki.

</card>

<card icon="i-simple-icons-clickhouse" title="ClickHouse" to="https://www.evlog.dev/integrate/adapters/hybrid/clickhouse">

Insert logs into ClickHouse for fast aggregation over huge volumes.

</card>

<card icon="i-simple-icons-opentelemetry" title="OTLP" to="https://www.evlog.dev/integrate/adapters/hybrid/otlp">

OpenTelemetry Protocol for Grafana, Datadog, Honeycomb, and more.

</card>

<card icon="i-custom-hyperdx" title="HyperDX" to="https://www.evlog.dev/integrate/adapters/hybrid/hyperdx">

Send logs to HyperDX via OTLP/HTTP using their documented ingest endpoint and API key.

</card>

<card icon="i-lucide-hard-drive" title="File System" to="https://www.evlog.dev/integrate/adapters/self-hosted/fs">

Write logs to local NDJSON files for debugging and AI agent integration.

</card>

<card icon="i-simple-icons-nuxt" title="NuxtHub" to="https://www.evlog.dev/integrate/adapters/self-hosted/nuxthub">

Self-hosted log storage in your NuxtHub database with automatic retention.

</card>

<card icon="i-lucide-cpu" title="Memory" to="https://www.evlog.dev/integrate/adapters/self-hosted/memory">

In-memory ring buffer that works in any runtime, including Cloudflare Workers.

</card>

<card icon="i-lucide-code" title="Custom" to="https://www.evlog.dev/extend/custom-drains">

Build your own adapter for any destination.

</card>

<card icon="i-lucide-globe" title="HTTP" to="https://www.evlog.dev/extend/drain-pipeline">

Send client logs to your server over HTTP without framework coupling.

</card>

<card icon="i-lucide-workflow" title="Pipeline" to="https://www.evlog.dev/extend/drain-pipeline">

Batch events, retry on failure, and handle buffer overflow.

</card>
</card-group>

## Call a drain outside a request

In plain TypeScript or Bun scripts (no HTTP framework), use the `drain` option in `initLogger`. Every emitted event is drained automatically.

```typescript [index.ts]
import type { DrainContext } from 'evlog'
import { initLogger, log, createRequestLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>()
const drain = pipeline(createAxiomDrain())

initLogger({
  env: { service: 'my-script' },
  drain,
})

log.info({ action: 'job_started' }) // drained automatically

const reqLog = createRequestLogger({ method: 'POST', path: '/process' })
reqLog.set({ processed: 42 })
reqLog.emit() // drained automatically

await drain.flush()
```

<callout color="neutral" icon="i-lucide-arrow-right">

See the full [bun-script example](https://github.com/evloghq/evlog/tree/main/examples/bun-script) for a realistic batch processing script.

</callout>

## Send to several places at once

Send logs to multiple services simultaneously by composing drains:

```typescript [src/index.ts]
import { createAxiomDrain } from 'evlog/axiom'
import { createOTLPDrain } from 'evlog/otlp'

const axiom = createAxiomDrain()
const otlp = createOTLPDrain()

const drain = async (ctx) => {
  await Promise.allSettled([axiom(ctx), otlp(ctx)])
}
```

Then pass `drain` to your framework:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
```

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

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

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

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

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

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

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

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

```typescript [Hono]src/index.ts
app.use(evlog({ drain }))
```

```typescript [Express]src/index.ts
app.use(evlog({ drain }))
```

```typescript [Fastify]src/index.ts
await app.register(evlog, { drain })
```

```typescript [Elysia]src/index.ts
app.use(evlog({ drain }))
```

```typescript [NestJS]src/app.module.ts
EvlogModule.forRoot({ drain })
```

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

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

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

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

```typescript [Standalone]
initLogger({ drain })
```

</framework-tabs>

## What a drain receives

Every adapter receives a `DrainContext` with:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        event
      </code>
    </td>
    
    <td>
      <code>
        WideEvent
      </code>
    </td>
    
    <td>
      The complete log event with all accumulated context
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        request
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      Request metadata (<code>
        method
      </code>
      
      , <code>
        path
      </code>
      
      , <code>
        requestId
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        headers
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      Safe HTTP headers (sensitive headers are filtered)
    </td>
  </tr>
</tbody>
</table>

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

**Security:** Sensitive headers (`authorization`, `cookie`, `x-api-key`, etc.) are automatically filtered and never passed to adapters.

</callout>

## Configure it from the environment

All adapters support automatic configuration via environment variables. No code changes needed when deploying to different environments.

Each adapter reads from standard environment variables, and the same names work in every framework:

```bash [.env]
# Axiom
AXIOM_API_KEY=xaat-xxx
AXIOM_DATASET=my-logs

# OTLP
OTLP_ENDPOINT=https://otlp.example.com

# HyperDX
HYPERDX_API_KEY=<YOUR_HYPERDX_API_KEY_HERE>

# PostHog
POSTHOG_API_KEY=phc_xxx

# Sentry
SENTRY_DSN=https://key@o0.ingest.sentry.io/123

# Better Stack
BETTER_STACK_API_KEY=your-source-token

# Datadog
DD_API_KEY=your-api-key
DD_SITE=datadoghq.eu

# Sazabi
SAZABI_INTAKE_URL=https://<key>.us-west-2.intake.sazabi.com

# Grafana Loki
LOKI_ENDPOINT=http://localhost:3100
# Grafana Cloud only:
LOKI_USER=123456
LOKI_API_KEY=glc_xxx

# ClickHouse
CLICKHOUSE_ENDPOINT=http://localhost:8123
CLICKHOUSE_PASSWORD=your-password
```

Adapters auto-read from these variables, so a `create*Drain()` factory takes no arguments.

## Missing credentials

Behavior depends on which API you call:

<table>
<thead>
  <tr>
    <th>
      API
    </th>
    
    <th>
      Missing credentials
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        create*Drain()
      </code>
      
       (factory)
    </td>
    
    <td>
      <code>
        console.error
      </code>
      
       + drain becomes a no-op — the HTTP response is never blocked
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sendTo*
      </code>
      
       / <code>
        sendBatchTo*
      </code>
      
       (direct)
    </td>
    
    <td>
      Throws if required credentials are absent — caller must pass a complete config
    </td>
  </tr>
</tbody>
</table>

This split is intentional: drains run fire-and-forget after each request; direct send helpers are for scripts and tests where a silent failure would hide misconfiguration.

---

- [Axiom](https://www.evlog.dev/integrate/adapters/cloud/axiom)
- [PostHog](https://www.evlog.dev/integrate/adapters/cloud/posthog)
- [Sentry](https://www.evlog.dev/integrate/adapters/cloud/sentry)
- [Better Stack](https://www.evlog.dev/integrate/adapters/cloud/better-stack)
- [Datadog](https://www.evlog.dev/integrate/adapters/cloud/datadog)
- [Sazabi](https://www.evlog.dev/integrate/adapters/cloud/sazabi)
- [Grafana Loki](https://www.evlog.dev/integrate/adapters/hybrid/loki)
- [ClickHouse](https://www.evlog.dev/integrate/adapters/hybrid/clickhouse)
- [OTLP](https://www.evlog.dev/integrate/adapters/hybrid/otlp)
- [HyperDX](https://www.evlog.dev/integrate/adapters/hybrid/hyperdx)
- [File System](https://www.evlog.dev/integrate/adapters/self-hosted/fs)
- [NuxtHub](https://www.evlog.dev/integrate/adapters/self-hosted/nuxthub)
- [Memory](https://www.evlog.dev/integrate/adapters/self-hosted/memory)


## Sitemap

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