GET /v3/jobs/{job_id}/logs serves per job — to an HTTPS endpoint you run, in batched POSTs, as they happen. Use it to feed your observability stack (Datadog, an OpenTelemetry collector, your own service) without polling each job for its logs.
Each drained event is one of the job’s customer-visible lifecycle records: queued, started, moderation.passed, provider.submitted, provider.error, retry.scheduled, progress, finalizing, download.ready, completed, failed, or recovered, with a level of info, warning, or error.
Create a drain
formatpicks the wire encoding:ndjson(the default) orotlp.secretsigns every NDJSON post and is required whenformatisndjson(64–4096 characters). It is write-only — reads never echo it back.headers(optional) are extra headers sent with every post — typically your receiver’s authentication. Also write-only: reads expose onlyheader_names.batch_sizecaps log lines per post (1–5000, default 1000).
NDJSON format
NDJSON posts arrive withContent-Type: application/x-ndjson — one JSON object per line, newline-terminated. Fields that are null are omitted.
Every NDJSON post carries
X-Hedra-Signature: the hex-encoded HMAC-SHA256 of the raw request body, keyed with the drain’s secret. Verify it before trusting a batch:
OTLP format
Withformat: "otlp", each batch is posted as one binary-protobuf ExportLogsServiceRequest (Content-Type: application/x-protobuf) — the encoding every OTLP/HTTP logs receiver must accept. Point url at your receiver’s logs path, for example an OpenTelemetry Collector’s https://collector.example.com:4318/v1/logs, and put the receiver’s authentication in headers:
timestamp maps to the record’s time, level maps to severity (INFO, WARN, ERROR), message is the body, and the hedra_* fields arrive as record attributes (hedra_job_id, hedra_model, hedra_event, hedra_source, …), so you can filter and group on them in your backend. A secret is optional for OTLP drains — receivers authenticate with headers instead.
Delivery and failure handling
Delivery is at-least-once: a batch your endpoint doesn’t acknowledge with a2xx is requeued and delivered again, so deduplicate on hedra_log_id if double-processing matters to you. Redirects are never followed.
Each failed batch increments the drain’s consecutive_failures; any successful batch resets it to zero. After five consecutive failures the drain auto-disables: enabled flips to false and disabled_reason reports consecutive_failures (a drain you paused yourself reports disabled_by_user). Re-enabling clears the counter:
last_failure_at, last_failure_status, and last_error describe the most recent failure. last_error is a structured error envelope — a stable code (DEADLINE_EXCEEDED, UNAVAILABLE, RESOURCE_EXHAUSTED, UNAUTHORIZED, and so on, the same vocabulary the rest of the API uses), a fixed message, and retryable, which tells you whether fixing the destination and re-enabling is likely to help. FAILED_PRECONDITION is the one that won’t recover on its own: the URL resolves to a blocked address range — fix the URL. Your destination’s URL, headers, credentials, and response body are never echoed back (and the URL is never written to Hedra’s own logs), so treat your own logs as the record of what your endpoint returned.
Manage a drain
PATCH /v3/log-drains/{drain_id} updates any subset of fields; omitted fields are unchanged.
- Rotate the secret by sending a new
secret. Switchingformattondjsonon a drain with no stored secret requires supplying one in the same request. - Replace headers by sending
headers— it replaces the full set, and{}clears it. - Pause and resume with
enabled. Pausing keeps the configuration; deleting the drain (DELETE /v3/log-drains/{drain_id}) removes it.
GET /v3/log-drains lists every drain with its health fields (consecutive_failures, last_success_at, last_failure_at, disabled_reason), which makes it a natural target for a periodic health check on your side.