Log drains
A log drain sends every line your apps write to stdout and stderr to the logging tool your team already uses, a few seconds after it's written. Search it, alert on it and keep it for as long as that tool keeps it. Log drains are free on every plan.
You can send to:
- Any OpenTelemetry (OTLP) endpoint: Grafana Cloud, Honeycomb, SigNoz, New Relic, your own OpenTelemetry Collector
- Datadog
- Axiom
- Better Stack
- Any HTTPS endpoint that accepts newline-delimited JSON
Add a drain
A drain belongs to the team, so team Owners and Admins set them up.
- Open Team settings and scroll to Log drains (or press Set up a log drain under an app's Logs tab).
- Pick where to send, then fill in the endpoint and key (see the guides below).
- Leave Apps on All apps in the team, or pick one app to send only its lines.
- Press Add log drain, then Send test line. velixir sends one line straight to the destination with your key and shows the status code it answered (for example
401 Unauthorized), so a wrong key or URL shows up straight away. The response body isn't shown; check your tool's own logs for the detail.
New lines start arriving within a couple of minutes, once every region has the new settings. Pause stops sending without losing the settings; Delete removes the drain and its saved key.
Keys and header values are stored encrypted and never shown again; the list shows the last four characters so you can tell keys apart. To change a key, use the API or delete the drain and add it again.
Grafana Cloud (OpenTelemetry)
- In the Grafana Cloud portal, open your stack and choose OpenTelemetry, then Configure.
- Copy the OTLP endpoint (it looks like
https://otlp-gateway-prod-eu-west-2.grafana.net/otlp) and generate an API token. - Grafana shows an
Authorizationheader built from your instance ID and the token (Basicfollowed by a base64 string). Copy the whole header. - In velixir, pick OpenTelemetry (OTLP), paste the endpoint, and paste the header into Headers:
Authorization: Basic MTIzNDU2OmdsY19leUo...
You don't need to add /v1/logs: velixir appends it unless the URL already ends with it. Your logs appear in Grafana's Logs (Loki) with service_name set to the app.
The same works for any OTLP/HTTP receiver: paste its endpoint, and put its key in Bearer token or in Headers (for example x-honeycomb-team: your-key for Honeycomb).
How lines map to OpenTelemetry
Each line becomes one log record, sent as OTLP/HTTP JSON:
| OpenTelemetry field | Value |
|---|---|
service.name (resource) |
The app's name in its URL, e.g. shop |
deployment.environment.name (resource) |
The slot: production, staging, ... |
service.instance.id, k8s.pod.name (resource) |
The instance that wrote the line |
velixir.app.id, velixir.deployment.id, velixir.team.id, velixir.process (resource) |
velixir's ids, and the process type when the app has them |
body |
The line, exactly as written |
severityText, severityNumber |
From the line's level, when it's JSON with one (see Levels) |
log.iostream (attribute) |
stdout or stderr |
Datadog
- In Datadog, open Organization settings, then API keys, and create a key (an API key, not an application key).
- In velixir, pick Datadog, choose your Datadog site (the domain you sign in to, such as
datadoghq.euorus5.datadoghq.com) and paste the key.
Lines arrive in Logs with source:velixir, service set to the app, status from the line's level, and the tags env:<slot>, process:<type> and velixir_app_id:<id>.
Axiom
- In Axiom, create a dataset (for example
velixir), then under Settings, API tokens, create a token that can ingest into it. - In velixir, pick Axiom, enter the dataset and the token.
If your Axiom organisation is in a region with its own API URL, enter that URL too; otherwise leave it empty.
Better Stack
- In Better Stack, open Telemetry, then Sources, and connect a new source of type HTTP.
- Copy the source token and the ingesting host (it looks like
s1234567.eu-nbg-2.betterstackdata.com). - In velixir, pick Better Stack, enter
https://followed by the ingesting host, and paste the token.
Any HTTPS endpoint
Pick HTTPS endpoint (NDJSON) for your own collector, a serverless function, or a tool not listed here. velixir POSTs batches of lines to the URL as newline-delimited JSON (Content-Type: application/x-ndjson), one record per line. Add a Bearer token, or up to 10 Headers such as an API key header.
Answer with any 2xx status. Batches are sent at least every 2 seconds and are at most about 1 MB. A failed request (a 5xx, a 408, a 429 or a timeout) is retried; any other 4xx is not, so check the endpoint with Send test line first.
The record
OTLP aside, every destination receives each line as this JSON object:
{
"timestamp": "2026-10-31T10:00:00.123456789Z",
"message": "GET /checkout 200 in 41ms",
"stream": "stdout",
"level": "info",
"app": "shop",
"app_id": "8c6f0b3e-...",
"slot": "production",
"process": "web",
"deployment_id": "5d2a9c11-...",
"team_id": "1b7e4f20-...",
"pod": "shop-production-7d9c8-x2k4p",
"container": "app",
"namespace": "vx-shop"
}
| Field | What it is |
|---|---|
timestamp |
When the line was written (UTC, nanoseconds) |
message |
The line, exactly as written. JSON lines stay a string, so nothing you log is lost or reshaped |
stream |
stdout or stderr |
level |
See Levels. null when the line has none |
app, app_id |
The app's name in its URL, and its id |
slot |
production, staging, or the slot the line came from |
process |
The process type (web, worker, ...) when the app defines them, else null |
deployment_id |
The deploy that started this instance |
team_id |
Your team |
pod, container, namespace |
The instance that wrote it. Several instances of one app each have their own pod |
Datadog drains also get ddsource, service, host, status and ddtags; Better Stack drains get dt.
Levels
When a line is a JSON object, velixir reads its level from the first of level, severity, lvl, log_level, @l or levelname it finds, in lower case. Numeric levels from pino and bunyan become names (30 is info, 50 is error). Plain-text lines have no level; most tools still let you search the message for ERROR.
What's sent, and what isn't
- Sent: everything your app's instances write to stdout and stderr, in every slot. A drain for all apps also gets your pull request previews, which are apps of their own.
- Not sent: build output (it's on the deploy's page), HTTP requests the app served (those are access logs, with their own API), and logs from managed databases and caches.
Delivery
- Speed. Lines usually arrive within a few seconds.
- When your endpoint is down, lines wait on each server in a buffer of up to 64 MB per drain (less on a busy server, never under 16 MB), and are sent when it recovers. Beyond that the newest lines are dropped for that drain only, so one unreachable destination never slows your apps or your other drains.
- Up to 5 drains per team. Each one gets every matching line, so you can send the same logs to two tools while you move from one to the other. Teams waiting to be claimed can't add drains.
- Settings the log collector rejects. If a drain's settings stop the collector from starting, velixir puts the previous working settings back for everyone and pauses that drain with a note. Check its URL, token and headers, then save it again to retry.
Security
- Endpoints must be
https://addresses on the public internet. velixir refuses URLs that are IP addresses or that resolve to private addresses or to velixir's own servers, and checks again before every change reaches the servers. - URLs can't contain spaces or any of
{ } | ^ " \; percent-encode them (a space is%20). Tokens and header values must be plain ASCII (header values may contain spaces). - A drain only ever receives lines from its own team's apps.
- Keys and header values are encrypted at rest, sent only to the destination you entered, and never shown again in the dashboard, API or CLI.
API and CLI
velixir drains # list the team's drains
velixir drains add --type datadog --site datadoghq.eu --token $DD_API_KEY
velixir drains add --type otlp --url https://otlp-gateway-prod-eu-west-2.grafana.net/otlp \
--header "Authorization: Basic MTIzNDU2OmdsY19..." --name grafana
velixir drains add --type axiom --dataset velixir --token $AXIOM_TOKEN --app <app-id>
velixir drains test <id> # send one test line now
velixir drains remove <id>
The same through the API, with an account-wide key whose owner is a team Owner or Admin:
| Call | What it does |
|---|---|
GET /api/v1/log-drains |
List the team's drains |
POST /api/v1/log-drains |
Add one: type, name, url, token, headers, site, dataset, appId. With an agent key it's saved paused and starts sending once a person approves it |
PATCH /api/v1/log-drains/{id} |
Change only what you send: enabled, name, url, a new token or headers, appId, or allApps: true. With an agent key, anything except renaming or pausing waits for a person to approve it |
POST /api/v1/log-drains/{id}/test |
Send one test line and return the destination's status code |
DELETE /api/v1/log-drains/{id} |
Delete it. With an agent key this waits for a person to approve it (see API keys) |