- Go 77.5%
- JavaScript 10.4%
- HTML 4.6%
- CSS 4%
- Shell 2.7%
- Other 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
POST /mcp lets an assistant read nodes, states, alerts, one device's logs, metric history and the dependency graph. Stateless JSON-RPC, authenticated by a bearer token made in Settings -> Account (only its SHA-256 is stored, in settings, so no migration). No token means 401; a request with an Origin header is refused. RAM is bounded per call: two concurrent calls at most, a ceiling on rows and points per tool, log messages cut at 400 characters unless max_chars asks for more (marked truncated; get_log_line reads one whole), and a reply over 64 KiB is refused with a hint to narrow the query. Collector config is never returned. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
| .forgejo/workflows | ||
| cmd/beacon | ||
| deploy | ||
| internal | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| FABLE_AUDIT.md | ||
| go.mod | ||
| go.sh | ||
| go.sum | ||
| README.md | ||
Beacon
Monitoring for one homelab, in one Go binary.
Beacon runs as a container on a MikroTik RB5009 and replaces LibreNMS, Gatus and Beszel: device and service inventory with an explicit dependency graph, polling over each device's own native protocol, syslog collection, and deduplicated notifications to Gotify.
The dependency graph is the point. Every other tool here reported a host outage as ten unrelated alerts; Beacon reports it as one, naming the cause and the services it took with it.
- The design, and working on the code:
CLAUDE.md - Deploying it:
deploy/README.md
What it does
Inventory and graph. One list of devices and services, edited in the UI, with an explicit dependency graph over it: what runs on what, what the network path goes through, what the UPS powers, what Caddy publishes. Everything else is derived from it.
Collectors. probe for ICMP, TCP and HTTP with TLS expiry. agent for the
hosts that push. routeros over the REST API, esphome over the native
protocol, nut for the UPS, proxmox for guest and node state, pbs for the
two Proxmox Backup Servers, starlink for the dish, homeassistant for the one
machine no agent can run on, and http, which reads numbers out of a device's
own web page for gear with neither an API nor SNMP.
Ingestors. Syslog on UDP and TCP :514, routed to a node by source address,
with filter rules applied at the front door and editable in the UI. Agent pushes
over HTTPS with a per-agent token.
Alerting. A state machine with failure and success thresholds, correlation across the dependency graph so one outage is one notification naming the cause, and delivery to a single Gotify application. Plus a dead-man's switch: a ping to something outside the house on a timer, sent only while Beacon can still read its own database, so a hub that has died quietly stops saying it is fine.
Storage. beacon.db for the inventory and alert state, metrics.db with a
chunk encoding behind the charts, logs.db with an FTS5 index behind the Logs
page. beacon import-rrd migrates LibreNMS's RRD history into metrics.db.
The UI. Server-rendered and it keeps itself current, so a page left open on
a wall shows what is true now rather than what was true when someone last
pressed a key. It is one board — tiles, alerts, the dependency graph on a canvas
with a force-directed layout written for it, and the inventory by tag — and a
device opens as a panel over it, carrying its state, its charts, its logs, its
dependencies and its own definition. Framed by the dashboard named in
BEACON_FRAME_ANCESTOR, every page posts it the current number of warnings
(nodes degraded) and errors (nodes down) for the badge on Beacon's button.
Running it
The toolchain runs in a container, so no Go install is needed (a host Go 1.26+ works too):
./go.sh test ./...
./go.sh build -o beacon ./cmd/beacon
BEACON_DATA=./data BEACON_SYSLOG_ADDR= ./beacon # empty address: no privileged :514
Then open http://localhost:8080, set the password, add a node and draw its dependencies in the node's own panel. Nothing is served until that password exists. There is one account and it has no username: this is a homelab watched by one person, and a user table with one row in it was a login field to get wrong and a name to print in a corner nobody read.
The agent
Hosts that cannot be polled from outside run the same binary as an agent. Give
the node an agent collector in the inventory, set a token on it, and hand the
host that token:
BEACON_URL=https://beacon.exithub.net \
BEACON_TOKEN=the-token-from-the-node \
BEACON_SPOOL=/var/lib/beacon/spool \
./beacon agent
It pushes every 15 s and spools to disk when the hub is unreachable, oldest
dropped first. Where smartctl, zpool, systemctl or nvidia-smi are
present it reports what they know, it reads /proc/mdstat where the kernel has
md arrays and /sys/class/drm where there is an Intel GPU, and
where there is a Docker socket it also reports container
state; that is read-only, and Beacon never starts, stops
or updates anything. Nothing connects to the agent: it takes no commands and
opens no listener. deploy/beacon-agent.service
is the systemd unit.
Build and deploy
Forgejo CI vets, tests and lints the code, builds one multi-arch image
(linux/amd64, linux/arm64) for both roles, pushes it to
git.exithub.net/ivan.pavlina/beacon and the router's repull-tick scheduler repulls it. The agents
are rolled out by hand with deploy/agent-deploy.sh. Details are in
CLAUDE.md and deploy/README.md.
Migrating LibreNMS history
./beacon import-rrd --dir /data/librenms/app/rrd --dry-run
Each subdirectory of --dir is a device, matched to a node of the same name.
The dry run reports what maps, what does not, and which directories have no
node; run it first and read its output. The RRD reader is pure Go — no
rrdtool, no cgo — and reads the finest AVERAGE archive of each file. What has
no mapping is imported under librenms.* rather than dropped, so throwing away
a year of some sensor's history stays a decision someone makes while looking at
it.