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

# Splunk

> Send agent logs to a Splunk HTTP Event Collector, with the whole record kept under event and the timestamp filed as the event's own time

A `splunk` drain posts to your own Splunk HTTP Event Collector — Splunk Cloud or a self-hosted stack. Each record becomes one HEC event with `source: lua`, `sourcetype: lua:log`, and the whole Lua record under `event`, so `event.eventName` and `event.attributes.*` are searchable exactly as the [event schema](/drains/event-schema) spells them.

<Info>
  Vendor destinations are opened per deployment. Where `splunk` is not open yet, a create answers `422 DRAIN_TYPE_UNAVAILABLE` and the message names the types that are. **It may not be enabled on your deployment yet** — that is a rollout state, not a fault.
</Info>

**Before you begin**

* A HEC token. Splunk Web → **Settings** → **Data inputs** → **HTTP Event Collector** → **New Token**.
* **HEC enabled globally.** On the same screen, **Global Settings** → **All Tokens** → **Enabled**. A token on a stack where HEC is globally disabled fails every delivery, and it is the most common setup mistake after the header form below.
* The token's default index. Lua does not choose one — see [below](#the-index-is-never-sent).

## What to enter

| Field    | Flag                     | Value                                                                             |
| -------- | ------------------------ | --------------------------------------------------------------------------------- |
| Name     | `--name`                 | 1–64 characters, unique in the organization                                       |
| Type     | `--type splunk`          |                                                                                   |
| Endpoint | `--endpoint`             | Your HEC host, with its port — `https://http-inputs-<stack>.splunkcloud.com:8088` |
| Token    | `--header Authorization` | Enter it as **`Splunk <token>`**, scheme included                                 |

```bash theme={null}
lua drains create \
  --name "Splunk prod" \
  --type splunk \
  --endpoint https://http-inputs-acme.splunkcloud.com:8088 \
  --header Authorization \
  --environments production \
  --min-severity info
```

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

Next: lua drains verify drn_3d81f0c927ba46e15903aa17
```

In CI, read the value from the environment instead:

```bash theme={null}
lua drains create --ci --json \
  --name "Splunk prod" --type splunk \
  --endpoint https://http-inputs-acme.splunkcloud.com:8088 \
  --header-from-env Authorization=SPLUNK_HEC_AUTH   # the value is "Splunk <token>"
```

<Warning>
  **Store `Splunk <token>`, not the bare token.** The stored header value is sent verbatim as `Authorization`; Lua never prefixes a scheme onto it, because prefixing one onto a value that already has it would 401 forever. A bare token is the single most common Splunk setup mistake, and it looks like a bad token rather than a bad header.
</Warning>

`/services/collector/event` is composed onto whatever host you give, and composition is **idempotent**: `https://splunk.example.com:8088` and `https://splunk.example.com:8088/services/collector/event` store the same endpoint, so paste whichever form your Splunk UI showed you. A query string or fragment is dropped — no intake here reads one, and a credential pasted into `?token=…` would otherwise be stored in clear.

There is **no host allow-list** for this type: a Splunk stack is your host. It is still validated as `https:` and as resolving entirely to public addresses, [re-checked on every send](/drains/protecting-your-destination).

## How ownership verification works

<Warning>
  **A vendor preset is reachability-checked, not ownership-verified.** `lua drains verify` posts one batch holding a single `lua.drain.test` record, and any `2xx` from the intake is accepted. It proves the host answers and the credential works. It does **not** prove you own that host — no Splunk intake can echo a challenge token back, so only a [generic HTTPS](/drains/generic-https) drain can be ownership-verified and only an [object-storage](/drains/object-storage) drain proves ownership by writing into a bucket.

  Point a `splunk` drain only at a stack you control.
</Warning>

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

A `403` means the token is wrong, disabled, or HEC is off globally. The drain stays in `pending_verification` and keeps buffering while you fix it. Verification is limited to 5 attempts per drain per hour, so read the error rather than retrying in a loop.

Splunk deliveries are **not signed**. There is no `X-Lua-Signature` and no signing secret; the `Authorization` header authenticates the sender.

## What arrives

The body is HEC's own format: the event objects **concatenated**, with no separator and **no enclosing array**. HEC answers `400 Invalid data format` to an array.

```json theme={null}
{ "time": 1789217997.644, "host": "agent_1789214224176_2vta8rnyn", "source": "lua", "sourcetype": "lua:log", "event": { "id": "1789217997644-jtoulxxxq", "timestamp": "2026-09-12T12:59:57.644Z", "eventName": "lua.skill.error", "severityNumber": 17, "severityText": "ERROR", "body": "Ticket lookup failed: upstream timeout", "attributes": { "lua.source": "skill", "gen_ai.tool.name": "lookup_tickets" } } }
```

