Skip to main content
Metrics, logs and the audit log stay on the control plane and its nodes by default. Data export sends copies to the tools you already run: Grafana and Prometheus, Loki, a syslog server, a SIEM webhook, an S3 compatible bucket, or an OpenTelemetry collector. Nothing is exported until you add a destination, and a destination that is down never slows a container, a deploy or the API.

What you can export

Manage destinations from Settings, Data export in the dashboard or with levelrail-cli export destinations.

Prometheus scrape

GET /metrics returns the Prometheus text format. It is gated like every read route: give Prometheus a token with the read ability as a bearer credential.
Cardinality is bounded. The only labels are app, database and node (one per series, from the resource the sample belongs to) and destination on the exporter’s own series, so the series count tracks what you have deployed and what you configured, not request traffic. The prefix is the lowercase brand short name followed by an underscore, levelrail_ on a stock install. A token limited to some apps only sees those apps and databases, and no node or platform series.

Loki and Grafana quick start

  1. Run Loki and Grafana wherever you like. For a trial on one host:
  2. Loki on the same host or a private network is an internal address, which export refuses by default. On the control plane set APP_EXPORT_ALLOW_PRIVATE_NETWORKS=true and restart (see Environment variables). A public HTTPS Loki needs nothing.
  3. Add the destination:
    For Grafana Cloud or a multi-tenant Loki add --header X-Scope-OrgID=tenant or --header "Authorization=Basic <base64>". Header values are stored encrypted and are never returned by the API.
  4. In Grafana add a Loki data source pointing at http://loki:3100, then query {job="levelrail", app="web"}.
Loki streams carry the labels job, app, kind (app or database), stream (stdout or stderr) and level, plus any --label you set. level is the structured level or severity field when the line is JSON, otherwise error for stderr and info for stdout.

Log destinations

Filters apply per destination: --app (repeatable, empty means every app) and --min-level (debug, info, warn, error). Database containers are exported only by destinations with no --app filter.
  • HTTP JSON lines: one JSON object per line: time, kind, app, stream, level, message and fields for structured lines. Content-Type: application/x-ndjson.
  • Loki: an endpoint with no path gets /loki/api/v1/push appended.
  • Syslog: tcp://host:port or tls://host:port, RFC 5424 with octet counting framing, facility local0. TLS verifies the server certificate against the system roots.
Log export reuses the log stream the live tail already reads: it adds a subscriber, never a second Docker reader.

Audit log export

Streaming sends each new audit row at least once. Each batch is NDJSON, and webhook batches carry these headers (the prefix follows the brand name): The signature is HMAC-SHA256 over <unix>.<raw body> with your key. Verify it and reject timestamps outside a few minutes:
audit_s3 writes each batch to <prefix>/<yyyy>/<mm>/<dd>/<first-created-at>-<first-id>.ndjson in a storage destination. The key is derived from the batch’s first row, so a retried batch overwrites itself. Tokens, bearer values, URL credentials and token-shaped strings are masked in every exported field. By default streaming starts at the moment the destination is created. --backfill starts at the oldest audit row instead.

Delivery semantics

  • A cursor (timestamp and row id) is saved after every successful batch, so a restart resumes where it stopped.
  • Delivery is at least once. A crash between a receiver accepting a batch and the cursor being saved sends that batch again, with the same id range.
  • A failing destination holds the cursor and retries. Rows are never skipped, and the audit log itself is the buffer.

On demand download

from is inclusive and to exclusive. The API route is GET /api/v1/audit-log/export?from=&to=&format=ndjson|csv and needs the root ability. A single download is capped at APP_EXPORT_AUDIT_MAX_ROWS.

OTLP metrics

A metrics_otlp destination pushes the same series as the scrape endpoint as OTLP/HTTP JSON (/v1/metrics is appended when the endpoint has no path). Counters are cumulative monotonic sums, everything else gauges. The interval defaults to APP_EXPORT_METRICS_INTERVAL and can be set per destination with --interval (10 to 3600 seconds).

Reliability and safety

  • Every destination has an in-memory queue of APP_EXPORT_QUEUE_LINES entries. When it fills, the oldest entries are dropped and counted. There is no disk buffer, so a control plane restart loses queued log lines but not audit rows.
  • Container log lines are handed to exporters through a non-blocking subscription. A slow destination cannot slow a container, the log store, a deploy or the API.
  • A batch is retried with exponential backoff and jitter up to APP_EXPORT_MAX_ATTEMPTS times. Responses in the 4xx range other than 408 and 429 are not retried, because retrying cannot fix them.
  • After APP_EXPORT_BREAKER_FAILURES consecutive failed batches a destination backs off for APP_EXPORT_BREAKER_COOLDOWN, then tries one batch.
  • Endpoints are validated when saved and again at every connection, including redirects: private, loopback and link-local addresses are refused unless APP_EXPORT_ALLOW_PRIVATE_NETWORKS=true. Endpoints cannot embed credentials.
  • Headers and signing keys are stored with envelope encryption and are never returned. The API shows header names and whether a key is set.
  • Each destination shows its last success, last error, queue depth, dropped and failed counts, and breaker state, in the dashboard, with levelrail-cli export destinations status <id>, and as export_* metrics.

CLI

--header and --hmac-key values end up in your shell history. Prefer a script that reads them from a secret store.

Next steps

Observability

Node-local metrics and the remote read endpoint.

Log archive

Scheduled, compressed log archives in a bucket.
Last modified on October 12, 2026