Datadog Adapter
Datadog is a monitoring and security platform. The evlog Datadog adapter sends your wide events to Datadog Logs using the HTTP Logs intake API (v2) with the DD-API-KEY header.
For OpenTelemetry-based ingestion instead, see the OTLP adapter.
Add the Datadog drain adapter
Installation
The Datadog adapter comes bundled with evlog:
import { createDatadogDrain } from 'evlog/datadog'
Quick Start
1. Get your API key
- Open Datadog Organization Settings → API Keys
- Create or copy an API key with permission to submit logs
2. Set environment variables
DD_API_KEY=your-api-key
# Optional — defaults to datadoghq.com (US1)
DD_SITE=datadoghq.eu
3. Wire the drain to your framework
// server/plugins/evlog-drain.ts
import { createDatadogDrain } from 'evlog/datadog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createDatadogDrain())
})
// 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(),
})
import { createDatadogDrain } from 'evlog/datadog'
app.use(evlog({ drain: createDatadogDrain() }))
import { createDatadogDrain } from 'evlog/datadog'
app.use(evlog({ drain: createDatadogDrain() }))
import { createDatadogDrain } from 'evlog/datadog'
await app.register(evlog, { drain: createDatadogDrain() })
import { createDatadogDrain } from 'evlog/datadog'
app.use(evlog({ drain: createDatadogDrain() }))
import { createDatadogDrain } from 'evlog/datadog'
EvlogModule.forRoot({ drain: createDatadogDrain() })
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):
- Overrides passed to
createDatadogDrain() - Runtime config at
runtimeConfig.datadogorruntimeConfig.evlog.datadog(Nuxt/Nitro) - Environment variables — see table below
Environment Variables
| Variable | Description |
|---|---|
DD_API_KEY | Datadog API key (required). Also: DATADOG_API_KEY |
DD_SITE | Site hostname (e.g. datadoghq.com, datadoghq.eu, us3.datadoghq.com). Also: DATADOG_SITE |
DATADOG_LOGS_URL | Full intake URL — overrides URL derived from site |
Runtime Config (Nuxt only)
export default defineNuxtConfig({
runtimeConfig: {
datadog: {
apiKey: '', // Set via DD_API_KEY or DATADOG_API_KEY
site: 'datadoghq.eu',
},
},
})
Override Options
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 withformatDatadogMessageLine. Easier to scan than a full JSON blob in Live Tail.evlog— full wide event as a JSON object (not a string). Numeric HTTPstatusfields anywhere in the tree are renamed tohttpStatusCodeso they never clash with Datadog’s reserved severitystatus.dd—{ trace_id, span_id }when the event carries trace context. See Trace correlation.service,status(Datadog severity — drives Live Tail color),ddsource:evlog,ddtags:env:…and optionalversion:…timestamp: Unix milliseconds fromWideEvent.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 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:
{
"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 — 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() — it parses the incoming W3C traceparent header into event.traceId / event.spanId:
import { createDefaultEnrichers } from 'evlog/enrichers'
const enrich = createDefaultEnrichers()
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:enrich', enrich)
})
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:
log.set({ traceId: tracer.scope().active()?.context().toTraceId() })
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 underevlog, not a JSON string inmessage - Metrics: log-based metrics on
@evlog.*attributes - Pipelines: if you previously parsed a full JSON string inside
message, move those facets to@evlog.*. Themessagefield 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
[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
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 — Send logs via OpenTelemetry (works with Datadog Agent / OTLP endpoint)
- Custom Adapters — Build your own destination