| HEC field    | Comes from                                                                                      |
| ------------ | ----------------------------------------------------------------------------------------------- |
| `time`       | The record's `timestamp`, as epoch **seconds** with millisecond precision                       |
| `host`       | `resource["lua.agent.id"]`                                                                      |
| `source`     | Always `lua`                                                                                    |
| `sourcetype` | Always `lua:log`                                                                                |
| `event`      | The **whole record**, unflattened, exactly as [Event schema](/drains/event-schema) describes it |

A timestamp that does not parse **omits `time` entirely**, so Splunk stamps index time. That is the honest fallback: a zero epoch would file the event in 1970 and look like data rather than a gap.

### The index is never sent

A HEC token carries its own list of allowed indexes and its own default. Lua does not name one, because naming an index a token may not write answers `403 Incorrect index` forever — a drain that verifies and then fails on every real batch. **Your records land in the token's default index.** To route them elsewhere, mint a token whose default is the index you want.

### Useful searches

```text theme={null}
index=main sourcetype="lua:log"
index=main sourcetype="lua:log" event.eventName="lua.skill.error"
index=main sourcetype="lua:log" event.attributes{}.gen_ai.tool.name="lookup_tickets"
index=main sourcetype="lua:log" event.eventName="lua.drain.heartbeat"
```

### Caps

| Cap                          | Value                                      |
| ---------------------------- | ------------------------------------------ |
| Records per batch            | 500                                        |
| Uncompressed bytes per batch | 768 KiB (HEC's own request limit is 1 MiB) |
| Bytes per event              | 256 KiB                                    |
| Compression                  | `gzip`, always                             |
| Retried                      | `408`, `429`, and every `5xx`              |
| Terminal                     | Every `3xx`, and every other `4xx`         |

The batch cap sits below the vendor's request limit on purpose: the cap is applied to the stored byte estimate *before* the encoder adds its envelope, and the headroom is what makes "no `413`" a property rather than a hope.

A record over 256 KiB has its `body` cut and suffixed `... [truncated by lua]` rather than being dropped — you still get the event, its severity, and all of its attributes.

HEC signals back-pressure as `503` and rate limiting as `429`. Both are statuses in the retry set, so nothing has to parse a Splunk error body.

## Test the token with curl

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

```bash theme={null}
HEC='https://http-inputs-acme.splunkcloud.com:8088'
HEC_TOKEN='<your HEC token>'

curl -sS -i -X POST "$HEC/services/collector/event" \
  -H "Authorization: Splunk $HEC_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"time":1789217997.644,"host":"agent_1789214224176_2vta8rnyn","source":"lua","sourcetype":"lua:log","event":{"id":"probe-0001","timestamp":"2026-09-12T12:59:57.644Z","eventName":"lua.drain.test","severityNumber":9,"severityText":"INFO","attributes":{"lua.drain.id":"drn_probe"}}}'
```

`200 {"text":"Success","code":0}` means accepted. `403 {"text":"Invalid token","code":4}` means the token is wrong; `400 {"text":"Incorrect index","code":7}` means the token cannot write where it was pointed.

Then do the same through the real delivery path:

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

## If it isn't working

<AccordionGroup>
  <Accordion title="403 on every delivery">
    Either the stored header value is a bare token rather than `Splunk <token>`, or HEC is disabled globally on the stack. Re-enter the whole value with `lua drains update <id> --header Authorization`, and check **Global Settings** → **All Tokens** → **Enabled**.
  </Accordion>

  <Accordion title="400 Invalid data format">
    That is what HEC answers to a JSON array. Lua sends concatenated objects, so this should not happen through a drain — if you see it, something between Lua and HEC is re-wrapping the body.
  </Accordion>

  <Accordion title="Events are in the wrong index">
    Lua never sends `index`. The records land in the HEC token's default index. Mint a token whose default is the index you want, and update the header value.
  </Accordion>

  <Accordion title="Every event has the same timestamp">
    You are looking at index time, which means `time` was omitted because the record's timestamp did not parse. Open one event and check `event.timestamp`; that field is always the platform's own reading.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Event schema" href="/drains/event-schema">What every field under `event` means.</Card>
  <Card title="Protecting your destination" href="/drains/protecting-your-destination">What verification does and does not prove.</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>
