目的:把两类监控——每台机器的代理/网络心跳、GitHub 组织的 self-hosted runner 在线状态——统一接入 Uptime Kuma,掉线告警、看得出是哪台,并各自保留原有的”防误判”逻辑。
这是 《从”代理又挂了”到一套多机监控告警系统》 的落地续篇:那篇手搓了一套云端接收 + cron 扫 mtime + 自拼飞书的系统,这篇把兜底层换成现成的 Uptime Kuma。
0. 为什么用 Kuma,以及一个共同模式
手搓那套「云端接收服务 + cron 判定 + 自拼飞书 JSON」,本质就是在重造 Uptime Kuma 的 Push 监控。如果已经有一个稳定的 Kuma(比如挂在公司云上),就把判定、超时、通知、历史曲线、仪表盘全交给它,自己只管”按时上报”。
两类监控走的是同一个模式——主动 Push:
- 不是 Kuma 去拨测目标(runner 状态藏在要鉴权的 GitHub API 里、代理探测要在机器本地做,Kuma 都拨不到);
- 而是本地脚本自己判定,把结论 push 给 Kuma:正常
status=up,异常status=down; - Kuma 超时收不到 push → 兜底告警(脚本挂了/断网也能发现)。
这样判定逻辑(连续失败才报、API 失败不误判、强制直连探测……)全留在脚本里,Kuma 只当”展示 + 告警的出口”。token / webhook 这些重凭证也不必塞进 Kuma。
1. 共同基础:Kuma Push 接口
两类监控都用 Push 类型监控,配置一次、理解一次即可。
建一个 Push 监控(Web UI)
- 登录 Uptime Kuma → Add New Monitor
- Monitor Type 选 Push
- Friendly Name:如
mac-mini-01-代理或Github-runner - Heartbeat Interval:必须 大于脚本的 push 周期,留足余量。脚本每 60s push 就设 90~120s,每 120s push 就设 150~180s——否则两次 push 之间会超时变红、下次又恢复,形成 Down/Up 抖动刷屏。
- Retries:0 或 1
- Notifications:勾选要用的通知渠道(飞书 / Webhook / 邮件等)
- 保存后详情页给出 Push URL,记下其中的
<PUSH_TOKEN>:1
https://<kuma域名>/api/push/<PUSH_TOKEN>?status=up&msg=OK&ping=
Push 接口参数
GET /api/push/<token> 接受三个参数:
| 参数 | 说明 |
|---|---|
status |
up 或 down |
msg |
展示文本(需 URL 编码,中文/空格必须编码) |
ping |
延迟毫秒数,可留空 |
成功返回 {"ok":true};token 错或监控停用返回 {"ok":false,"msg":"Monitor not found or not active."}。
飞书通知
Settings → Notifications 加一个 Feishu(填群机器人 webhook),在每个监控里勾上即可。Kuma 原生支持,不用自己拼 JSON。
关键认知:Kuma 里一个 token = 一个监控 = 一个 up/down 状态。
msg/ping只是附带展示文本,不参与判活。所以”具体是哪台/哪个出问题”要靠msg带出来,或者拆成多个监控(多个 token)。
2. 代理 / 网络心跳接入
每台机器原本就在本地探测代理(走代理打一个轻量请求,连续失败才算异常)。现在把”报平安”从自建云端服务改成 push 给 Kuma。
心跳上报:强制直连是命根子
1 | # --noproxy '*' 强制直连:代理挂了,这条心跳也得照样发出去,否则就失去意义 |
launchd / cron 每 60s 跑一次。机器整机断网/关机 → Kuma 收不到 → 超时告警(这正是”反向心跳”:正常时报平安,外部观察者发现没动静才报警,覆盖了”机器自己发不出告警”的盲区)。
脚本只剩两件事:走代理探测一次、把结果 push 给 Kuma。「连续几次算异常」「去重不刷屏」「告警发到哪」全部由 Kuma 配置决定,脚本因此极简、不易坏。而且 push 接口收 status 和 ping,所以「代理异常」和「整机失联」两层能并进同一个监控、一块面板:
- 代理挂但机器还在 → 脚本探测失败、主动 push
down(msg 说明是代理); - 整机断网/关机 → 脚本根本发不出 → Kuma 超时判 down。
两种情况都告警,且能从 msg / 是否超时区分。
脚本 ~/bin/clash-kuma.sh(macOS 落地版)
1 |
|
几个小决策(都是踩过坑后的选择):
- 一次 curl 同时取码和耗时:
-w '%{http_code} %{time_total}',再用${out%% *}/${out##* }拆分,省一次请求; awk算毫秒而非bc:macOS 默认没bc,awk一定有;- down 的 msg 用纯 ASCII(
clash-proxy-down-code-000):免 URL 编码的麻烦;超时探测时code为000; - curl 加
</dev/null:习惯性切断 stdin,避免在循环里被吞。
定时:launchd ~/Library/LaunchAgents/com.user.clashkuma.plist
1 |
|
1 | launchctl unload ~/Library/LaunchAgents/com.user.clashkuma.plist 2>/dev/null |
关于「开机自启」的关键区别
~/Library/LaunchAgents/(用户级 LaunchAgent)是 登录后自启,不是开机即启:
| 场景 | 是否自动跑 |
|---|---|
| 登录账户后 | ✅ |
| 重启后停在登录界面、未登录 | ❌(登录后才加载) |
| 登录态下睡眠/唤醒 | ✅ 继续 |
| 注销 logout | ❌ 停止 |
- 日常登录使用的 Mac → 等同开机自启,无需额外处理;
- 开了自动登录 → 真·开机自启;
- 要求「未登录也后台跑」→ 改放
/Library/LaunchDaemons/(系统级,需 sudo),开机即启无需登录。
1 | launchctl print gui/$(id -u)/com.user.clashkuma | grep -E 'state|runs' |
3. GitHub Runner 状态接入
那几台机器同时也是某组织的 GitHub Actions self-hosted runner。runner 在线状态藏在要 Bearer token 认证、返回 JSON 数组的 GitHub API 里,不能让 Kuma 直接拨测:
- Kuma 的 HTTP 关键词检查是对整个响应体做包含匹配,而 GitHub 返回的是所有 runner 的数组,无法表达「每一个都 online」;
- 致命盲区:runner 被删除/注销时响应里没有
offline字样,Kuma 反而以为正常(漏报)。
所以同样走「脚本查询 + 主动 push」:
1 | 云端 check-runners.sh (cron 每2分钟) |
关键设计点:1 个监控 + msg 区分哪台
- 用 1 个 Push 监控表达「runner 整体是否健康」,把具体掉线的机器名拼进
msg——告警通知里就能看到是哪台。 - 脚本主动 push
status=down→ 掉线当下立即告警,不必等 Kuma 超时; - Kuma 的超时判活作为兜底:脚本挂了/云端断网,Kuma 收不到也会告警。
若需要每台 runner 独立的历史曲线,得改成每台一个 Push 监控(一个 token)。当前一个监控只够”整体红绿 + 告警里看哪台”。
云端脚本(关键片段)
位置:/opt/docker-compose/heartbeat/check-runners.sh(云服务器,root,cron 每 2 分钟)。
1 | KUMA_PUSH="https://<kuma域名>/api/push/<PUSH_TOKEN>" |
要点:
jq -sRr @uri对 msg 做 URL 编码,否则中文/空格会破坏请求;curl ... </dev/null:切断 curl 的 stdin(在循环里 curl 会吞 stdin 的经典坑,统一加上);- API 查询失败也 push down:否则 token 过期那天 Kuma 反而以为一切正常(这条防误判是原系统就有的,迁到 Kuma 也别丢)。
cron
/etc/cron.d/runner-check:
1 | */2 * * * * root /opt/docker-compose/heartbeat/check-runners.sh >> /opt/docker-compose/heartbeat/runner-check.log 2>&1 |
GitHub token 存在 /root/.gh-runner-token(600),脚本从文件读,不进 cron/日志。
4. 谁来监控这个监控者?
这一步恰好绕回原文的主题。手搓版的兜底是 Python 标准库 + 文件 mtime,”简单到不会坏”——对最该被信任的兜底层而言,零件越少越好。换成 Kuma(Node + SQLite + Web 栈),监控者自己的故障面就变大了;而且 Kuma 要是挂了,是静默挂,没人告诉你。
你没法用一个挂掉的 Kuma 去报告它自己挂了。
所以真用 Kuma,记得再给它自己挂一层外部 healthcheck:healthchecks.io、或者第二个独立实例去 ping 它的状态页。把花哨的仪表盘交给 Kuma,但别让”更重的监控栈”把你最底层的那点信任也变复杂。
5. 验证
1 | # a. push 端点连通性(应返回 {"ok":true},Kuma 监控变绿) |
6. 常见问题
Q: 收到成对的 [Down]→[Up] 告警,间隔约等于 push 周期?
A: Kuma 的 Heartbeat Interval 设得短于脚本 push 周期,两次 push 之间超时。把 Interval 调到大于 push 周期并留余量(push 60s → 90~120s;push 120s → 150~180s)。Down 消息为 No heartbeat in the time window 即此类超时(非脚本主动报 down)。
Q: 告警里 Ping: N/A?
A: 这是 Kuma 通知渠道的固定消息格式,push 脚本控制不了。超时 down 本就无 ping 可测;up 时若 ping= 传空就是 N/A。想显示数字就把探测/响应耗时当 ping 传入(仅 up 有效,见第 2 节代理延迟那段)。要彻底删除该行,需在 Kuma 后台改通知渠道的消息模板,且仅部分通知类型支持自定义模板。
Q: 告警能精确到哪台吗?
A: 能,看告警 Message 里的 掉线: <机器名>。但单个 Kuma 监控本身是整体红绿,不区分台;要分台就拆成每台一个 Push 监控。
Q: GitHub token 过期了会怎样?
A: API 返回非 runners 响应,脚本走「API 失败」分支:push down + 日志记 API 查询失败,不会误判全部 runner 掉线。
Q: 代理挂了,心跳还发得出去吗?
A: 发得出——心跳那行带了 --noproxy '*' 强制直连,不走代理。只有整机断网/关机才发不出,那种情况由 Kuma 超时兜底告警。
Q: clash 没开就报红,正常吗?
A: 正常,脚本如实上报了代理不可用(探测 code=000)。开启 clash 后下一周期(≤60s)自动 push up 变绿。
Q: 和「自建飞书告警」版本有什么区别?该选哪个?
A: 自建版脚本自己做阈值/去重/飞书广播(见 上一篇);本文这套把这些全交给 Kuma,脚本只探测+push。二者择一即可,别对同一个 Kuma token 同时双发。
7. 待办 / 可选改进
- 每个 Push 监控的 Heartbeat Interval 都大于对应脚本的 push 周期(消除 Down/Up 抖动)
- 每个监控都已勾选通知渠道(否则只变红不告警)
- 给 Kuma 自己挂一层外部 healthcheck(它在公司云稳,但 down 了是静默 down)
- (可选)代理 up 时把探测延迟当
ping上报,让 N/A 变成真实曲线 - (可选)runner / 每台机器若需独立历史曲线,改为一台一个 Push 监控
注:文中 <kuma域名>、<PUSH_TOKEN>、<本机token>、<飞书webhook>、GitHub token 等均为占位符,实际值见服务器配置,勿外泄。