Skip to main content
An otlp drain sends the same records as an OTLP/HTTP ExportLogsServiceRequest in JSON encoding. Point it at an OpenTelemetry Collector and everything downstream of that collector — Loki, Elastic, Honeycomb, a vendor’s OTLP endpoint — gets Lua’s logs without a Lua-specific integration. Use it when you already run a collector, or when you want one integration that outlives whichever backend you are on this year. Before you begin
  • An OTLP/HTTP logs endpoint reachable over HTTPS. Private, loopback, link-local, and cloud-metadata addresses are refused; plain http:// is refused.
  • Whatever your endpoint authenticates with — usually a bearer token or an API-key header.

What to enter

The endpoint is used verbatim — nothing is appended. If your collector listens on the default HTTP port, the endpoint is https://<host>:4318/v1/logs. --format is not used: OTLP drains always send OTLP JSON, gzipped.
OTLP deliveries are not signed. There is no X-Lua-Signature header and no signing secret; the collector authenticates the sender with the header you configured. If you need a signature, use a generic HTTPS drain.
Besides your own headers and the transport ones (Content-Type: application/json, Content-Encoding: gzip), a delivery carries exactly two: There is no X-Lua-Signature and no X-Lua-Schema on an OTLP delivery; the schema URL travels in the payload instead, as resourceLogs[].schemaUrl.

How ownership verification works

OTLP uses a test post: a single batch containing one lua.drain.test record is sent to the endpoint, and any 2xx from it is accepted as proof that the endpoint exists and accepts your credential.
The drain moves from pending_verification to healthy on a 2xx, and records buffered since it was created (up to six hours’ worth) start flowing. Verification is limited to 5 attempts per drain per hour. Because the collector is the one judging the request, a 200 here means “the collector accepted it” — not “the backend behind the collector stored it”. Check the collector’s own exporter metrics after the first batch.

The wire shape

One batch becomes one resourceLogs entry. The full field mapping is on Event schema; this is what it looks like on the wire.
records[].id becomes the log.record.uid attribute. Deduplicate on log.record.uid, never on X-Lua-Batch-Id — delivery is at-least-once, and a retried record arrives under a new batch id.

Caps and retry

OTLP’s own specification is narrower than the generic HTTPS contract on purpose. A 400 from an OTLP endpoint means the payload was rejected, so retrying it cannot help and the batch is dropped and counted instead. 500 is terminal here too, which is where OTLP differs most from the other destinations: a generic HTTPS, Datadog, or Better Stack drain retries a 500, an OTLP drain does not. Only 429, 502, 503, and 504 are retried. A collector that answers 500 under load therefore loses those batches rather than slowing delivery down — if yours does that, make it answer 503 while it sheds load. A 200 carrying partialSuccess.rejectedLogRecords is treated as delivered and the attempt is marked errorClass: "partial"; the refused records are added to the drain’s health.rejectedCount24h. A partialSuccess naming zero refused records is a warning, not a rejection, and leaves a plain ok attempt behind. Partial success is never retried — that is what the OTLP specification asks for, and re-sending would duplicate the records the collector did accept.

Test it with curl

Prove the endpoint and the credential before you create the drain. This is the exact shape a delivery has, minus the gzip:
A healthy collector answers 200 {"partialSuccess":{}}. A 401 means the header name or value is wrong; a 404 usually means the path is missing /v1/logs. Gzip it to test the real thing, since every delivery is compressed:

A collector to receive it

A ready-to-run collector configuration — OTLP/HTTP receiver with bearer auth, a batch processor, and an exporter — is on OpenTelemetry Collector config, together with the Loki and debug exporters to start from. The Grafana dashboard is built on what that pipeline produces.

If it isn’t working

The endpoint needs the full logs path. https://otel.example.com is not enough: use https://otel.example.com/v1/logs. Nothing is appended to what you enter.
Your endpoint is answering 400 or 500. Under the OTLP contract both are terminal — a 400 says the payload was rejected, so re-sending it cannot help, and 500 is outside the four statuses the specification lets a client retry. Check the collector’s log for the reason; a strict downstream that refuses unknown attribute keys is the usual cause of a 400, and the additive-change policy means new attribute keys will keep appearing. For a collector that is merely overloaded, answer 503 instead of 500 and the batch is retried.
service.name is the agent’s name and service.instance.id is its id. If a processor in your pipeline overwrites service.name, downstream grouping collapses every agent into one service. Nothing in the collector needs to set service.name for Lua records — it is already right.
The batch counted as delivered and is not retried. lua drains deliveries <id> --json marks each such attempt errorClass: "partial", and lua drains status <id> --json carries the running total under health.rejectedCount24h. A steady rejection rate is usually an attribute-limit or a body-size rule in the backend behind your collector.

Next steps

OpenTelemetry Collector config

A receiver, processor, and exporter pipeline to paste.

Grafana dashboard

Panels over the fields this drain produces.

Event schema

The full field-by-field OTLP mapping.

Delivery guarantees

Retry, drops, heartbeat, and health states.