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

# Ship logs to your stack

> What a log drain is, what it selects, the health states it moves through, and the quotas and limits that bound it

A **log drain** copies your agents' execution log records to a destination you own — an HTTPS endpoint, an OpenTelemetry collector, Datadog, or Better Stack — as they happen. It is a copy, not a move: everything a drain sends is still readable with [`lua logs`](/reference/cli/logs) and still counts against the platform's own [retention window](/concepts/security-and-data#retention).

Drains exist so the logs you already have stop being a place you visit and start being a place that pages you. The [monitor packs](/drains/packs/datadog-monitors) turn a working drain into a firing alert in a few minutes.

<Info>
  Log drains are switched on for a deployment as a whole, not per organization. While they are off, `lua drains` and the drain routes answer `404` — the resource does not exist rather than being forbidden, so a caller cannot tell the feature apart from a typo. If every verb answers `404`, ask [support@heylua.ai](mailto:support@heylua.ai).
</Info>

## What a drain is

A drain is a rule that lives on the **organization**, not on one agent. One drain can cover every agent in the organization, including agents created after the drain was, or a named list. That is deliberate: a platform team configures one destination and one credential, and application teams keep shipping agents into it without touching it.

Each drain has:

| Part                | What it is                                                                                          |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| A **destination**   | A type (`http`, `otlp`, `datadog`, `betterstack`) plus the endpoint, site, or token that type needs |
| **Selectors**       | Which sources, severities, agents, and environments are copied                                      |
| A **state**         | Where the drain is in its lifecycle, from unverified to healthy to paused                           |
| **Health counters** | Delivered, dropped, and rejected counts over the last 24 hours, latency, and backlog                |
| A **quota**         | A daily event and byte allowance, with a degradation ladder before it pauses                        |

Create one with [`lua drains create`](/reference/cli/drains), over the [REST API](/reference/rest/log-drains), or from the admin dashboard.

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

## What arrives

Every delivery is a **batch**: one `resource` object describing the agent, and up to 500 `records` describing what happened. The shape is versioned and published as a JSON Schema at [`https://docs.heylua.ai/schemas/logs/1.0`](https://docs.heylua.ai/schemas/logs/1.0); the field-by-field reference is on [Event schema](/drains/event-schema).

```json theme={null}
{
  "schemaUrl": "https://docs.heylua.ai/schemas/logs/1.0",
  "batchId": "01JBW8N2Q4Z6H8Y0KX3R9T5V7M",
  "resource": {
    "service.name": "support-agent",
    "service.namespace": "org_4f2c9a1b",
    "service.instance.id": "agent_1789214224176_2vta8rnyn",
    "deployment.environment.name": "production",
    "lua.org.id": "org_4f2c9a1b",
    "lua.agent.id": "agent_1789214224176_2vta8rnyn"
  },
  "records": [
    {
      "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.source": "skill",
        "lua.primitive.name": "tickets",
        "gen_ai.tool.name": "lookup_tickets",
        "gen_ai.operation.name": "execute_tool",
        "exception.type": "Error",
        "exception.message": "upstream timeout"
      }
    }
  ]
}
```

Consumers dedup on `records[].id`, never on `X-Lua-Batch-Id`.

## Selectors

Selectors decide what a drain copies. They are evaluated per record, before anything leaves the platform.

### Sources

A source is where a record came from. Nineteen are selectable in this phase: the eighteen agent log sources plus `execution`, which carries the start, completion, and failure of an execution rather than a line your code printed.

| Source            | What it carries                                                                                                                                                         | On a new drain   |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `skill`           | Tool calls: input, result, duration, and thrown errors                                                                                                                  | Yes              |
| `job`             | Scheduled [job](/concepts/jobs) runs                                                                                                                                    | Yes              |
| `webhook`         | [Webhook](/concepts/webhooks) handler runs                                                                                                                              | Yes              |
| `trigger`         | [Trigger](/concepts/triggers) handler runs                                                                                                                              | Yes              |
| `preprocessor`    | Preprocessor decisions and blocks                                                                                                                                       | Yes              |
| `postprocessor`   | Postprocessor rewrites                                                                                                                                                  | Yes              |
| `device`          | [Device](/concepts/devices) command handling. **Declared, not emitted yet** — device work is logged under `device-trigger` today, so selecting this source adds nothing | Yes              |
| `device-trigger`  | Device trigger handler runs                                                                                                                                             | Yes              |
| `mcp`             | [MCP server](/concepts/mcp-servers) tool calls, by server and tool name                                                                                                 | Yes              |
| `rag`             | Knowledge-base retrieval                                                                                                                                                | Yes              |
| `runtime`         | The agent runtime's own lines, including model-provider errors                                                                                                          | Yes              |
| `model-resolver`  | Which model a turn resolved to, and why                                                                                                                                 | Yes              |
| `workflow`        | [Workflow](/concepts/workflows) run lifecycle                                                                                                                           | Yes              |
| `workflow-step`   | Per-step execution                                                                                                                                                      | Yes              |
| `workflow-script` | Script-form workflow output                                                                                                                                             | Yes              |
| `agent_error`     | Failures of the message pipeline itself: billing, validation, model errors                                                                                              | Yes              |
| `execution`       | Execution start, completion, and failure, with a duration                                                                                                               | Yes              |
| `user_message`    | What an end user said                                                                                                                                                   | **No** — content |
| `agent_response`  | What the agent replied                                                                                                                                                  | **No** — content |

`user_message` and `agent_response` carry end-user content. They are off unless you turn on `--include-content`, which prints an acknowledgement you have to accept before the drain is created. Even then, every record still passes the [scrubber](#what-never-leaves-lua), and a drain's `redact` list drops named attributes — `user.id`, for instance — before the record is encoded.

### Severity

`--min-severity` keeps records at or above one of `debug`, `info`, `warn`, `error`. The default is `info`, which drops `debug` lines — the bulk of most agents' volume.

`--sampling debug=0.1` keeps a deterministic tenth of `debug` records instead of all or none. Sampling is by execution, so a sampled execution keeps all of its debug lines rather than a scattered fraction of them; only `debug` is sampleable.

### Agents and environments

`--agents all` (the default) covers every agent in the organization, now and later. `--agents <id>,<id>` pins the drain to a list. `--environments production` (the default) or `--environments production,sandbox` decides whether the sandbox is copied too; a sandbox drain is the fastest way to prove a destination works before production traffic reaches it.

<Tip>
  Run `lua drains create` inside a project directory and `--agents` defaults to that project's agent rather than to every agent in the organization.
</Tip>

## States

A drain moves through six states. Delivery only happens in three of them.

| State                  | Delivering? | Buffering? | Meaning                                                                                |
| ---------------------- | ----------- | ---------- | -------------------------------------------------------------------------------------- |
| `pending_verification` | No          | Yes        | Created, or its endpoint changed. Records queue until ownership is proved              |
| `healthy`              | Yes         | —          | Verified and succeeding                                                                |
| `degraded`             | Yes         | —          | More than 10% of the last five minutes' attempts failed, or it has just resumed        |
| `failing`              | Yes         | —          | No success for 15 minutes. Still trying                                                |
| `paused`               | No          | Yes        | Stopped by you, by the quota ladder, or automatically after 24 hours without a success |
| `disabled`             | No          | No         | Turned off. Nothing is queued while it is off                                          |

A drain that stays in `failing` for 24 hours pauses itself. Resuming always lands in `degraded`, never straight in `healthy`: it has to prove itself again. Changing the endpoint or the type sends a drain back to `pending_verification`, and records buffer while you re-verify. [Delivery guarantees](/drains/delivery-guarantees#health-states) has the full transition table.

```bash theme={null}
lua drains status
```

```text Output theme={null}
NAME            TYPE         STATE     ENV         AGENTS  BACKLOG  LAST SUCCESS
Datadog prod    datadog      healthy   production  all     0        2026-09-21T09:14:02Z
Receiver (dev)  http         degraded  sandbox     2       142      2026-09-21T09:08:47Z
```

`lua drains status` exits `2` when any drain is `failing`. That is also the usage exit code, so a guard that must tell a failing drain from a mistyped command reads the `state` field in `--json` rather than the exit status.

## What never leaves Lua

Three things hold, on every drain, with no way to turn them off:

* **Credential shapes are masked.** Every record body and every string attribute passes a scrubber before it is encoded. It replaces recognizable credentials — bearer and basic headers, JWTs, private-key blocks, connection-string passwords, URL secret parameters, Lua API keys and handoff codes, and well-known vendor keys from Stripe, Slack, GitHub, Twilio, Google, OpenAI, and Anthropic — with a marker such as `[redacted:vendor-key]`. Your organization can add up to 20 regular expressions of its own, each at most 256 characters, masked as `[redacted:org:<rule-id>]`.
* **Destination credentials are never readable.** Header values and the HMAC signing secret are write-only. A read of a drain returns header names and the last four characters, never a value; the signing secret is shown exactly once, when it is created or rotated.
* **No request body is stored.** The delivery log keeps the status code, latency, byte count, error class, and up to 1 KB of the destination's *response* — scrubbed. It has no field that can hold what was sent.

Scrubbing is not a substitute for not logging secrets. Anything your code prints with `console.log` is stored as printed, and the scrubber only catches shapes it recognizes.

## Quotas and limits

| Limit                        | Value                                                              |
| ---------------------------- | ------------------------------------------------------------------ |
| Drains per organization      | 5; 10 on Enterprise                                                |
| Records per batch            | 500; 1,000 for Datadog                                             |
| Uncompressed bytes per batch | 1 MiB; 5 MiB for Datadog                                           |
| Custom headers per drain     | 10                                                                 |
| Organization scrub rules     | 20, at most 256 characters each                                    |
| `redact` attribute keys      | 32                                                                 |
| Verification attempts        | 5 per drain per hour                                               |
| Events per day               | 1,000,000 on Team; 10,000,000 on Business; unmetered on Enterprise |
| Bytes per day                | 1 GiB on Team; 10 GiB on Business; unmetered on Enterprise         |

Going over the daily quota degrades the drain in steps rather than cutting it off:

| Usage | What happens                          |
| ----- | ------------------------------------- |
| 80%   | A notification. Nothing is dropped    |
| 100%  | `debug` records are dropped           |
| 125%  | `info` records are dropped too        |
| 150%  | The drain pauses, with reason `quota` |

`warn` and `error` records are the last to go: the ladder never drops them before it pauses the drain. Quota counters reset at 00:00 UTC and are visible on `lua drains status --json` under `quota`.

## Egress allow-listing

Deliveries leave from Lua's own network, not from your agent's sandbox, so an IP allow-list needs the platform's egress address rather than a DNS name. There is one, in `eu-west-1`:

```text theme={null}
34.255.23.103/32
```

It covers every drain in both the `production` and `sandbox` agent environments — a drain with `--environments production,sandbox` needs nothing else allow-listed. The same value is served machine-readably from `https://docs.heylua.ai/drains/egress.json`, so an allow-list can be refreshed from CI rather than copied by hand. An address changes rarely and never without notice on this page.

Every delivery identifies itself with `User-Agent: LuaDrain/1.0 (+https://docs.heylua.ai/drains)`.

The `X-Lua-*` protocol headers ride the [generic HTTPS](/drains/generic-https) drain only, which is also the only destination whose batches are signed: `X-Lua-Batch-Id`, `X-Lua-Drain-Id`, `X-Lua-Schema`, and `X-Lua-Signature` (see [Verify signatures](/drains/verify-signatures)). An [OTLP](/drains/opentelemetry) delivery carries `X-Lua-Batch-Id` and nothing else of ours. The [Datadog](/drains/datadog) and [Better Stack](/drains/better-stack) presets carry none of them: a vendor intake gets the vendor's own authentication header, and the batch and drain ids travel inside the record instead. Route a shared receiver by `X-Lua-Drain-Id` only when it is a generic HTTPS drain that reaches it.

## Pick a destination

<Columns cols={2}>
  <Card title="Generic HTTPS" href="/drains/generic-https">Your own receiver. Signed, with a copy-paste Node example.</Card>
  <Card title="OpenTelemetry" href="/drains/opentelemetry">OTLP/HTTP into a collector, Loki, or anything downstream of one.</Card>
  <Card title="Datadog" href="/drains/datadog">Log Explorer, with `service`, `status`, and `env` already right.</Card>
  <Card title="Better Stack" href="/drains/better-stack">A hosted source, with levels mapped.</Card>
</Columns>

## Next steps

<Columns cols={2}>
  <Card title="Event schema" href="/drains/event-schema">Every field, the severity table, and the versioning policy.</Card>
  <Card title="Delivery guarantees" href="/drains/delivery-guarantees">At-least-once, batching, retry, and what a drop looks like.</Card>
  <Card title="lua drains" href="/reference/cli/drains">Every verb, flag, and `--json` shape.</Card>
  <Card title="Log drains REST" href="/reference/rest/log-drains">The routes, bodies, and error codes.</Card>
</Columns>
