The system monitor service
monitord runs once on every MixOS host, as its own unprivileged user. It
samples the system every second, keeps history, and answers questions about
the host on the Bus. It only reads. The reasons are in the
monitord decision.
Monitor, the task manager window, draws what monitord serves. Agents and scripts ask it directly.
Asking it
On the Bus, the service is monitord. Any node's monitord is reachable
over the mesh as monitord.<node>.bus:
send monitord monitord.snapshot
print($reply.cpu.busy)
send monitord.alpha.bus monitord.processes sort=memory limit=5
Every verb is a query: it changes nothing. A refusal comes back with rc
10 and a body whose status says why: bad_request, not_found,
unknown_verb, failed or busy.
| Verb | Body | Returns |
|---|---|---|
monitord.status | — | Version, uptime, sample interval, whether the process sampler is running, and the history database's size and retention |
monitord.snapshot | — | The system now (below) |
monitord.processes | sort, reverse, limit, filter, user, tree, kernel_threads | The process table |
monitord.process | pid | One process in full |
monitord.series | — | The history series held: live and stored |
monitord.history | series, from or since, to, max_points | Points for each series |
Unknown body fields are refused, so a typo is an error, not a silent default.
monitord.snapshot
Rates and percentages are over the last sample interval (interval_s).
Sizes are bytes; rates are per second.
host,ts(Unix seconds),uptime_s,boot_time.cpu:model,cores, andbusy,user,system,iowait,stealin percent;per_core, a list of the same per CPU withmhz;context_switches_ps,forks_ps.memory:total,available,used(total - available),free,cached,buffers,shared,dirty.swap:total,used,free.load:one,five,fifteen,runnable,tasks.pressure: forcpu,memoryandio, the share of time tasks stalled (some, andfullwhere the kernel reports it), each asavg10,avg60,avg300. Absent on kernels without PSI.tasks:total,running,blocked.disks: each whole disk withmodel,rotational,read_bps,write_bps,reads_ps,writes_psandbusy(percent of the interval with I/O in flight). Loop and RAM devices are left out.filesystems: each real filesystem once per device, withmount,device,kind,total,used,available.net: each interface but loopback, withstate,physical,speed_mbps,rx_bps,tx_bpsand the running totals.
A value monitord can't know is null, never zero.
monitord.processes
{"sort":"cpu", "limit":100, "filter":"firefox", "user":"alice", "tree":false}
sortiscpu(the default),memory,io,pid,nameoruser. Numbers sort largest first and names A to Z;reverseflips it.limitdefaults to 100; 0 means every row.filtermatches the name or command line, ignoring case.treeputs parents before children, siblings in sort order, each row with itsdepth. A filter keeps a match's ancestors, so the tree still joins up.- The kernel's own threads are left out unless
kernel_threadsis true.
The reply has total (rows that matched), count (rows returned) and
rows. Each row has pid, ppid, name, cmdline, user, uid,
state, cpu, memory (resident), virtual_memory, threads, nice,
priority, started_at, read_bps, write_bps, unit (the systemd
service or scope) and kernel_thread.
cpu is a share of the whole machine, as Task Manager shows it: one busy
core of eight is 12.5.
The process table is sampled only while someone reads it. A call keeps the sampler running for 30 seconds. The first call after a quiet spell takes two scans 0.3 s apart, so it answers in about a third of a second.
monitord runs unprivileged, so another user's I/O counters, open files and
working directory are unreadable to it. Those fields are null.
monitord.process
{"pid": 1234} returns the table's fields plus exe, cwd, the whole
cmdline, cpu_time_s, memory_peak, swap, read_bytes,
write_bytes, open_files, voluntary_switches,
involuntary_switches, cgroup and children.
monitord.history
{"series":["cpu.busy","net.*"], "since":3600, "max_points":600}
seriesis one name or a list. A name ending in*matches a prefix:cpu.core.*is every core.- Give
from(Unix seconds) orsince(seconds back fromto); the default is the last ten minutes.todefaults to now. - Each point is
[time, average, minimum, maximum]. Points are merged into buckets ofstep_sseconds so a series has at mostmax_points(default 600). tiersays where the points came from:
| Tier | Resolution | Kept | Series |
|---|---|---|---|
live | every sample | 10 minutes | all, per core included |
minute | 1 minute | 7 days | system-wide |
hour | 1 hour | 1 year | system-wide |
The series are cpu.busy, cpu.user, cpu.system, cpu.iowait,
cpu.steal, mem.used, mem.available, mem.cached, swap.used,
load.1, load.5, load.15, psi.<cpu|memory|io>.<some|full>,
disk.<name>.<read|write|busy> and net.<name>.<rx|tx>, plus, in memory
only, cpu.core.<n>.busy and cpu.core.<n>.mhz.
The summary topic
Every 5 seconds monitord publishes a small retained summary on
monitord.summary: host, CPU busy and core count, memory, swap, load, total
network and disk rates, the 10-second pressure figures and the task count.
A mesh dashboard subscribes to it instead of polling every node.
The command line
The same binary works with no daemon running. It reads the kernel itself, so it answers on a node's console or over ssh either way:
monitord snapshot the system now, sampled over half a second
monitord ps --sort memory the process table (--tree, --user, --filter, --all)
monitord history cpu.busy --since 86400 stored history from the database
monitord signal 1234 TERM send a signal, as you
monitord renice 1234 10 change a nice value, as you
--json prints what the matching verb returns. signal and renice run as
the person who typed them, and the kernel decides whether they may. No Bus
verb signals or renices anything: acting across users or nodes is
hostd's job.
Running it
monitord.service runs /opt/mixos/bin/monitord serve as the user
monitord, with no capabilities and a strict sandbox. The history
database is /var/lib/monitord/history.db; it is a cache, and deleting it
loses history and nothing else.
serve takes:
--interval-ms(default 1000);--db PATHor--no-db;--minute-days(default 7) and--hour-days(default 365);--no-bus, to sample and record without joining the Bus.
When the broker is down, monitord keeps sampling and recording, and joins when it comes back.
Testing
cargo test -p monitord
The kernel readers take the text of each file, and the verb tests run
against a fixture /proc and /sys tree, so no test needs a real host or
a broker.