A KISS monitoring service: one static binary, one YAML config file, one SQLite file.
Gjallar watches your services, shows a status page with history (black & red, HTMX-refreshed), and alerts you when something goes down — and when it recovers.
| Type | What it checks |
|---|---|
http |
HTTP/HTTPS request: status code (expect_status), body regex, TLS certificate expiry (cert_expiry_warn) |
postgres |
SQL query result against a rule (pure-Go pgx driver) |
oracle |
SQL query result against a rule (pure-Go go-ora driver, no Oracle client needed) |
ping |
ICMP echo (privileged or unprivileged) |
redis |
TCP connect + optional AUTH + PING/+PONG |
elasticsearch |
freshness: hours since max(timestamp_field) in an index, checked against a rule |
prometheus |
Fetches a /metrics route and evaluates a rule against metric values, with optional label selectors |
Rules: > N, >= N, < N, <= N, == x, != x, ~ regex, rows > 0 (row count).
- Any shoutrrr URL: Telegram, email/SMTP, ntfy, Discord, Slack, Gotify, Pushover, generic webhooks, ...
- Free Mobile SMS API (
type: freemobile). - Signal via a signal-cli-rest-api
gateway (
type: signal:urlto/v2/send, sendernumber,recipients).
A monitor alerts after failure_threshold consecutive failures (no flapping noise),
and again on recovery. Open incidents survive restarts: no duplicate alerts.
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w"
cp gjallar.example.yaml gjallar.yaml # edit to taste
./gjallar -config gjallar.yamlThen open http://localhost:8080. The page auto-refreshes every 10 s.
Configuration reference: see gjallar.example.yaml —
every field is documented there.
Any config value may reference an environment variable with ${VAR} syntax:
dsn: "postgres://monitor:${PG_PASSWORD}@db1:5432/app"Startup fails with a clear error if a referenced variable is undefined. A bare
$ (e.g. in a ~ ^OPEN$ regex rule) is left untouched.
Gjallar reloads its configuration on SIGHUP (systemctl reload gjallar).
The new config is fully validated first — if it is broken, the running
configuration is kept and the error is logged.
Set enabled: false on a monitor to stop checking and alerting on it while
keeping it on the status page, shown with a grey DISABLED badge and
excluded from the group's up/total count. Omitting enabled (or true)
leaves it active.
- name: "legacy-api"
type: http
url: "http://legacy/health"
enabled: false # listed as disabled, never checkedGive monitors an optional group: "Hyperion" and the status page shows them
under a common header with an up/total count. Monitors without a group are
listed first, without a header. Groups appear in config order.
By default a monitor alerts once when it goes down and once when it recovers.
Set realert: 1h (globally in defaults or per monitor) to also get a
"still down" reminder at that interval during long outages.
Unprivileged ping (privileged: false, the default) needs:
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"Privileged ping (privileged: true) needs raw sockets:
sudo setcap cap_net_raw+ep ./gjallaror the systemd unit's AmbientCapabilities=CAP_NET_RAW (see
deploy/gjallar.service). Gjallar self-tests ping at
startup and exits with the fix if permissions are missing.
Check results and incidents live in a single SQLite file (database). Results
older than retention (default 30 days) are pruned hourly. Back up by copying
the file.
go test ./...Database integration tests are skipped unless pointed at a server:
docker run --rm -e POSTGRES_PASSWORD=secret -p 5432:5432 postgres:16
GJALLAR_TEST_PG_DSN="postgres://postgres:secret@localhost:5432/postgres?sslmode=disable" go test ./internal/check/
docker run --rm -p 1521:1521 -e ORACLE_PASSWORD=secret gvenzl/oracle-free:23-slim
GJALLAR_TEST_ORA_DSN="oracle://system:secret@localhost:1521/FREEPDB1" go test ./internal/check/- One goroutine per monitor → one results channel → one consumer (single SQLite writer, lock-free alert state machine).
- Everything embedded: templates, htmx, CSS, logo. No CDN, no CGO, no ORM, no router dependency.