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

# OpenTelemetry Collector config

> A collector pipeline that receives a Lua OTLP log drain: OTLP/HTTP receiver with bearer auth and TLS, memory limiter, attribute promotion, batching, and an exporter

A complete OpenTelemetry Collector configuration for receiving an [OTLP drain](/drains/opentelemetry). It authenticates the drain with a bearer token, promotes two resource attributes onto every record so attribute-indexing backends can filter on them, batches, and exports — to `debug` while you are proving it works, and to Loki as the worked example.

**Before you begin**

* A place to run the collector (`otel/opentelemetry-collector-contrib` — `bearertokenauth` and the `transform` processor are contrib components, not in the core distribution).
* A TLS certificate for the collector's hostname. Lua refuses plain `http://` endpoints, so the drain must reach it over HTTPS: either terminate TLS in the collector, as below, or put it behind a proxy that does and drop the `tls:` block.
* A token you generate. It is shared state: the same value goes in the drain's `Authorization` header and in the collector's environment.

## The config

```yaml lua-otel-collector.yaml theme={null}
# OpenTelemetry Collector — receive logs from a Lua OTLP log drain.
#
# The drain POSTs gzipped OTLP/JSON to  https://<this host>:4318/v1/logs
# with  Authorization: Bearer <token>.  Lua only accepts HTTPS endpoints,
# so either terminate TLS here (the tls: block below) or put this behind a
# proxy that does and drop the block.
#
# Docs: https://docs.heylua.ai/drains/opentelemetry

extensions:
  # Validates the Authorization header on every incoming request.
  # Set LUA_DRAIN_TOKEN to the same value you gave the drain's
  # Authorization header (including the "Bearer " prefix on the Lua side).
  bearertokenauth/lua:
    scheme: Bearer
    token: ${env:LUA_DRAIN_TOKEN}

  health_check:
    endpoint: 0.0.0.0:13133

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
        auth:
          authenticator: bearertokenauth/lua
        # A Lua batch is at most 1 MiB uncompressed; 4 MiB leaves headroom.
        max_request_body_size: 4194304
        tls:
          cert_file: /etc/otel/tls/tls.crt
          key_file: /etc/otel/tls/tls.key

processors:
  # Shed load before the process is killed, never after.
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15

  # Copy two resource attributes onto every record so backends that index
  # log attributes (Loki structured metadata, Elastic fields) can filter on
  # them without a resource join.
  transform/lua:
    error_mode: ignore
    log_statements:
      - context: log
        statements:
          - set(attributes["lua.env"], resource.attributes["deployment.environment.name"])
          - set(attributes["lua.agent.name"], resource.attributes["service.name"])

  batch:
    timeout: 5s
    send_batch_size: 1024
    send_batch_max_size: 2048

exporters:
  # Start here: prints what arrived, so you can confirm the drain end to end
  # before you point it at a backend. Remove it once the pipeline works.
  debug:
    verbosity: normal
    sampling_initial: 5
    sampling_thereafter: 200

  # Example backend: Loki's native OTLP endpoint. Swap for whichever
  # otlphttp/otlp exporter your stack uses — the pipeline does not change.
  otlphttp/loki:
    logs_endpoint: https://loki.example.com:3100/otlp/v1/logs
    compression: gzip
    headers:
      X-Scope-OrgID: lua
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 1000

service:
  extensions: [bearertokenauth/lua, health_check]
  pipelines:
    logs:
      receivers: [otlp]
      processors: [memory_limiter, transform/lua, batch]
      exporters: [debug, otlphttp/loki]
  telemetry:
    logs:
      level: info
```

### What each piece is for

| Component                        | Why it is there                                                                                                                                                                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bearertokenauth/lua`            | Validates the `Authorization` header on every request. Without it the endpoint accepts logs from anyone who finds the hostname                                                                          |
| `otlp` receiver, `http` protocol | The drain POSTs to `/v1/logs` on this listener. `4318` is the OTLP/HTTP default                                                                                                                         |
| `max_request_body_size`          | A Lua batch is at most 1 MiB uncompressed; 4 MiB leaves headroom without letting an unbounded body in                                                                                                   |
| `memory_limiter`                 | Sheds load before the process is killed. It must be **first** in the pipeline to do its job                                                                                                             |
| `transform/lua`                  | Copies `deployment.environment.name` and `service.name` from the resource onto each record, so backends that index log attributes but not resource attributes can still filter by environment and agent |
| `batch`                          | One export per 1,024 records or 5 seconds, instead of one per delivery                                                                                                                                  |
| `debug`                          | Prints what arrived. The fastest way to confirm the drain end to end                                                                                                                                    |
| `otlphttp/loki`                  | The worked example. Swap it for whichever OTLP exporter your backend needs — nothing else in the pipeline changes                                                                                       |

`service.name` already carries the agent's name and `service.instance.id` its id, so nothing in the pipeline needs to construct them. A processor that overwrites `service.name` will collapse every agent into one service downstream.

## Run it

```bash theme={null}
export LUA_DRAIN_TOKEN='a-long-random-string-you-generated'

