Skip to content

Reading the logs

Where each service's log lives — self-host or Kubernetes — how to shape a JSON log line into something readable, and when the answer isn't in the logs at all.

Not every failure lands in the hub's log. Look here first:

Symptom Look at
the hub won't start, or 500s everywhere curl -s http://localhost:8080/q/health, then the hub log
login fails, token rejected the keycloak log
a run failed, rows are missing lm run get and lm dlq — not the logs
a webhook never arrived lm outputs list
https or certificate trouble the caddy log (TLS proxy)
slow queries, connection refused the postgres log

/q/health names the failing check with DOWN: Observability. Which restart is safe: the on-call runbook.

Where the logs live

Self-host stack (Docker Compose)

The stack started by deploy/selfhost/up.sh has three services: hub, postgres, keycloak (plus caddy with the TLS proxy). Run these from the repo root:

# Follow the hub live (Ctrl-C stops following, not the hub)
./deploy/selfhost/logs.sh hub

# Any docker compose logs flag: a time window, timestamps
docker compose -f docker-compose.selfhost.yml logs --since 10m -t hub

# Another service: keycloak, postgres
./deploy/selfhost/logs.sh keycloak

# Everything at once (noisy, but shows cross-service ordering)
./deploy/selfhost/logs.sh

caddy lives in the TLS overlay file; name both files to reach it:

docker compose -f docker-compose.selfhost.yml -f docker-compose.tls.yml logs -f caddy

Kubernetes (cloud edition)

The chart names its workloads <release>-hub and <release>-keycloak; the default release is lumnik:

kubectl logs -f deploy/lumnik-hub          # follow the hub
kubectl logs --since=10m deploy/lumnik-hub # recent window only
kubectl logs --previous deploy/lumnik-hub  # the crashed container, after a restart
kubectl logs -f deploy/lumnik-keycloak     # auth trouble

Add -n <namespace> if you installed into one.

Anatomy of a hub log line

The packaged hub (Docker, Kubernetes) writes one JSON object per line:

{"timestamp":"2026-08-18T09:41:24+02:00","level":"ERROR","loggerName":"io.lumnik.hub.connector…","threadName":"executor-thread-1","mdc":{"tenantId":"12","runId":"66541f10-0dc7-4534-85b0-942668020fb1"},"message":"chunk 4 failed: …"}
Field Meaning
level ERROR, WARN, INFO, DEBUG
loggerName the component; ingestion lines start with io.lumnik.hub.connector
mdc.tenantId the tenant the line belongs to
mdc.runId the run id (as in lm run list) — on every line a run wrote, absent elsewhere

Slice the stream with jq (--no-log-prefix strips compose's hub-1 | prefix; fromjson? // empty skips any non-JSON line):

LOGS="docker compose -f docker-compose.selfhost.yml logs --no-log-prefix"

# Errors only, human-readable
$LOGS hub | jq -rR 'fromjson? // empty | select(.level=="ERROR") | "\(.timestamp) \(.message)"'

# One tenant's story
$LOGS hub | jq -rR 'fromjson? // empty | select(.mdc.tenantId=="12") | .message'

# One run's story, warnings included — the id comes from `lm run list`
$LOGS hub | jq -rR 'fromjson? // empty | select(.mdc.runId=="66541f10-0dc7-4534-85b0-942668020fb1") | "\(.level) \(.message)"'

No jq on the box? grep '"level":"ERROR"' still works on JSON lines.

Turning up the volume

Env var Effect
QUARKUS_LOG_CATEGORY__IO_LUMNIK__LEVEL=DEBUG debug for lumnik code only — the usual choice
QUARKUS_LOG_LEVEL=DEBUG debug for everything, frameworks included — a firehose

Self-host. A line in .env does not reach the hub: it only gets the variables docker-compose.selfhost.yml lists. Add a small override file next to it instead:

# debug.yml
services:
  hub:
    environment:
      QUARKUS_LOG_CATEGORY__IO_LUMNIK__LEVEL: DEBUG
docker compose -f docker-compose.selfhost.yml -f debug.yml up -d hub

Also name every overlay file you started with (-f docker-compose.tls.yml), or the hub restarts without it. A later up.sh keeps debug.yml too. To turn it back down, run the same command without -f debug.yml.

Kubernetes (cloud edition).

kubectl set env deploy/lumnik-hub QUARKUS_LOG_CATEGORY__IO_LUMNIK__LEVEL=DEBUG

Turn it down with kubectl set env deploy/lumnik-hub QUARKUS_LOG_CATEGORY__IO_LUMNIK__LEVEL-.

Turn it back down

DEBUG is for the minutes you are actively looking. It drowns the signal, and the log may echo data you would rather not keep.

When the answer is not in the logs

Missing rows and failed runs are answered by the run ledger and the dead-letter queue, not by log lines: Diagnosing a failed run. Outbound deliveries: lm outputs list. In the TUI: :runs, :dlq and :outputs.