monitor

开发指南

架构与协议

┌─────────────┐  WebSocket / JSON-RPC 2.0  ┌───────────────┐      ┌─────────┐
│    agent    │ ── Authorization: Bearer ─▶│      hub      │◀──── │ browser │
│ (Linux VPS) │ ◀────── ping.tasks ─────── │ axum + SQLite │      │  React  │
└─────────────┘                            └───────────────┘      └─────────┘
  读 /proc                                    monitor.db          面板 / 状态页
  不写任何文件                                 后台内置,主题可换

三个仓库

仓库内容
monitorhub + 内置后台 + install.sh
agentLinux agent,发布自己的 musl 二进制
monitor-theme-default默认公开页主题。发布构建产物,hub 按 sha256 钉住并嵌入

agent 单独一个仓库,因为它装在别的机器上,发布节奏也不同;默认主题单独一个仓库,让主题有独立的接口约定、版本和开发流程。

hub 使用的是主题的构建产物,不编译主题源码。发布的 theme.tar.gz 解开就是一个可安装的主题目录,默认主题和第三方主题遵循同一套约定。

线上协议

WebSocket 上承载 JSON-RPC 2.0 通知:只有 method 和 params,没有 id,不需要响应。一条长连接两个方向都能主动发送,报文是带方法名的 JSON,直接可读。

agent → hub

methodparams何时发
helloFacts每次连接建立后一次
reportMetrics每 --interval 秒
ping.result{ task_id, latency_ms },-1 表示连不上每个探测任务按自己的间隔

Facts:hostname、os、kernel、arch、virt、cpu_name、cpu_cores、mem_total、swap_total、disk_total、agent_version、ipv4、ipv6

Metrics:

boot_id  iface  uptime  cpu  load[3]
mem_total  mem_used  swap_total  swap_used  disk_total  disk_used
net_rx_total  net_tx_total    ← 内核 lifetime 计数器,hub 负责累加
net_rx  net_tx                ← 瞬时速率 B/s,agent 自己算差值
tcp  udp  procs

boot_id 来自 /proc/sys/kernel/random/boot_id,后接 / 与所计网卡集合的摘要。它标明 net_rx_total / net_tx_total 在哪段区间内可以相减:hub 只比较是否相等,变了就重新对基线。重启、改 --iface、网卡增减或被重新归类都走这一条。iface 回报当前的 --iface,面板用它显示并预填。

hub 按 agent 自己的节奏收,超出的直接丢弃:与上一条间隔不足 500 ms 的 report、同一连接的第二个 hello、每 5 秒超过 128 条的 ping.result(每节点至多 64 个探测,每个最短 5 秒一次)。

hub → agent

methodparams何时发
ping.tasks[{ id, target, interval }]连接建立时;面板增删改探测任务时立刻下发

agent 收到后保留没变的任务(id、target、interval 都相同),只重启变了的,避免每次下发都把所有计时器清零。每个节点最多运行 64 个任务。

鉴权

token 走 Authorization: Bearer 头,不走 URL query,因为 query 会进反代的 access log。

token 只在握手时校验一次,所以换发 token 或删除节点时,hub 会主动断开那条连接。

请求路径

反代或 WAF 按路径放行时,hub 用到的就是这些,另加主题自己的前端路由(默认主题是 / 和 /node/{id})。

agent 侧

GET  /api/agent/ws          Bearer token,长连接
GET  /install.sh            公开,不含密钥
GET  /agent/{arch}          公开,把 release 二进制从 GitHub 转发给节点
POST /api/agent/register    公开,凭窗口内有效的注册 key 换一个节点 token

注册接口另认一个 X-Node-Token 头:安装脚本重跑时带上机器已有的 token,它仍对应节点就原样返回,不看窗口和 key,所以窗口关了也能重跑;对应不上再按 key 注册。

arch 只认 x86_64 和 aarch64。二进制总是经 hub 转发,能连上 hub 就能装,只有 IPv6 或访问不了 GitHub 的机器也一样。同时最多转发 4 份,其余排队最多 30 秒,仍轮不到的返回 503,安装脚本会自动重试。

读取(登录看全部;未登录且公开页开着,只看公开节点)

GET /api/me
GET /api/nodes
GET /api/nodes/{id}/metrics?hours=N&points=W&series=metrics|ping
GET /api/ws                 每 2 秒推一次快照
GET /api/themes/{short}/config

登录

POST /api/auth/login    POST /api/auth/logout
GET  /api/auth/github   GET  /api/auth/github/callback

面板(全部要管理员会话)