docker run --rm -p 4318:4318 -p 13133:13133 \
  -e LUA_DRAIN_TOKEN \
  -v "$PWD/lua-otel-collector.yaml:/etc/otel/config.yaml:ro" \
  -v "$PWD/tls:/etc/otel/tls:ro" \
  otel/opentelemetry-collector-contrib:latest \
  --config=/etc/otel/config.yaml
```

Check it is up:

```bash theme={null}
curl -sS http://localhost:13133/
```

## Point a drain at it

```bash theme={null}
lua drains create \
  --name "OTel collector" \
  --type otlp \
  --endpoint https://otel.example.com:4318/v1/logs \
  --header Authorization \
  --environments production

# when prompted for the Authorization value, enter:  Bearer <LUA_DRAIN_TOKEN>

lua drains verify drn_c8104e77b3a9dd5216f0e442
```

Verification sends one `lua.drain.test` record. With the `debug` exporter in the pipeline it appears in the collector's own output within a second or two:

```text theme={null}
LogRecord #0
ObservedTimestamp: 2026-09-21 09:14:02.881 +0000 UTC
Timestamp: 2026-09-21 09:14:02.743 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
EventName: lua.drain.test
Body: Str(Lua drain connectivity test)
Attributes:
     -> log.record.uid: Str(01JBWA6R1KD3P8N0SY4V2XQZ7T-0)
     -> lua.drain.id: Str(drn_c8104e77b3a9dd5216f0e442)
```

Then `lua drains status` shows the drain `healthy` and the backlog draining.

## Swap the exporter

The receiver, auth, and processors stay the same whatever is downstream. Replace `otlphttp/loki` and the `exporters:` list in the pipeline:

<Tabs>
  <Tab title="Loki">
    ```yaml theme={null}
    exporters:
      otlphttp/loki:
        logs_endpoint: https://loki.example.com:3100/otlp/v1/logs
        compression: gzip
        headers:
          X-Scope-OrgID: lua
    ```

    The [Grafana dashboard](/drains/packs/grafana-dashboard) is built on what this produces.
  </Tab>

  <Tab title="Any OTLP backend">
    ```yaml theme={null}
    exporters:
      otlphttp/vendor:
        endpoint: https://otlp.vendor.example.com
        compression: gzip
        headers:
          api-key: ${env:VENDOR_API_KEY}
    ```
  </Tab>

  <Tab title="Files, for a first look">
    ```yaml theme={null}
    exporters:
      file/raw:
        path: /var/log/otel/lua-logs.json
    ```

    One JSON object per line. Useful for confirming the schema before you commit to a backend.
  </Tab>
</Tabs>

## If it isn't working

<AccordionGroup>
  <Accordion title="The drain will not verify">
    Run the [OTLP curl](/drains/opentelemetry#test-it-with-curl) against the same URL from outside your network. A `401` means the token does not match `LUA_DRAIN_TOKEN`; remember the drain's stored header value includes the `Bearer ` prefix and the collector's `token` does not. A connection failure usually means TLS: Lua refuses plain `http://`, and a self-signed certificate is refused as well.
  </Accordion>

  <Accordion title="bearertokenauth is not a known extension">
    You are running the core distribution. `bearertokenauth` and `transform` are contrib components: use `otel/opentelemetry-collector-contrib`.
  </Accordion>

  <Accordion title="Records arrive but nothing reaches the backend">
    Keep `debug` in the pipeline while you diagnose — it tells you whether the problem is before or after the batch processor. The collector's own logs report exporter failures with the backend's status code; `sending_queue` and `retry_on_failure` mean a brief backend outage is absorbed rather than lost.
  </Accordion>

  <Accordion title="Memory grows under load">
    `memory_limiter` must be first in the `processors` list, before `transform` and `batch`. Behind it, `sending_queue.queue_size` bounds what the exporter holds. If the backend is persistently slower than the drain, the collector will refuse batches and Lua will retry them within its [six-hour horizon](/drains/delivery-guarantees#the-six-hour-horizon) — that is the intended backpressure path.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Grafana dashboard" href="/drains/packs/grafana-dashboard">Panels over what this pipeline produces.</Card>
  <Card title="OpenTelemetry" href="/drains/opentelemetry">The drain side: wire shape, caps, and retry.</Card>
  <Card title="Event schema" href="/drains/event-schema#otlp-mapping">The full OTLP field mapping.</Card>
  <Card title="Delivery guarantees" href="/drains/delivery-guarantees">Retry, drops, and the heartbeat.</Card>
</Columns>
