No description
  • Python 66.6%
  • JavaScript 11.2%
  • HTML 10%
  • Shell 7.2%
  • CSS 5%
Find a file
2026-06-09 11:30:50 +07:00
app initial commit 2026-06-09 11:30:50 +07:00
config initial commit 2026-06-09 11:30:50 +07:00
freebsd initial commit 2026-06-09 11:30:50 +07:00
static initial commit 2026-06-09 11:30:50 +07:00
templates initial commit 2026-06-09 11:30:50 +07:00
tests initial commit 2026-06-09 11:30:50 +07:00
.gitignore initial commit 2026-06-09 11:30:50 +07:00
README.md initial commit 2026-06-09 11:30:50 +07:00
requirements.txt initial commit 2026-06-09 11:30:50 +07:00

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: env ZMON_SESSION_SECRET, else auto-generated config/secret.key.
  • config/nodes.yamlnodes: list (name, host, port, groups, key). The keys: map and per-node key are 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 httpapi noauth IP 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's key: (from the keys: map). Matches the node's httpapi.erl ECDSA scheme: keyid=base64(sha1(pubkey)), a strictly increasing seq, and sig over METHOD: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 /upstreams for 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"]}]}.