POST   /api/nodes
PUT    /api/nodes/order            PUT /api/nodes/batch
PUT    /api/nodes/{id}             DELETE /api/nodes/{id}
POST   /api/nodes/{id}/token       PUT /api/nodes/{id}/traffic
POST   /api/register-window        DELETE /api/register-window
GET    /api/ping-tasks             POST /api/ping-tasks
DELETE /api/ping-tasks/{id}
GET    /api/settings               PUT /api/settings
GET    /api/sessions               DELETE /api/sessions/{id}
POST   /api/notify/test            GET  /api/version
GET    /api/themes                 POST /api/themes
DELETE /api/themes/{short}         GET  /api/themes/{short}/preview
POST   /api/themes/{short}/update  PUT  /api/themes/{short}/config
GET    /api/db                     GET  /api/db/backup
POST   /api/db/restore             POST /api/db/vacuum

其余路径的处理顺序:

/api/*           未匹配即 404,不回落 SPA
/admin, /admin/* 内置后台
其它             当前磁盘主题;不可用时回落内置默认主题

数据模型

八张表。schema 版本记在 SQLite 自带的 PRAGMA user_version 里。

表作用
settingkey/value 配置,替代配置文件
node节点配置 + agent 上报的静态信息,含分组、离线通知的开关与状态
traffic单调递增的流量累计。1:1 于 node,约每分钟写一次
metric历史明细,每节点每分钟一行,最多留 7 天
ping_task / ping_node探测任务及其节点分配
ping_record探测结果,同样最多留 7 天
metric_hour / ping_hour小时汇总,由上面两张表每小时折叠而来,按保留天数删
session登录会话,存 sha256,14 天过期

traffic 单独一张表,因为它和 node 的读写方式完全不同:一个是偶尔修改的配置,一个是持续累加的数据。更重要的是,metric 可以随意清理而累计流量不受影响,累计值不是从明细算出来的。

累计值约每分钟记一次账:hub 在内存里保留每个节点最新的一条读数,跨分钟时记账,boot_id 变化或读数变小时断点两侧各记一次,连接断开和 hub 正常退出时补记。内核计数器一直在累加,后一次记账包含中间的全部增量,结果与逐条记账相同。hub 被强行杀掉时少记最后不到一分钟,节点下次上报补回(前提是期间没重启)。探测结果同样按分钟成批写入。

metric 的一行描述的是它之前的那一分钟,而不是某一时刻:网速由累计值的差算出,其余字段是这一分钟内的平均值。同一行另存这一分钟里 agent 报过的最高网速,聚合成宽窗口时取桶内的最大值,所以 7 天窗口里的一次测速仍是它当时的速率。

小时汇总的一行是这一小时里全部分钟行的平均值,另存参与平均的分钟数;延迟存答上来的次数、超时次数、中位数和最低最高值。一小时结束后再等一小时才汇总:agent 最慢一小时上报一次,探测结果要等下一帧才落库。分钟明细在所在的小时汇总之后才删。

历史查询的两个上限

hours   窗口宽度   1 到保留天数 × 24,登录与匿名相同
points  返回点数   60–1440,默认 1440;调用方报上自己能画多少点,hub 只会调低

两个上限管两件事:points 限制返回多少行,hours 限制扫描多少行。7 天以内的窗口读分钟明细,最多读一周的行;更宽的窗口读小时汇总,一年也只有 8760 行,再加上最近还没汇总的那一两个小时的分钟明细。所以无论窗口多宽,一次请求读的行数都不超过一周的分钟明细,匿名请求不必另设更低的上限。

更宽的窗口每个点至少一小时。资源的平均值按分钟数加权合并,和直接用分钟明细算的一致;延迟的丢包率和最低最高值是精确的,中位数在一个点跨多个小时时,取各小时中位数按答上来的次数加权后的中位数。

分辨率由屏幕决定:样本放得下就一个不抽稀,放不下才聚合。上限由 hub 决定而不是调用方,因为这个接口匿名可访问。

实时状态放在内存里

每条 agent 连接的出站通道、会话号和最新一次上报都在内存里,hub 重启后在一个上报周期内重建。

在线与否只看 WebSocket 是否连着:握手时加入,断开时移除,只有这一处记录。

连着不等于活着。网络中断、内核卡死、NAT 表项超时时,TCP 连接会停在半开状态。所以 hub 每 30 秒发一个 WebSocket Ping,收到任何帧都算活着,连续 120 秒没有收到就主动断开,agent 随即重连。判定离线最慢 150 秒。

在 GitHub 上修改这一页最后更新 2026-10-03