---
title: "Datadog Adapter"
description: "Send wide events to Datadog Logs via the native HTTP intake API. Supports all Datadog sites and DD_* environment variables."
canonical_url: "https://www.evlog.dev/integrate/adapters/cloud/datadog"
---
# Datadog Adapter

> Send wide events to Datadog Logs via the native HTTP intake API. Supports all Datadog sites and DD_* environment variables.

[Datadog](https://www.datadoghq.com) is a monitoring and security platform. The evlog Datadog adapter sends your wide events to [Datadog Logs](https://docs.datadoghq.com/logs/) using the **HTTP Logs intake API (v2)** with the `DD-API-KEY` header.

For OpenTelemetry-based ingestion instead, see the [OTLP adapter](https://www.evlog.dev/integrate/adapters/hybrid/otlp).

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

Add the Datadog drain adapter to send evlog wide events to Datadog Logs.

1. Identify which framework I'm using and follow its evlog integration pattern
2. Install evlog if not already installed
3. Import createDatadogDrain from 'evlog/datadog'
4. Wire createDatadogDrain() into my framework's drain configuration
5. Set DD_API_KEY (or DATADOG_API_KEY) and optionally DD_SITE in .env
6. Test by triggering a request and checking Log Explorer in Datadog

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

</prompt>

## Installation

The Datadog adapter comes bundled with evlog:

```typescript [src/index.ts]
import { createDatadogDrain } from 'evlog/datadog'
```

## Quick Start

### 1. Get your API key

1. Open [Datadog Organization Settings → API Keys](https://app.datadoghq.com/organization-settings/api-keys)
2. Create or copy an API key with permission to submit logs

### 2. Set environment variables

```bash [.env]
DD_API_KEY=your-api-key
# Optional — defaults to datadoghq.com (US1)
DD_SITE=datadoghq.eu
```

### 3. Wire the drain to your framework

<framework-tabs>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```typescript [Standalone]
import { createDatadogDrain } from 'evlog/datadog'

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

</framework-tabs>

Wide events appear in **Logs → Explorer**. The adapter sets `ddsource` to `evlog` and `message` to a JSON string of the full wide event for easy JSON parsing in pipelines.

## Configuration

The adapter reads configuration from multiple sources (highest priority first):

1. **Overrides** passed to `createDatadogDrain()`
2. **Runtime config** at `runtimeConfig.datadog` or `runtimeConfig.evlog.datadog` (Nuxt/Nitro)
3. **Environment variables**: see table below

### Environment Variables

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

<tbody>
  <tr>
    <td>
      <code>
        DD_API_KEY
      </code>
    </td>
    
    <td>
      Datadog API key (required). Also: <code>
        DATADOG_API_KEY
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        DD_SITE
      </code>
    </td>
    
    <td>
      Site hostname (e.g. <code>
        datadoghq.com
      </code>
      
      , <code>
        datadoghq.eu
      </code>
      
      , <code>
        us3.datadoghq.com
      </code>
      
      ). Also: <code>
        DATADOG_SITE
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        DATADOG_LOGS_URL
      </code>
    </td>
    
    <td>
      Full intake URL — overrides URL derived from <code>
        site
      </code>
    </td>
  </tr>
</tbody>
</table>

### Runtime Config (Nuxt only)

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  runtimeConfig: {
    datadog: {
      apiKey: '', // Set via DD_API_KEY or DATADOG_API_KEY
      site: 'datadoghq.eu',
    },
  },
})
```

### Override Options

<framework-tabs>

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

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }))
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }))
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

export const { handle, handleError } = createEvlogHooks({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }))
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createDatadogDrain({
        apiKey: '***',
        site: 'us5.datadoghq.com',
        timeout: 10000,
      }),
    }),
]
```

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

app.use(evlog({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
}))
```

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

app.use(evlog({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
}))
```

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

await app.register(evlog, {
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

app.use(evlog({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
}))
```

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

