> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Datadog

> Send agent logs to the Datadog Logs intake with service, status, and env already mapped, plus a curl to prove the API key

A `datadog` drain posts to the Datadog Logs HTTP intake for your site. Records arrive in Log Explorer with `service` set to the agent's name, `status` set to the record's severity, and `env`, `agent`, `org`, and `source` as tags — so the standard Datadog facets work with no pipeline of your own.

**Before you begin**

* A Datadog API key (not an application key) with log-write access.
* The site your Datadog organization is on. It is the domain in your Datadog URL.

## What to enter

| Field   | Flag                  | Value                                                                 |
| ------- | --------------------- | --------------------------------------------------------------------- |
| Name    | `--name`              | 1–64 characters, unique in the organization                           |
| Type    | `--type datadog`      |                                                                       |
| Site    | `--site`              | One of the sites below                                                |
| API key | `--header DD-API-KEY` | The CLI prompts for the value; it is never passed on the command line |

`--endpoint` is **not** accepted for this type. The intake URL is derived from the site:

| `--site`            | Intake                                                   |
| ------------------- | -------------------------------------------------------- |
| `datadoghq.com`     | `https://http-intake.logs.datadoghq.com/api/v2/logs`     |
| `datadoghq.eu`      | `https://http-intake.logs.datadoghq.eu/api/v2/logs`      |
| `us3.datadoghq.com` | `https://http-intake.logs.us3.datadoghq.com/api/v2/logs` |
| `us5.datadoghq.com` | `https://http-intake.logs.us5.datadoghq.com/api/v2/logs` |
| `ap1.datadoghq.com` | `https://http-intake.logs.ap1.datadoghq.com/api/v2/logs` |
| `ddog-gov.com`      | `https://http-intake.logs.ddog-gov.com/api/v2/logs`      |

Only those hosts are allowed. Anything else is refused when the drain is created.

```bash theme={null}
lua drains create \
  --name "Datadog prod" \
  --type datadog \
  --site datadoghq.eu \
  --header DD-API-KEY \
  --environments production \
  --min-severity info
```

```text Output theme={null}
? Value for header DD-API-KEY: ********************************
✔ Created drain drn_2b7f10d9ac4e83615702ffab (pending_verification)

Next: lua drains verify drn_2b7f10d9ac4e83615702ffab
```

In CI, where prompting is not possible, read the value from the environment instead:

```bash theme={null}
lua drains create --ci --json \
  --name "Datadog prod" --type datadog --site datadoghq.eu \
  --header-from-env DD-API-KEY=DATADOG_API_KEY \
  --environments production
```

<Note>
  Datadog deliveries are **not signed**. There is no `X-Lua-Signature` header and no signing secret; the `DD-API-KEY` header authenticates the sender. If you need a signature, use a [generic HTTPS](/drains/generic-https) drain.
</Note>

## How ownership verification works

Datadog uses a **test post**: one batch containing a single `lua.drain.test` record is sent to the intake, and any `2xx` is accepted.

```bash theme={null}
lua drains verify drn_2b7f10d9ac4e83615702ffab
```

A `403` from the intake means the API key is wrong or is not valid for that site — an API key is site-specific, and a US key against `datadoghq.eu` fails exactly this way. The drain stays in `pending_verification` with `verification.outcome: "no_2xx"` and `statusCode: 403`, and records keep buffering (for up to six hours) while you fix it. Verification is limited to 5 attempts per drain per hour.

## What arrives

Each record becomes one entry in a JSON array:

```json theme={null}
[
  {
    "ddsource": "lua",
    "service": "support-agent",
    "hostname": "agent_1789214224176_2vta8rnyn",
    "ddtags": "env:production,agent:agent_1789214224176_2vta8rnyn,org:org_4f2c9a1b,source:skill",
    "message": "Ticket lookup failed: upstream timeout",
    "status": "error",
    "timestamp": 1789217997644,
    "lua": {
      "id": "1789217997644-jtoulxxxq",
      "timestamp": "2026-09-12T12:59:57.644Z",
      "observedTimestamp": "2026-09-12T12:59:59.102Z",
      "eventName": "lua.skill.error",
      "severityNumber": 17,
      "severityText": "ERROR",
      "body": "Ticket lookup failed: upstream timeout",
      "attributes": {
        "lua.log.type": "log",
        "lua.source": "skill",
        "lua.primitive.name": "tickets",
        "gen_ai.tool.name": "lookup_tickets",
        "gen_ai.operation.name": "execute_tool",
        "lua.duration_ms": 1841,
        "exception.type": "Error",
        "exception.message": "upstream timeout"
      }
    }
  }
]
```

| Datadog field | Comes from                                                                                    |
| ------------- | --------------------------------------------------------------------------------------------- |
| `ddsource`    | Always `lua`                                                                                  |
| `service`     | `resource["service.name"]` — the agent's name                                                 |
| `hostname`    | `resource["lua.agent.id"]`                                                                    |
| `ddtags`      | `env:<environment>,agent:<agentId>,org:<orgId>,source:<lua.source>`                           |
| `message`     | The record's `body`, or its `eventName` when there is no body                                 |
| `status`      | The severity, lower-cased: `debug`, `info`, `warn`, `error`                                   |
| `timestamp`   | Epoch milliseconds                                                                            |
| `lua`         | The **whole record**, unchanged, exactly as [Event schema](/drains/event-schema) describes it |

