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

# Better Stack

> Send agent logs to a Better Stack source, with levels mapped and the whole record kept as structured fields

A `betterstack` drain posts to one Better Stack source's ingesting host. Records arrive with `dt` as the timestamp, `level` as the severity, `message` as the log line, and the whole Lua record kept alongside as structured fields you can query and alert on.

**Before you begin**

* A Better Stack source of type **HTTP**. Its Connect page gives you two things you need: the **ingesting host** and the **source token**.

## What to enter

| Field    | Flag                     | Value                                                              |
| -------- | ------------------------ | ------------------------------------------------------------------ |
| Name     | `--name`                 | 1–64 characters, unique in the organization                        |
| Type     | `--type betterstack`     |                                                                    |
| Endpoint | `--endpoint`             | `https://<your ingesting host>` from the source's Connect page     |
| Token    | `--header Authorization` | The CLI prompts for the value. Enter it as `Bearer <source token>` |

```bash theme={null}
lua drains create \
  --name "Better Stack prod" \
  --type betterstack \
  --endpoint https://s1234567.eu-nbg-2.betterstackdata.com \
  --header Authorization \
  --environments production \
  --min-severity info
```

```text Output theme={null}
? Value for header Authorization: ********************************
✔ Created drain drn_44a0cb18e7d2905f31b6ee08 (pending_verification)

Next: lua drains verify drn_44a0cb18e7d2905f31b6ee08
```

The ingesting host is specific to your source and to the region the source lives in — it is not a single shared endpoint, so copy it from the Connect page rather than guessing.

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

## How ownership verification works

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

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

A `401` means the token is wrong or is missing its `Bearer ` prefix — the stored value is sent as the whole header value, so `Bearer ` is part of what you enter. The drain stays in `pending_verification` and keeps 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}
[
  {
    "dt": "2026-09-12T12:59:57.644Z",
    "level": "error",
    "message": "Ticket lookup failed: upstream timeout",
    "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"
      }
    }
  }
]
```

| Better Stack field | Comes from                                                                                    |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `dt`               | The record's `timestamp`, RFC 3339                                                            |
| `level`            | The severity, lower-cased: `debug`, `info`, `warn`, `error`                                   |
| `message`          | The record's `body`, or its `eventName` when there is no body                                 |
| `lua`              | The **whole record**, unchanged, exactly as [Event schema](/drains/event-schema) describes it |

The batch's `resource` is **not** flattened into each entry, so an entry carries no organization id, agent id, or agent name — only the record. Group by `lua.attributes["lua.primitive.name"]` for a primitive and by `lua.eventName` for a kind of event.

<Note>
  If you need the agent or the environment **on every record** — one source receiving several agents, for example — send to an [OpenTelemetry](/drains/opentelemetry) collector instead and promote the resource attributes there. The published [collector config](/drains/packs/otel-collector) already copies `service.name` and the environment onto each record, and its exporter can forward to Better Stack.
</Note>

### Useful queries

```text theme={null}
lua.severityText:ERROR
lua.eventName:"lua.skill.error"
lua.eventName:"lua.drain.heartbeat"
lua.attributes.gen_ai.tool.name:"lookup_tickets"
```

### Caps

| Cap                          | Value                                    |
| ---------------------------- | ---------------------------------------- |
| Records per batch            | 500                                      |
| Uncompressed bytes per batch | 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`       |

## Test the token with curl

Prove the host and the token before creating the drain. This sends exactly the shape a delivery has:

```bash theme={null}
INGEST='https://s1234567.eu-nbg-2.betterstackdata.com'
SOURCE_TOKEN='<your Better Stack source token>'

curl -sS -i -X POST "$INGEST" \
  -H "Authorization: Bearer $SOURCE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '[
    {
      "dt": "2026-09-12T12:59:57.644Z",
      "level": "info",
      "message": "Lua drain connectivity test",
      "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" }
      }
    }
  ]'
```

A `202` means accepted; the entry shows up in Live tail within a few seconds. A `401` means the token is wrong or the `Bearer ` prefix is missing; a `404` usually means the ingesting host belongs to a different source or region.

Then do the same through the real delivery path:

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

## Alert on it

Better Stack alerts on a saved query. Three that are worth having from day one:

| Alert                   | Query                                 | Condition                  |
| ----------------------- | ------------------------------------- | -------------------------- |
| Agent errors spiking    | `lua.severityText:ERROR`              | More than 5 in 5 minutes   |
| Drain heartbeat missing | `lua.eventName:"lua.drain.heartbeat"` | Fewer than 1 in 15 minutes |
| Records dropped         | `lua.eventName:"lua.drain.dropped"`   | More than 0 in 15 minutes  |

The heartbeat is what makes the other two trustworthy: it arrives every five minutes while the drain is `healthy`, so its absence tells you the pipeline is down rather than that your agents went quiet. [Delivery guarantees](/drains/delivery-guarantees#heartbeat) explains what it does and does not prove.

## If it isn't working

<AccordionGroup>
  <Accordion title="401 on every delivery">
    The stored header value is sent verbatim as `Authorization`. It must read `Bearer <source token>`, prefix included. Update it with `lua drains update <id> --header Authorization` and re-enter the whole value.
  </Accordion>

  <Accordion title="404 or a redirect">
    The ingesting host is per-source and per-region. Copy it from that source's Connect page. A `3xx` is treated as a failure and never followed, so a host that redirects has to be replaced with its final URL.
  </Accordion>

  <Accordion title="Nothing arrives although the drain is healthy">
    `healthy` means the ingesting host accepted the batch. Check that you are looking at the right source, and that the source's retention and any archiving rule have not moved the data. `lua drains deliveries <id>` shows the status code and response excerpt for each recent batch.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <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="Ship logs to your stack" href="/drains/overview">Selectors, states, and quotas.</Card>
  <Card title="lua drains" href="/reference/cli/drains">Every verb and flag.</Card>
</Columns>
