Architecture
Everything in jaque, from the dashboard to Livestatus to the notifier, is a fold over the same append-only event log. Nothing downstream of the state machine reaches in and mutates a status field directly — it reads events from the log at its own pace and builds whatever view it needs from them.
checks (tcp, http, dns, icmp, tls, snmp, legacy, wasm, command) | vscheduler --> executors --> state machine (pure fold) | v event log (memory | pebble | nats) | +---------------+---------+---------+-------------+ | | | | | v v v v v projections notifier perfdata archive (replay -> (UI, API, (adapters) sinks (segments) rebuild any livestatus) (clickhouse, projection) remote_write, http, exec, file, otlp)Scheduler
Section titled “Scheduler”sched/ decides when the next check for each object runs. Heap is the
baseline Scheduler implementation (ADR-004): a min-heap keyed by due
time, with an ID→index map so cancelling or rescheduling an entry is
O(log n) instead of a scan. To avoid every object on the same interval
firing in the same instant, Jitter derives a deterministic offset in
[0, interval) from a hash of the object’s ID — not a random draw, so the
same config reproduces the same schedule across restarts and replay. A
timer wheel is the documented alternative, but only lands if a benchmark
shows the heap is the bottleneck.
Executors
Section titled “Executors”exec/ runs whatever the scheduler hands it under global and per-host
concurrency limits, via a worker Pool. Every check type — native
(tcp, http, dns, icmp, tls, snmp), legacy Nagios plugin exec,
WASM, and command — implements the same Runner interface and is
dispatched through that one pool, so the scheduler and the pool never know
which kind of check they’re running.
The state machine
Section titled “The state machine”state/ is where a check result becomes a decision. Transition(state, event, clock) -> (state', effects) is pure: no I/O, no goroutines, no
side effects taken directly — only a new state and a list of effects as
data for the runtime to interpret. That purity is what lets soft/hard
transitions, flap detection and reachability be property-tested against
thousands of random event sequences without starting a single goroutine.
The event log
Section titled “The event log”eventlog/ is the transport every domain event flows through (ADR-002):
AcknowledgementSet, StateChanged, NotificationSent, and everything
else land here, never as a direct mutation. The log runs over one of three
transports selected with -eventlog: memory (no durability, replay only
from process start), file:// (embedded Pebble, durable, no external
service), or nats:// (external NATS JetStream, for clustered
deployments where more than one process reads the same log). Because
nothing mutates state outside the log, every projection is rebuildable by
replaying it from the start.
Projections
Section titled “Projections”projection/ folds eventlog.Event into the per-object read model:
Fold is a pure (Object, Event) -> Object, and Table wraps it behind
a concurrency-safe map that implements eventlog.Applier. This is the
same shape the dashboard, the query API, and Livestatus each read from —
one fold, several consumers at their own read position.
The notifier
Section titled “The notifier”notifier/ and notify/ split evaluation from delivery. notify/ holds
the decision logic — contacts, policies, escalations, windows — run by
the owning engine, which appends NotificationRequested to the log.
notifier/ never decides on the log, only reacts to it: a Consumer
folds notification events and calls a dispatcher for whatever it owns and
still has open, so a notifier only delivers, it never evaluates.
sink/ gets every check’s perfdata to wherever it’s supposed to end up,
via seven types: clickhouse, remote_write, archive, http, exec,
file, and otlp. Each is declared as a named entry under sinks: in
CUE, typed by type the same way checks and contacts are, and each reads
the log independently — a slow sink falls behind without blocking a fast
one.
The -target flag
Section titled “The -target flag”All of the above lives in one binary. -target picks which role this
process plays: all (everything, the default), engine (scheduler,
executors, state machine), worker (executors only, pulled from a work
queue), ui (dashboard and API, no execution), sink, or notifier.
Splitting roles across processes is a deployment choice, not a different
codebase — the same packages run either way.
Further reading
Section titled “Further reading”The decisions behind this shape, and why each one was made, live in
Design decisions, which points at the full ADRs in
docs/adr/.