The complete record under `lua` is what makes the drain useful in Datadog: every field is addressable as an attribute. `@lua.eventName`, `@lua.attributes.gen_ai.tool.name`, `@lua.attributes.lua.primitive.name`, and `@lua.attributes.lua.execution.id` all work in Log Explorer and in monitor queries.

### Useful queries

```text theme={null}
source:lua status:error
source:lua @lua.eventName:lua.skill.error
source:lua @lua.eventName:lua.mcp.* env:production
source:lua @lua.attributes.gen_ai.tool.name:lookup_tickets
source:lua @lua.eventName:lua.drain.heartbeat
```

Facet `@lua.eventName`, `@lua.attributes.lua.source`, and `@lua.attributes.gen_ai.tool.name` the first time you use them; the rest (`service`, `status`, `env`, `host`) are standard Datadog facets already.

### Caps

| Cap                          | Value                                    |
| ---------------------------- | ---------------------------------------- |
| Records per batch            | 1,000                                    |
| Uncompressed bytes per batch | 5 MiB                                    |
| Bytes per record             | 1 MiB                                    |
| Compression                  | `gzip`, always                           |
| Request timeout              | 10 s                                     |
| Retried status codes         | `408`, `429`, `500`, `502`, `503`, `504` |
| Terminal                     | Every other `4xx`, and every `3xx`       |

A single record over 1 MiB has its `body` cut and suffixed `…[truncated for destination]` rather than being dropped — you still get the event, its severity, and all of its attributes.

## Test the key with curl

Prove the key and the site before creating the drain. This sends exactly the shape a delivery has:

```bash theme={null}
SITE='datadoghq.eu'
DD_API_KEY='<your Datadog API key>'

curl -sS -i -X POST "https://http-intake.logs.$SITE/api/v2/logs" \
  -H "DD-API-KEY: $DD_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '[
    {
      "ddsource": "lua",
      "service": "support-agent",
      "hostname": "agent_1789214224176_2vta8rnyn",
      "ddtags": "env:production,agent:agent_1789214224176_2vta8rnyn,org:org_4f2c9a1b,source:drain",
      "message": "Lua drain connectivity test",
      "status": "info",
      "timestamp": 1789217997644,
      "lua": {
        "id": "probe-0001",
        "timestamp": "2026-09-12T12:59:57.644Z",
        "observedTimestamp": "2026-09-12T12:59:59.102Z",
        "eventName": "lua.drain.test",
        "severityNumber": 9,
        "severityText": "INFO",
        "attributes": { "lua.drain.id": "drn_probe" }
      }
    }
  ]'
```

`202 {}` means accepted. Search `source:lua` in Log Explorer; it should appear within a few seconds. `403 {"status":"error","code":403,...}` means the key is wrong for that site.

Then do the same through the real delivery path:

```bash theme={null}
lua drains test drn_2b7f10d9ac4e83615702ffab
```

## Alert on it

The [Datadog monitor pack](/drains/packs/datadog-monitors) is four importable monitors built on these field names: the drain's heartbeat going missing, an error-rate spike per agent, records being dropped, and the runtime truncating an execution's output.

### Your first alert, end to end

Ten minutes of work, then one drill you watch. The steps link to the page that has the detail; nothing here is new, it is the order to do it in.

