- Python 66.6%
- JavaScript 11.2%
- HTML 10%
- Shell 7.2%
- CSS 5%
| app | ||
| config | ||
| freebsd | ||
| static | ||
| templates | ||
| tests | ||
| .gitignore | ||
| README.md | ||
| requirements.txt | ||
zmon-py
FastAPI rewrite of the zmon monitoring dashboard. Polls one or more zzcam/gscam nodes over HTTP and pushes live updates to the browser via SSE.
Replaces the old Erlang/Nitrogen ../zmon. Shows per node: online status,
connections/users, request rate, success rate, memory, uptime, version. Drill in
to a node to view its connected users and their connections (user, server,
IP, client id, state — with a live filter) and its upstreams, and to reload its
config. (No update/upgrade button — the license/update server is gone.)
The connections viewer is at /node/{name}/sessions (also reached by clicking the
conns/users count on the dashboard); it reads the node's /zzapi/sessions.
Layout
app/ FastAPI backend (config, poller, sse, routes, auth, ...)
config/ config.yaml, nodes.yaml, users.yaml (keep 0600 — secrets)
templates/ Jinja2 pages
static/ vanilla JS (EventSource) + CSS
tests/ unit tests
Run (development)
source ~/.venv/bin/activate # or: python -m venv .venv && pip install -r requirements.txt
uvicorn app.main:app --reload --port 8080
Open http://127.0.0.1:8080 — log in (default admin / admin; change it via
the password page). A user only sees nodes whose groups intersect theirs.
Configuration
config/config.yaml— bind/port,poll_interval,request_timeout. Session secret: envZMON_SESSION_SECRET, else auto-generatedconfig/secret.key.config/nodes.yaml—nodes:list (name,host,port,groups,key). Thekeys:map and per-nodekeyare kept for the future central/ECDSA mode and ignored in local mode.config/users.yaml— writable; passwords stored only as bcrypt hashes (legacy{sha1}accepted and auto-upgraded on login). Changed via the dashboard.
ZMON_CONFIG_DIR overrides the config directory.
Authentication to nodes (per node, auth: in nodes.yaml)
auth: none— plain HTTP; the node admits the dashboard via its httpapinoauthIP allowlist (e.g.{noauth, ["127.0.0.1"]}). For local/co-located nodes.auth: ecdsa— the dashboard signs every request with the ECDSA key named by the node'skey:(from thekeys:map). Matches the node'shttpapi.erlECDSA scheme:keyid=base64(sha1(pubkey)), a strictly increasingseq, andsigoverMETHOD:PATH:<seq:64-big>(ecdsa/sha256/ secp256k1). The "default" key is pre-enrolled on every node, so it works out of the box; for a custom key, enroll its public key on the node. For remote nodes reached over the network.
Groups
Both users and nodes carry a groups list. A user sees a node iff their groups
intersect the node's groups (enforced on the snapshot, the SSE stream, and every
per-node API/page). The dashboard shows a Group column and sorts rows by
group then name.
Telegram notifications
Each user configures their own bot token (Account page) and, per node, a
chat_id plus thresholds. Notifications are per-user, so one node can alert
several admins with different conditions. Alerts are edge-triggered: one
message when a condition becomes bad (🔴) and one when it clears (🟢) — no spam.
The poller evaluates conditions every cycle and sends via each user's bot.
Conditions (blank = disabled):
- offline ≥ X s — node unreachable for X seconds
- success < Y % — request success rate below Y
- conns > A / conns < B — connection count out of range
- upstream up ≥ C min then down ≥ D s — an upstream that had been connected
for C minutes is now down for D seconds (this condition makes the poller also
fetch
/upstreamsfor that node)
Get your chat id by messaging the bot, then calling
https://api.telegram.org/bot<token>/getUpdates. (A bot cannot message itself —
the chat id is your user/group id, not the bot's.) Tokens are stored in
users.yaml; treat that file as a secret (mode 0600). No extra dependency —
delivery uses httpx.
Tests
python -m pytest tests/ -q
URL prefix / reverse proxy
The app is served under a configurable prefix (base_path in config.yaml,
default /zmon). Every route, static asset, redirect, and JS fetch lives under
that prefix, and the session cookie is scoped to it (so it won't clash with the
parent site). In standalone mode / 307-redirects to the prefix. Set
base_path: "" to serve at the root.
Point your existing HTTPS site at the app, preserving the /zmon/ prefix
(the app expects the full path; no rewriting):
nginx:
location /zmon/ {
proxy_pass http://127.0.0.1:8080; # no trailing path -> prefix preserved
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE (/zmon/events): stream, don't buffer
proxy_buffering off;
proxy_read_timeout 1h;
}
Caddy:
reverse_proxy /zmon/* 127.0.0.1:8080 {
flush_interval -1 # stream SSE
}
Deployment (FreeBSD)
No venv or pip needed — all deps are in pkg. From the repo:
rsync -a ./ root@host:/tmp/zmon-py/ # copy the tree
ssh root@host 'sh /tmp/zmon-py/freebsd/install.sh'
install.sh (idempotent): pkg installs the deps (py311-fastapi,
py311-uvicorn, py311-httpx, py311-bcrypt, py311-Jinja2, py311-multipart,
py311-pydantic2, py311-pyyaml, py311-itsdangerous, py311-sse-starlette,
py311-uvloop, py311-httptools), creates the zmon user, installs the app to
/usr/local/share/zmon, config to /usr/local/etc/zmon (existing files kept,
secret.key generated, dir 0700), and the rc.d script. Then:
# edit /usr/local/etc/zmon/{nodes,users}.yaml, then:
sysrc zmon_enable=YES
service zmon start # logs -> /var/log/zmon.log
Access (bound to 127.0.0.1) via SSH tunnel, or behind the reverse proxy above.
The node(s) being polled must allow the dashboard via their httpapi noauth
allowlist, e.g. {sb2_api, httpapi, [40004], [{users,[]},{noauth,["127.0.0.1"]}]}.