Observability
Health probes, Prometheus metrics and OpenTelemetry tracing are wired in and live on every boot — no dashboards, alert rules, or RAG/chat metrics ship with it.
The hub ships with health probes, Prometheus metrics and OpenTelemetry tracing wired
in (quarkus-smallrye-health, quarkus-micrometer-registry-prometheus,
quarkus-opentelemetry). Nothing to install — the endpoints are live on every boot.
What's exposed out of the box
| Endpoint | What it answers | Auth |
|---|---|---|
GET /q/health |
overall UP/DOWN, all checks | none |
GET /q/health/live |
"the process is alive" — restart me if not | none |
GET /q/health/ready |
"I can serve traffic" (DB reachable, migrations done) — the Helm chart's readiness probe. The selfhost compose healthcheck (what up.sh waits on) watches /q/health/live instead; same boot window in practice, since the hub serves neither until Flyway finishes |
none |
GET /q/metrics |
Prometheus text format: JVM, HTTP server, DB pool + the domain metrics below | none |
GET /api/platform/health |
application-level: {"platform":"ok","modules":{...}} — one entry per installed module with its version |
lm_integrator |
The /q/* endpoints are standard Quarkus paths on the application port (8080) — the
hub does not re-root them. They carry no authentication: liveness/readiness are
harmless, but /q/metrics names your connectors in its labels. The TLS façade proxies
everything except /realms/* to the hub, so on a public origin /q/* is reachable
from the internet — block /q/* at the proxy or firewall if that bothers you (it
should).
/api/platform/health is the one that knows about lumnik: it walks the module
manifest registry and reports each module's version. It sits under /api/platform/*,
so prod requires a token with the lm_integrator role (dev-bypass waves it through
in %dev).
Scraping with Prometheus
Point a scrape job at /q/metrics:
scrape_configs:
- job_name: lumnik-hub
metrics_path: /q/metrics
static_configs:
- targets: ["your-hub-host:8080"]
That's the whole integration — no push gateway, no agent.
Domain metrics
Beyond the standard JVM/HTTP/pool meters, the hub emits:
| Metric | Tags | When it moves |
|---|---|---|
lumnik_runs_total |
status (Completed, Partial, Failed, Cancelled) |
a connector run reaches a terminal status |
lumnik_rate_limited_total |
bucket |
a request was refused 429 by rate limiting — the tenant is in the WARN log line, deliberately not in a tag |
lumnik_dead_letters_total |
reason |
a row is quarantined to the DLQ — real losses only; benign skips (duplicates, policy-excluded) do not count |
csv_filesource_list_total |
transport, connector |
files listed on a remote source |
csv_filesource_list_duration_seconds |
transport, connector |
time spent listing |
csv_filesource_open_total |
transport, connector, result (ok/error) |
a file open attempt |
csv_filesource_bytes_read_total |
transport, connector |
bytes downloaded from a remote file |
csv_filesource_after_process_total |
transport, action, result |
a post-ingest file action (move/delete/…) |
Two caveats:
- The run counters are best-effort, at-least-once. They increment inside the
run's transaction, so a rolled-back run may still have counted. The authoritative
loss signal is the DB-derived
deadLetterCounton the run itself, not the counter. - The
csv_filesource_*family fires only on the one-shot ingest path (lm csv ingest→POST /api/platform/sources/csv/ingest). Scheduled connector runs read files through a different path and do not emit them. If your CSV connector runs on a schedule, watchlumnik_runs_totaland the DLQ, notcsv_filesource_*.
Meters are registered lazily on first use — a fresh boot shows none of these until the corresponding thing has happened once. Absence means "hasn't happened", not "broken".
Scheduled background work
The hub breathes on eight clocks — every one a Quarkus @Scheduled tick with
concurrentExecution = SKIP (a tick that finds the previous one still running skips
its turn; the guarantee is per-JVM, i.e. per hub instance):
| Clock | Cadence | What it does |
|---|---|---|
| Connector scheduler | every 1 min | fires each connector whose spec.schedule cron is due — the ingestion breath itself |
| Workflow dormancy sweep | every 15 min¹ | watches every declared lifecycle for a document sitting unmoved past its threshold |
| Webhook delivery | every 5 s | sweeps the event outbox and delivers each event to the tenant's registered integrations (lm integration) |
| Webhook retry | every 30 s | re-attempts failed deliveries with backoff — the failure journal (lm outputs) feeds from here |
| Email delivery | every 5 s | sweeps the outbox and mails events matching notification rules (lm notify) — best-effort: a failed send is logged and skipped; mocked until the SMTP knobs are set |
| Process reactions | every 5 s | evaluates kind: Process rules against fresh events |
| Audit dispatch | every 2 s | flushes audit events to their sinks |
| Event retention purge | daily 04:30 | trims the outbox — never past what the slowest consumer has seen |
| Audit retention purge | daily 04:45 | trims core.audit_event past the retention window (default 365 d; 0 disables) |
Three outbox consumers (webhook, email, process) share one sweeping mechanism with a
per-consumer cursor in core.event_consumer, so each advances independently and the
retention purge never trims past the slowest of them. Audit dispatch is a separate
pipeline (core.audit_outbox → core.audit_event, a dispatched flag, the admin
datasource) — it has no cursor there, and the purge neither sees nor waits for it.
¹ The dormancy sweep is the only cadence exposed as a config knob today — the workflow dormancy sweep watches every declared lifecycle for a document stuck past its threshold.
lumnik.workflow.sweep.every=15m
LUMNIK_WORKFLOW_SWEEP_EVERY=5m
A shorter interval means an overdue document is noticed sooner — it does not change the
threshold itself (spec.dormancy.after, declared per workflow). Restart the hub after
changing either form; like the rate-limit knobs, env vars only reach the process at boot.
Traces to a collector
OpenTelemetry is compiled in but effectively off in the shipped stacks:
%devand%testdisable the SDK outright.- The self-host compose sets
QUARKUS_OTEL_SDK_DISABLED: "true"because the stack ships no collector — without it, every boot spamsConnection refused: localhost:4317retries.
To wire tracing, run a collector (OTLP/gRPC), then on the hub container:
QUARKUS_OTEL_SDK_DISABLED=false
OTEL_EXPORTER_ENDPOINT=http://your-collector:4317
and restart the hub (for selfhost: edit the hub environment in
docker-compose.selfhost.yml, then deploy/selfhost/up.sh). The endpoint default is
http://localhost:4317; only the traces exporter is configured — OTel metrics/logs
export is not set up.
Logs
Console logs carry the tenant in every line (%X{tenantId} in the format); %prod
switches to JSON console logging, ready for whatever log shipper you already run.
What does NOT exist yet
- No dashboards or alert rules ship with lumnik — the metrics above are a thin floor, bring your own Grafana.
- No per-connector tag on
lumnik_runs_total— it counts runs by status only. Partialis the status worth alerting on, more thanFailed: a failed run is loud and retried, while a partial one wrote everything it read and simply did not read everything there was. It means the reader stopped on its own budget, or the run began past records the source had already deleted under its stored position. The run'serror_messagenames which. For per-connector history, query the run ledger (lm runs, or theconnector.connector_runtable).- No row-count / duration metrics per run — those live on the run record in the DB, not in Prometheus.
- No RAG/chat metrics — token usage, ask-to-SQL guard verdicts and abstentions are not metered today.
- No OTel metrics or logs export — traces only.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
lumnik_runs_total / lumnik_dead_letters_total absent from /q/metrics on a fresh boot |
Meters register lazily on first use | Absence means "hasn't happened yet on this process", not broken — trigger a run, or check deadLetterCount on the run record directly, which is authoritative regardless of the counter. |
A scheduled CSV connector shows nothing under csv_filesource_* even though it's clearly ingesting |
That metric family only fires on the one-shot ingest path (lm csv ingest / POST /api/platform/sources/csv/ingest) — scheduled runs go through a different code path and don't emit it |
Watch lumnik_runs_total and the DLQ for a scheduled CSV connector instead. |
| No traces reach the collector, even with one running | %dev/%test disable the OTel SDK outright, and the self-host compose sets QUARKUS_OTEL_SDK_DISABLED: "true" by default (no collector ships with the stack) |
Set QUARKUS_OTEL_SDK_DISABLED=false and OTEL_EXPORTER_ENDPOINT on the hub container, then restart it. |
GET /api/platform/health returns 401/403 |
It sits under /api/platform/*, which requires a token with lm_integrator (dev-bypass only waves it through in %dev) |
Use a token carrying lm_integrator, or query the unauthenticated /q/health instead if you only need up/down. |
Changed lumnik.workflow.sweep.every (or its env form) but the sweep still runs on the old cadence |
Env vars, like the rate-limit knobs, are only read at process boot | Restart the hub after changing either form. |