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
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.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 onelua.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.
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 oneresourceLogs 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: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
404 on the test post
404 on the test post
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.Everything is dropped with no retries
Everything is dropped with no retries
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.Records arrive but the service name is wrong
Records arrive but the service name is wrong
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.partialSuccess reports rejected records
partialSuccess reports rejected records
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.

