多机代理监控 + GitHub Runner 状态统一接入 Uptime Kuma

目的:把两类监控——每台机器的代理/网络心跳、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)

  1. 登录 Uptime Kuma → Add New Monitor
  2. Monitor TypePush
  3. Friendly Name:如 mac-mini-01-代理Github-runner
  4. Heartbeat Interval:必须 大于脚本的 push 周期,留足余量。脚本每 60s push 就设 90~120s,每 120s push 就设 150~180s——否则两次 push 之间会超时变红、下次又恢复,形成 Down/Up 抖动刷屏。
  5. Retries:0 或 1
  6. Notifications:勾选要用的通知渠道(飞书 / Webhook / 邮件等)
  7. 保存后详情页给出 Push URL,记下其中的 <PUSH_TOKEN>
    1
    https://<kuma域名>/api/push/<PUSH_TOKEN>?status=up&msg=OK&ping=

Push 接口参数

GET /api/push/<token> 接受三个参数:

参数 说明
status updown
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
2
3
# --noproxy '*' 强制直连:代理挂了,这条心跳也得照样发出去,否则就失去意义
curl --noproxy '*' -s -o /dev/null -m 8 \
"https://<kuma域名>/api/push/<本机token>?status=up&msg=OK&ping="

launchd / cron 每 60s 跑一次。机器整机断网/关机 → Kuma 收不到 → 超时告警(这正是”反向心跳”:正常时报平安,外部观察者发现没动静才报警,覆盖了”机器自己发不出告警”的盲区)。

彩蛋:把”代理异常”也并进同一个监控

Push 接口收 statusping,所以本机脚本可以一举两得:

1
2
3
4
5
6
7
8
9
10
11
12
13
if curl -x "$PROXY" -sf -o /dev/null -m "$TIMEOUT" "$TEST_URL"; then
# 代理正常:上报 up,顺便把代理延迟当 ping 画成曲线
delay=$(curl -x "$PROXY" -s -o /dev/null -w '%{time_total}' "$TEST_URL")
ms=$(awk "BEGIN{printf \"%d\", $delay*1000}")
curl --noproxy '*' -s -o /dev/null -m 8 \
"https://<kuma域名>/api/push/<本机token>?status=up&msg=OK&ping=${ms}"
else
fails=$((fails+1))
if [ "$fails" -ge "$THRESHOLD" ]; then
curl --noproxy '*' -s -o /dev/null -m 8 \
"https://<kuma域名>/api/push/<本机token>?status=down&msg=Clash探测连续失败&ping="
fi
fi

这样「代理异常」和「整机失联」两层都收敛进同一个监控、一块面板

  • 代理挂但机器还在 → 脚本 push down,msg 说明是代理;
  • 整机断网/关机 → 脚本根本发不出 → Kuma 超时判 down。

两种情况都告警,且能从 msg / 是否超时区分。

macOS 上完整的可落地版本(最终脚本 + launchd plist + 「登录后自启 vs 开机即启」的区别)见单独一篇:《Clash 代理监控对接 Uptime Kuma(clash-kuma)》


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
2
3
4
5
6
7
云端 check-runners.sh (cron 每2分钟)
├─ 查 GitHub API 全部 self-hosted runner
├─ 某台 offline → 飞书告警 🔴 (精确到台,原有逻辑保留)
└─ push 到 Kuma 单个 Push 监控:
全 online → status=up, msg="全部N个在线"
有掉线 → status=down, msg="掉线: <机器名>"
API失败 → status=down, msg="API查询失败:..."

关键设计点: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
KUMA_PUSH="https://<kuma域名>/api/push/<PUSH_TOKEN>"

# 向 Kuma push:$1=up|down $2=消息文本(自动 URL 编码)
kuma() {
[ -z "$KUMA_PUSH" ] && return
local status="$1" msg="$2" enc
enc=$(printf '%s' "$msg" | jq -sRr @uri) # URL 编码,处理中文/空格
curl -s -m 10 -o /dev/null "${KUMA_PUSH}?status=${status}&msg=${enc}&ping=" </dev/null
}

# ... 查询 GitHub API、遍历 runner、收集 down_list ...

# 汇报整体状态
if [ -n "$down_list" ]; then
kuma down "掉线:${down_list} (共${cnt}个runner)"
else
kuma up "全部 ${cnt} 个 runner 在线"
fi

要点:

  • 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
2
3
4
5
6
7
8
9
10
11
# a. push 端点连通性(应返回 {"ok":true},Kuma 监控变绿)
curl -s "https://<kuma域名>/api/push/<PUSH_TOKEN>?status=up&msg=test&ping="

# b. 手动跑一次 runner 检查脚本(全 online 应 push up)
sudo HB_WEBHOOK="<飞书webhook>" /opt/docker-compose/heartbeat/check-runners.sh

# c. 模拟掉线(push down,Kuma 应变红并按通知渠道告警)
curl -s "https://<kuma域名>/api/push/<PUSH_TOKEN>?status=down&msg=$(printf '%s' '掉线: office-build-mac-04' | jq -sRr @uri)&ping="

# d. 确认 cron 在自动跑
tail -3 /opt/docker-compose/heartbeat/runner-check.log

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 超时兜底告警。


7. 待办 / 可选改进

  • 每个 Push 监控的 Heartbeat Interval 都大于对应脚本的 push 周期(消除 Down/Up 抖动)
  • 每个监控都已勾选通知渠道(否则只变红不告警)
  • 给 Kuma 自己挂一层外部 healthcheck(它在公司云稳,但 down 了是静默 down)
  • (可选)代理 up 时把探测延迟当 ping 上报,让 N/A 变成真实曲线
  • (可选)runner / 每台机器若需独立历史曲线,改为一台一个 Push 监控

注:文中 <kuma域名><PUSH_TOKEN><本机token><飞书webhook>、GitHub token 等均为占位符,实际值见服务器配置,勿外泄。