Skip to main content
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 and still counts against the platform’s own retention window. 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 turn a working drain into a firing alert in a few minutes.
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 [email protected].

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: Create one with lua drains create, over the REST API, or from the admin dashboard.

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; the field-by-field reference is on Event schema.
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. 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, 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.
Run lua drains create inside a project directory and --agents defaults to that project’s agent rather than to every agent in the organization.

States

A drain moves through six states. Delivery only happens in three of them. 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 has the full transition table.
Output
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

Going over the daily quota degrades the drain in steps rather than cutting it off: 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:
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 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). An OTLP delivery carries X-Lua-Batch-Id and nothing else of ours. The Datadog and 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

Generic HTTPS

Your own receiver. Signed, with a copy-paste Node example.

OpenTelemetry

OTLP/HTTP into a collector, Loki, or anything downstream of one.

Datadog

Log Explorer, with service, status, and env already right.

Better Stack

A hosted source, with levels mapped.

Next steps

Event schema

Every field, the severity table, and the versioning policy.

Delivery guarantees

At-least-once, batching, retry, and what a drop looks like.

lua drains

Every verb, flag, and --json shape.

Log drains REST

The routes, bodies, and error codes.