EvlogModule.forRoot({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

const handler = withEvlog(new RPCHandler(router), {
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

export default withEvlog(handler, {
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

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

initLogger({
  drain: createDatadogDrain({
    apiKey: '***',
    site: 'us5.datadoghq.com',
    timeout: 10000,
  }),
})
```

</framework-tabs>

### Full Configuration Reference

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

<tbody>
  <tr>
    <td>
      <code>
        apiKey
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Datadog API key (required)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        site
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        datadoghq.com
      </code>
    </td>
    
    <td>
      Site for intake host <code>
        http-intake.logs.${site}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        intakeUrl
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      from <code>
        site
      </code>
    </td>
    
    <td>
      Full <code>
        POST
      </code>
      
       URL for <code>
        /api/v2/logs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timeout
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        5000
      </code>
    </td>
    
    <td>
      Request timeout (ms)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        retries
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      Retries on transient failures
    </td>
  </tr>
</tbody>
</table>

## Requests

Each batch is sent as gzip-compressed JSON with `Content-Encoding: gzip`, as Datadog recommends. The Logs intake accepts at most 1000 entries per request, so a larger batch goes out as several requests of up to 1000, one after another. If one of them fails, the requests already accepted stay accepted, and a retry of the whole batch can send those entries twice.

The intake also rejects a request above 5 MB uncompressed with a `413`. The adapter splits by entry count only, so a batch of large events can still reach that limit: lower the [drain pipeline](https://www.evlog.dev/extend/drain-pipeline)'s `batch.size` (default 50) if your events are heavy.

## Log shape

Each wide event becomes one Datadog log with:

- **message**: short one-line summary for the list view (e.g. `ERROR GET /api/checkout (400)`), built with `formatDatadogMessageLine`. Easier to scan than a full JSON blob in Live Tail.
- **evlog**: full wide event as a **JSON object** (not a string). Numeric HTTP **status** fields anywhere in the tree are renamed to **httpStatusCode** so they never clash with Datadog’s reserved severity `status`.
- **dd**: `{ trace_id, span_id }` when the event carries trace context. See [Trace correlation](#trace-correlation).
- **service**, **status** (Datadog severity, drives Live Tail color), **ddsource**: `evlog`, **ddtags**: `env:…` and optional `version:…`
- **timestamp**: Unix milliseconds from `WideEvent.timestamp`

**Severity (status)** at intake root is computed by the adapter from the wide event’s **level** and HTTP **status** (`resolveDatadogLogStatus` in `evlog/datadog`). Business-only fields on **HTTP 200** stay **info** unless you call **log.error()**.

For advanced use, `sanitizeWideEventForDatadog(event)` returns only the sanitized object you would store under `evlog`.

## Trace correlation

Datadog links a log to a trace through the reserved **dd.trace_id** / **dd.span_id** attributes at the **root** of the payload. Nested copies (`@evlog.traceId`) are searchable but do not correlate on their own. That takes a [Trace Id Remapper](https://docs.datadoghq.com/logs/log_configuration/processors/trace_remapper/) in a Datadog log pipeline, configuration living outside your codebase. The adapter lifts `event.traceId` and `event.spanId` into a root `dd` block instead, so correlation works with no pipeline setup:

```json
{
  "message": "ERROR GET /api/checkout (400)",
  "evlog": { "traceId": "4bf92f35…", "spanId": "00f067aa…", "…": "full wide event" },
  "dd": { "trace_id": "4bf92f35…", "span_id": "00f067aa…" },
  "service": "my-app",
  "status": "error",
  "ddsource": "evlog"
}
```

The nested copy under `evlog` stays, so existing `@evlog.*` facets and dashboards keep working. Only **non-empty strings** are lifted, so an empty or non-string `traceId` / `spanId` is skipped rather than sent as an id Datadog would fail to resolve, and the `dd` key is absent when neither id survives that check.

Those fields are populated by `createTraceContextEnricher`, included in [`createDefaultEnrichers()`](https://www.evlog.dev/use-cases/enrichers), which parses the incoming W3C `traceparent` header into `event.traceId`. The span id in that header belongs to the caller, so it goes to `event.parentSpanId`, and `dd.span_id` is only set when something else provides `event.spanId`:

<framework-tabs>

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

const enrich = createDefaultEnrichers()

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

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

const enrich = createDefaultEnrichers()

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

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

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

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

const enrich = createDefaultEnrichers()
export const { handle, handleError } = createEvlogHooks({ enrich: enrich })
```

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

const enrich = createDefaultEnrichers()

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

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

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

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

const enrich = createDefaultEnrichers()
app.use(evlog({ enrich: enrich }))
```

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

const enrich = createDefaultEnrichers()
app.use(evlog({ enrich: enrich }))
```

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

const enrich = createDefaultEnrichers()
await app.register(evlog, { enrich: enrich })
```

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

const enrich = createDefaultEnrichers()
app.use(evlog({ enrich: enrich }))
```

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

const enrich = createDefaultEnrichers()
EvlogModule.forRoot({ enrich: enrich })
```

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

const enrich = createDefaultEnrichers()
const handler = withEvlog(new RPCHandler(router), { enrich: enrich })
```

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

const enrich = createDefaultEnrichers()
export default withEvlog(handler, { enrich: enrich })
```

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

const enrich = createDefaultEnrichers()
initLogger({ enrich: enrich })
```

</framework-tabs>

If your ids come from somewhere else (a tracer SDK, a vendor header), set `event.traceId` / `event.spanId` yourself and the adapter picks them up the same way:

```typescript
const span = tracer.scope().active()?.context()
log.set({ traceId: span?.toTraceId(), spanId: span?.toSpanId() })
```

`resolveDatadogTraceContext(event)` is exported from `evlog/datadog` if you need the same mapping in a custom drain.

## Querying in Datadog

- **Log Explorer**: `source:evlog`, `service:your-app`, `status:error`
- **Facets**: prefer `@evlog.path`, `@evlog.requestId`, `@evlog.level`, etc. Core fields are under **evlog**, not a JSON string in `message`
- **Metrics**: log-based metrics on `@evlog.*` attributes
- **Pipelines**: if you previously parsed a full JSON **string** inside `message`, move those facets to **@evlog.***. The `message` field is now a short summary line only.

## Simple logs vs wide events

Plain-text lines in Live Tail (e.g. “Form field is empty”) usually come from **log.info('tag', 'msg')** or similar, not from the **wide event** sent on **emit()**. Those lines go to the console (and any Agent-based log stream), while the Datadog drain sends one structured log per wide event under **source:evlog**.

## Troubleshooting

### Missing API key

```text [Console]
[evlog/datadog] Missing API key. Set DATADOG_API_KEY, DD_API_KEY...
```

Set `DD_API_KEY` (or unprefixed `DATADOG_API_KEY`) and restart the process.

### 403 Forbidden

The API key may lack log ingestion permission or belong to the wrong organization. Verify the key in Datadog and try a new key.

### Wrong region / site

If logs never appear, confirm `DD_SITE` matches your Datadog account (e.g. EU: `datadoghq.eu`). For a custom intake URL, set `DATADOG_LOGS_URL`.

## Direct API usage

```typescript [server/utils/datadog.ts]
import { sendToDatadog, sendBatchToDatadog } from 'evlog/datadog'

await sendToDatadog(event, {
  apiKey: process.env.DD_API_KEY!,
  site: process.env.DD_SITE,
})

await sendBatchToDatadog(events, {
  apiKey: process.env.DD_API_KEY!,
})
```

## Next Steps

- [OTLP Adapter](https://www.evlog.dev/integrate/adapters/hybrid/otlp): Send logs via OpenTelemetry (works with Datadog Agent / OTLP endpoint)
- [Custom Adapters](https://www.evlog.dev/extend/custom-drains): Build your own destination

---

- [Datadog Logs](https://docs.datadoghq.com/logs/)
- [OTLP Adapter](https://www.evlog.dev/integrate/adapters/hybrid/otlp)


## Sitemap

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