Cloud

Datadog Adapter

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

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:

src/index.ts
import { createDatadogDrain } from 'evlog/datadog'

Quick Start

1. Get your API key

  1. Open Datadog Organization Settings → API Keys
  2. Create or copy an API key with permission to submit logs

2. Set environment variables

.env
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())
})

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

VariableDescription
DD_API_KEYDatadog API key (required). Also: DATADOG_API_KEY
DD_SITESite hostname (e.g. datadoghq.com, datadoghq.eu, us3.datadoghq.com). Also: DATADOG_SITE
DATADOG_LOGS_URLFull intake URL — overrides URL derived from site

Runtime Config (Nuxt only)

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

Override Options

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

Full Configuration Reference

OptionTypeDefaultDescription
apiKeystringDatadog API key (required)
sitestringdatadoghq.comSite for intake host http-intake.logs.${site}
intakeUrlstringfrom siteFull POST URL for /api/v2/logs
timeoutnumber5000Request timeout (ms)
retriesnumber2Retries 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.
  • 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 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 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:

server/plugins/evlog-enrich.ts
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 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

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

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