<Steps>
  <Step title="Prove the API key — 1 minute">
    Run the [curl above](#test-the-key-with-curl) against your site. `202 {}` means the key and the site agree. Doing this first turns the most common failure — a US key against `datadoghq.eu` — into a one-line answer instead of a drain stuck in `pending_verification`.
  </Step>

  <Step title="Create the drain — 2 minutes">
    ```bash theme={null}
    lua drains create \
      --name "Datadog prod" \
      --type datadog \
      --site datadoghq.eu \
      --header DD-API-KEY \
      --agents all \
      --environments production \
      --min-severity info
    ```

    Note the time. The interesting number is how long it takes a record your agents actually produced to appear in Log Explorer, and the clock starts here.
  </Step>

  <Step title="Verify — 1 minute">
    ```bash theme={null}
    lua drains verify drn_2b7f10d9ac4e83615702ffab
    ```

    A `2xx` from the intake moves the drain to `healthy` and releases everything buffered since it was created. Verification is limited to 5 attempts per drain per hour, so read the error rather than retrying in a loop.
  </Step>

  <Step title="See a record — 2 minutes">
    ```bash theme={null}
    lua drains test drn_2b7f10d9ac4e83615702ffab
    lua drains status drn_2b7f10d9ac4e83615702ffab
    ```

    Search `source:lua` in Log Explorer. If your agents are quiet, send one message to an agent in the drain's environment — that produces `execution` and, when a tool runs, `skill` records within seconds.

    `lua drains deliveries drn_2b7f10d9ac4e83615702ffab --kind batch --json` shows the first delivery of real records, as opposed to the synthetic `test` and `verify` ones.
  </Step>

  <Step title="Import the heartbeat monitor — 3 minutes">
    Take **\[Lua] Log drain heartbeat missing** from the [monitor pack](/drains/packs/datadog-monitors) and import it first. The other three alert on something being *present*, so their silence only means anything while the pipeline is known to be up.

    **Add a notification handle to its `message` before saving.** The pack ships with none — an imported monitor alerts in the UI and pages nobody.
  </Step>

  <Step title="Prove it fires — one drill, on its own clock">
    An alert you have not seen fire is a guess. Run the drill below.
  </Step>
</Steps>

### Prove the alert fires

Two drills. The first works for every drain and needs no change to your agents; the second is faster but needs traffic you can make fail.

<Tabs>
  <Tab title="Stop the heartbeat">
    A drain heartbeats once every five minutes, and **only in state `healthy`**. Pausing it stops the heartbeat, the monitor's fifteen-minute window empties, and it fires.

    ```bash theme={null}
    lua drains pause drn_2b7f10d9ac4e83615702ffab --reason "alert drill"
    # wait, watching the monitor
    lua drains resume drn_2b7f10d9ac4e83615702ffab
    ```

    Three things to expect, so the drill does not read as a failure:

    * **Ten to twenty minutes, not fifteen.** The last heartbeat may be up to five minutes old when you pause, and the window is fifteen minutes long, so the earliest possible fire is about ten minutes after the pause. The monitor also carries `notify_no_data` with a twenty-minute `no_data_timeframe`, so it fires on that leg by twenty minutes at the latest. Both legs are deliberate: for the one alert every other alert depends on, a duplicate page beats a missed one.
    * **Nothing is lost.** A paused drain keeps buffering. Records queue and deliver when you resume.
    * **Resume lands in `degraded`, not `healthy`.** Heartbeats do not restart the instant you resume — the drain has to succeed a few times first. Watch `lua drains status` until it reads `healthy` again, then confirm the monitor recovered.

    Write down the pause time and the time the alert reached whoever you handed it to. That number — not the monitor's existence — is what tells you the path works.
  </Tab>

  <Tab title="Drive an error spike">
    **\[Lua] Agent error rate spiking** fires above five records matching `lua.*.error` in five minutes, grouped by agent and environment, and warns at three. Produce six failing tool calls inside five minutes and it fires in five to eight — the fastest honest drill there is.

    Use a sandbox agent and a drain created with `--environments sandbox`, and make something fail on purpose: a tool given input it rejects, an MCP server pointed at a host that is down, or a tool that throws when it sees a magic argument.

    ```text theme={null}
    source:lua @lua.eventName:lua.*.error env:sandbox
    ```

    Run that query in Log Explorer first to confirm the records are arriving and the facets exist, then let the monitor evaluate.
  </Tab>
</Tabs>

#### Two things not to use as a drill

* **Do not point the drain at a broken endpoint to force `failing`.** It takes fifteen minutes with no success to get there and it teaches everyone watching that a red drain is normal. The pause drill proves the same alert in less time and loses nothing.
* **Do not push yourself over the daily quota.** The [degradation ladder](/drains/delivery-guarantees#quota-degradation) drops `debug` records at 100%, `info` at 125%, and pauses the drain at 150% — a real loss of your own records, to test a monitor.

## If it isn't working

<AccordionGroup>
  <Accordion title="403 on every delivery">
    The API key is not valid for the site. Datadog keys are per-organization and per-site; a key minted in a `datadoghq.com` organization is refused by `datadoghq.eu`. Recreate the drain with the right `--site`, or update the header value with `lua drains update <id> --header DD-API-KEY`.
  </Accordion>

  <Accordion title="Logs arrive but service is empty">
    `service` is the agent's name. An agent with no name set shows the id instead. A Datadog log pipeline that remaps `service` will also override it — check your pipelines before blaming the drain.
  </Accordion>

  <Accordion title="Attributes are not searchable">
    Datadog indexes attributes but only facets the ones you ask it to. Open one record in Log Explorer, expand `lua.attributes`, and create a facet on the fields you want to group or alert by. The [monitor pack](/drains/packs/datadog-monitors) lists the three facets its queries need.
  </Accordion>

  <Accordion title="429 from the intake">
    Retried with backoff, honouring `Retry-After`. Sustained rate limiting usually means several drains are pointed at the same Datadog organization; raise `--min-severity` to `warn`, or narrow `--sources`, to cut volume at the source rather than at the intake.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Datadog monitor pack" href="/drains/packs/datadog-monitors">Four monitors to import, with the facets they need.</Card>
  <Card title="Event schema" href="/drains/event-schema">What every field under `lua` means.</Card>
  <Card title="Delivery guarantees" href="/drains/delivery-guarantees">Retry, drops, heartbeat, and health states.</Card>
  <Card title="lua drains" href="/reference/cli/drains">Every verb and flag.</Card>
</Columns>
