Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/create-adapter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ The exact wording may vary depending on the adapter (e.g., `feat: add OTLP adapt
| 2 | `packages/evlog/tsdown.config.ts` | Add build entry |
| 3 | `packages/evlog/package.json` | Add `exports` + `typesVersions` entries |
| 4 | `packages/evlog/test/adapters/{name}.test.ts` | Create tests |
| 5 | `apps/docs/content/4.adapters/{n}.{name}.md` | Create adapter doc page (before `custom.md`) |
| 6 | `apps/docs/content/4.adapters/1.overview.md` | Add adapter to overview (links, card, env vars) |
| 5 | `apps/docs/content/5.adapters/{n}.{name}.md` | Create adapter doc page (before `custom.md`) |
| 6 | `apps/docs/content/5.adapters/1.overview.md` | Add adapter to overview (links, card, env vars) |
| 7 | `skills/review-logging-patterns/SKILL.md` | Add adapter row in the Drain Adapters table |
| 8 | Renumber `custom.md` | Ensure `custom.md` stays last after the new adapter |

Expand Down Expand Up @@ -71,7 +71,7 @@ Add a build entry in `packages/evlog/tsdown.config.ts` alongside the existing ad
'adapters/{name}': 'src/adapters/{name}.ts',
```

Place it after the last adapter entry (currently `hyperdx` in `tsdown.config.ts`).
Place it after the last adapter entry in `tsdown.config.ts` (follow existing ordering in that file).

## Step 3: Package Exports

Expand Down Expand Up @@ -113,7 +113,7 @@ Required test categories:

Create `apps/docs/content/4.adapters/{n}.{name}.md` where `{n}` is the next number before `custom.md` (custom should always be last).

Use the existing Axiom adapter page (`apps/docs/content/4.adapters/2.axiom.md`) as a reference for frontmatter structure, tone, and sections. Key sections: intro, quick setup, configuration (env vars table + priority), advanced usage, querying in the target service, troubleshooting, direct API usage, next steps.
Use the existing Axiom adapter page (`apps/docs/content/5.adapters/2.axiom.md`) as a reference for frontmatter structure, tone, and sections. Key sections: intro, quick setup, configuration (env vars table + priority), advanced usage, querying in the target service, troubleshooting, direct API usage, next steps.

**Important: multi-framework examples.** The Quick Start section must include a `::code-group` with tabs for all supported frameworks (Nuxt/Nitro, Hono, Express, Fastify, Elysia, NestJS, Standalone). Do not only show Nitro examples. See any existing adapter page for the pattern.

Expand Down
5 changes: 5 additions & 0 deletions .changeset/evl-144-datadog-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"evlog": minor
---

Add Datadog Logs HTTP drain adapter (`evlog/datadog`): `createDatadogDrain()`, `sendToDatadog` / `sendBatchToDatadog`, env vars `DD_API_KEY` / `NUXT_DATADOG_*` / `DD_SITE`, and intake URL for all Datadog sites. Maps wide events with a short `message` line, full payload under `evlog`, severity `status`, and recursive `httpStatusCode` renaming so HTTP `status` fields never clash with Datadog’s reserved severity ([EVL-144](https://linear.app/evlog/issue/EVL-144)).
16 changes: 15 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ evlog/
│ │ ├── vite/ # Vite plugin (evlog/vite)
│ │ ├── shared/ # Toolkit: building blocks for custom framework integrations (evlog/toolkit)
│ │ ├── ai/ # AI SDK integration (evlog/ai)
│ │ ├── adapters/ # Log drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack)
│ │ ├── adapters/ # Log drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack, Datadog)
│ │ ├── enrichers/ # Built-in enrichers (UserAgent, Geo, RequestSize, TraceContext)
│ │ └── runtime/ # Runtime code (client/, server/, utils/)
│ └── test/ # Tests
Expand Down Expand Up @@ -325,6 +325,7 @@ evlog provides built-in adapters for popular observability platforms. Use the `e
| PostHog | `evlog/posthog` | Send logs to PostHog Logs via OTLP for structured logging and observability |
| Sentry | `evlog/sentry` | Send logs to Sentry Logs for structured logging and debugging |
| Better Stack | `evlog/better-stack` | Send logs to Better Stack for log management and alerting |
| Datadog | `evlog/datadog` | Send logs to Datadog Logs via the HTTP intake API (`DD-API-KEY`) |

**Using Axiom Adapter:**

Expand Down Expand Up @@ -404,6 +405,19 @@ export default defineNitroPlugin((nitroApp) => {

Set environment variable: `NUXT_BETTER_STACK_SOURCE_TOKEN`.

**Using Datadog Adapter:**

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

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

Set environment variables: `NUXT_DATADOG_API_KEY` or `DD_API_KEY`, and optionally `DD_SITE` (e.g. `datadoghq.eu`).

**Multiple Destinations:**

```typescript
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/app/components/app/AppHeaderCenter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ const landingItems = [
icon: 'i-simple-icons-betterstack',
description: 'Stream logs to Better Stack',
to: '/adapters/better-stack'
},
{
label: 'Datadog',
icon: 'i-simple-icons-datadog',
description: 'Send logs to Datadog',
to: '/adapters/datadog'
}
]
},
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/content/5.adapters/1.overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ links:
to: /adapters/better-stack
color: neutral
variant: subtle
- label: Datadog
icon: i-simple-icons-datadog
to: /adapters/datadog
color: neutral
variant: subtle
- label: File System
icon: i-lucide-hard-drive
to: /adapters/fs
Expand Down Expand Up @@ -152,6 +157,15 @@ initLogger({ drain: createAxiomDrain() })
Send logs to Better Stack for log management and alerting.
:::

:::card
---
icon: i-simple-icons-datadog
title: Datadog
to: /adapters/datadog
---
Send logs to Datadog Logs via the native HTTP intake API.
:::

:::card
---
icon: i-lucide-hard-drive
Expand Down Expand Up @@ -304,6 +318,10 @@ SENTRY_DSN=https://key@o0.ingest.sentry.io/123

# Better Stack (NUXT_BETTER_STACK_* or BETTER_STACK_*)
BETTER_STACK_SOURCE_TOKEN=your-source-token

# Datadog (NUXT_DATADOG_* or DATADOG_* or DD_*)
DD_API_KEY=your-api-key
DD_SITE=datadoghq.eu
```

Adapters auto-read from these variables, so just call `createXDrain()` with no arguments.
220 changes: 220 additions & 0 deletions apps/docs/content/5.adapters/8.datadog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
title: Datadog Adapter
description: Send wide events to Datadog Logs via the native HTTP intake API. Supports all Datadog sites and DD_* environment variables.
navigation:
title: Datadog
icon: i-simple-icons-datadog
links:
- label: Datadog Logs
icon: i-lucide-external-link
to: https://docs.datadoghq.com/logs/
target: _blank
color: neutral
variant: subtle
- label: OTLP Adapter
icon: i-simple-icons-opentelemetry
to: /adapters/otlp
color: neutral
variant: subtle
---

[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](/adapters/otlp).

::code-collapse

```txt [Prompt]
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/adapters/datadog
Framework setup: https://www.evlog.dev/frameworks
```

::

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

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

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createDatadogDrain())
})
```
```typescript [Hono]
import { createDatadogDrain } from 'evlog/datadog'

app.use(evlog({ drain: createDatadogDrain() }))
```
```typescript [Express]
import { createDatadogDrain } from 'evlog/datadog'

app.use(evlog({ drain: createDatadogDrain() }))
```
```typescript [Fastify]
import { createDatadogDrain } from 'evlog/datadog'

await app.register(evlog, { drain: createDatadogDrain() })
```
```typescript [Elysia]
import { createDatadogDrain } from 'evlog/datadog'

app.use(evlog({ drain: createDatadogDrain() }))
```
```typescript [NestJS]
import { createDatadogDrain } from 'evlog/datadog'

EvlogModule.forRoot({ drain: createDatadogDrain() })
```
```typescript [Standalone]
import { createDatadogDrain } from 'evlog/datadog'

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

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

| Variable | Nuxt alias | Description |
|----------|------------|-------------|
| `DD_API_KEY` | `NUXT_DATADOG_API_KEY` | Datadog API key (required). Also: `DATADOG_API_KEY` |
| `DD_SITE` | `NUXT_DATADOG_SITE` | Site hostname (e.g. `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`). Also: `DATADOG_SITE` |
| `DATADOG_LOGS_URL` | `NUXT_DATADOG_LOGS_URL` | Full intake URL — overrides URL derived from `site` |

### Runtime Config (Nuxt only)

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

### Override Options

```typescript [server/plugins/evlog-drain.ts]
const drain = createDatadogDrain({
apiKey: '***',
site: 'us5.datadoghq.com',
timeout: 10000,
})
```

### Full Configuration Reference

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | — | Datadog API key (required) |
| `site` | `string` | `datadoghq.com` | Site for intake host `http-intake.logs.${site}` |
| `intakeUrl` | `string` | from `site` | Full `POST` URL for `/api/v2/logs` |
| `timeout` | `number` | `5000` | Request timeout (ms) |
| `retries` | `number` | `2` | Retries on transient failures |

## 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`.
- **`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`.

## 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 NUXT_DATADOG_API_KEY, DATADOG_API_KEY, or 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` / `NUXT_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](/adapters/otlp) — Send logs via OpenTelemetry (works with Datadog Agent / OTLP endpoint)
- [Custom Adapters](/adapters/custom) — Build your own destination
3 changes: 2 additions & 1 deletion apps/docs/skills/review-logging-patterns/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: review-logging-patterns
description: Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, React Router, NestJS, Express, Hono, Fastify, Elysia, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack), sampling, enrichers, and AI SDK integration (token usage, tool calls, streaming metrics).
description: Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, React Router, NestJS, Express, Hono, Fastify, Elysia, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack, Datadog), sampling, enrichers, and AI SDK integration (token usage, tool calls, streaming metrics).
license: MIT
metadata:
author: HugoRCD
Expand Down Expand Up @@ -747,6 +747,7 @@ All options work in Nuxt (`evlog` key), Nitro (passed to `evlog()`), Next.js (`c
| PostHog | `evlog/posthog` | `POSTHOG_API_KEY`, `POSTHOG_HOST` |
| Sentry | `evlog/sentry` | `SENTRY_DSN` |
| Better Stack | `evlog/better-stack` | `BETTER_STACK_SOURCE_TOKEN` |
| Datadog | `evlog/datadog` | `DD_API_KEY` or `DATADOG_API_KEY`, optional `DD_SITE` / `DATADOG_LOGS_URL` |
| File System | `evlog/fs` | None (local file system) |

In Nuxt/Nitro, use the `NUXT_` prefix (e.g., `NUXT_AXIOM_TOKEN`) so values are available via `useRuntimeConfig()`. All adapters also read unprefixed variables as fallback.
Expand Down
4 changes: 4 additions & 0 deletions apps/playground/server/plugins/evlog-drain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// import { createSentryDrain } from 'evlog/sentry'
// import { createBetterStackDrain } from 'evlog/better-stack'
import { createFsDrain } from 'evlog/fs'
import { createDatadogDrain } from 'evlog/datadog'

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', (ctx) => {
Expand All @@ -26,6 +27,9 @@ export default defineNitroPlugin((nitroApp) => {
// const betterStackDrain = createBetterStackDrain()
// betterStackDrain(ctx)

const datadogDrain = createDatadogDrain()
datadogDrain(ctx)

const fsDrain = createFsDrain()
fsDrain(ctx)
})
Expand Down
1 change: 1 addition & 0 deletions examples/react-router/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.react-router
build
1 change: 1 addition & 0 deletions examples/sveltekit/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.svelte-kit
build
Loading
Loading