macOS · 一条命令 · 127.0.0.1
Launchd 没有 dashboard。这是在浏览器管理自己的 launchd services 的 console,同时输出 Prometheus metrics。
一条命令装好。浏览器里看状态、日志、告警。
| 标签 | PID | 退出 | 重启 | 操作 | |
|---|---|---|---|---|---|
示例应用3105 项 | |||||
com.example.app.api我的已列入 | 730872h 14m | 0 | 28 | 🔔重启日志 | |
com.example.app.web我的自动(路径)↻ 频繁重启 | 730916d 3h | 0 | 1207↻ | 🔔重启日志 | |
com.example.app.collector我的自动(路径) | 7310418m | 0 | 21 | 🔔重启日志 | |
com.example.worker.nightly我的已列入 | — | 1 | 3 | 🔔启动日志 | |
com.example.backup.weekly我的自动(路径)下次 · Sun 03:00 | — | 0 | 0 | 🔔启动日志 | |
运维0012 项 | |||||
com.example.worker.old我的已列入⏹ 已退役 12 天 | — | — | 0 | 启用日志 | |
com.example.legacy.agent我的自动(路径) | — | 0 | 0 | 启动日志 | |
点「我的」、「其他」或「噪音」。然后看 /metrics 和 Telegram 告警。
为什么
launchctl 列出一切,于是你什么也看不见。
原有的工具回答「加载了什么」,从不回答「我部署了什么,它是否健康」。按用户域来过滤也没用——一台正在使用的 Mac 上,看起来像「你的」任务,大多是 Dropbox、Google 的更新程序、Spotify、Tailscale,再加上一两个浏览器辅助程序。那个重启了 1,207 次的任务,就埋在正中间。
Deployboard 从一份明确的清单出发:你自己控制的允许列表,再加上路径推导——plist 的日志路径、工作目录、program 或参数只要落在你的项目树里,那就是你的任务,自动算进去。其余是系统自己的事,永远只隔一次点击,但不会挡在前面。
- launchctl list — 545 行,分不清哪些是你的
- 悄悄重启了 1,207 次的开发服务器,看起来仍很健康
- 你有意停用的 plist,一直被当成失败
- 没有历史:「这算正常吗?」没有答案
- 要等你偶然发现,才知道有东西已经死了
- 我的 / 其他 / 噪音 — 默认先显示你的部署
- 来自 launchctl print 的 runs(重启次数),超过阈值就标出
- disabled 是一种状态,不是错误 — 退役就是退役
- Prometheus 的 /metrics,让 Grafana 能显示「从何时开始」
- 状态变化时才发 Telegram 告警,按应用,带冷却时间
状态
七种状态,其中一种的意思是「我把它退役了」。
大多数工具把状态收成 running / stopped / error,于是你几个月前自己关掉的 plist,会永远看起来像失败。Deployboard 保留 launchd 真正区分的那些状态,并读取 launchctl print-disabled,让意图也成为答案的一部分。
PID > 0 — launchd print 也确认它在运行
PID = 0、正常退出、已定义计划、没有近期日志
PID = 0、正常退出、日志的 mtime 在 --recent-window 之内
PID = 0、正常退出、没有计划、没有近期日志
上次退出状态非零
launchctl print-disabled 显示 disabled,没有存活的 pid — 退役,不是故障
存在 plist 文件,但 launchctl list 不返回该 label
这个 fork 加上的
上游回答「加载了什么」。这里回答「我部署了什么」。
四项新增,全部建在 launchctl 自己的输出上——不去猜一个任务属于哪个厂商。
用 launchctl print,而不是 launchctl list。
launchctl list 没有重启次数,也没有 disabled 标志。Deployboard 用 launchctl print 读取 runs 和 pid,用 launchctl print-disabled 读取意图——于是抖动看得见,有意退役的任务也不再看起来像坏了。
清单由你决定。
我的 / 其他 / 噪音,每一行都有来源(已列入、自动(路径)、未分类)。分类操作会写回 config.json,大约 2 秒内热重载——教它一个新应用,不必重启。
给 launchd 的 Prometheus 指标。
deployboard_job_up、_runs、_last_exit_code、_disabled,再加上 IPv4/IPv6 端口探测。仪表盘只能告诉你「现在停了」;时间序列能告诉你「03:10 开始抖动」。
可按应用开关的 Telegram 告警。
只在状态变化时通知——error、offline、端口无法连接、重启风暴,以及恢复——并有按应用的开关、冷却时间、免打扰时段。机器人令牌放在 macOS 钥匙串里,永远不写进文件。
每个任务的 stdout 与 stderr,最多 10,000 行,可实时筛选。
退出码、program 是否存在、program 是否可执行、plist 所有者、plist 权限、日志路径。
GET /api/events 每 5 秒推送完整的任务列表。
经 launchctl 做 start / stop / reload,并有确认对话框。
与界面用的是同一面,有文档,curl 也好用。
一个 Go 二进制文件,前端以 go:embed 嵌入,没有守护进程,没有遥测。
指标
这个领域里,唯一的 launchd exporter。
把 Prometheus 指到界面用的同一个端口。标签基数有界,省略是诚实的(从未退出的任务没有 last-exit 序列),只有暴露了端口的任务才有 probe 序列——探测也会试 IPv6 的 ::1,因为有些开发服务器只绑在那里。
# HELP deployboard_job_up 1 when the launchd job is running.# TYPE deployboard_job_up gaugedeployboard_job_up{label="com.example.app.api",category="ours",group="Example App"} 1deployboard_job_runs{label="com.example.app.web",category="ours",group="Example App"} 1207deployboard_job_disabled{label="com.example.worker.old",category="ours"} 1deployboard_job_probe_reachable{label="com.example.app.api",port="8001"} 1deployboard_job_probe_latency_seconds{label="com.example.app.api",port="8001"} 0.003deployboard_jobs_total{category="ours"} 16deployboard_jobs_total{category="noise"} 528
# restart churn, per job, over 6hderiv(deployboard_job_runs{category="ours"}[6h]) * 3600# anything of yours that is not runningdeployboard_job_up{category="ours"} == 0
从提供界面的同一个进程抓取即可,不必再部署 exporter,也不需要 sidecar。没有的序列就省略,不会编造:从未退出的任务不会有 last_exit 行,没有端口的任务也不会有 probe 行。
告警
它在 Telegram 上告诉你,而且只说一次。
按状态变化来发,所以一个来回抖动的任务不能刷屏:坏掉时一条,若持续重启则一条受速率限制的,恢复时再一条。恢复消息永远不会被免打扰时段压掉。机器人令牌放在 macOS 钥匙串里,由仪表盘自己写入——永远不进文件,永远不进日志行,也永远不回到页面上。
API
界面用到的一切,你的脚本也能用。
Web 界面是 /api/jobs 和 /api/events 的一个消费者。curl 也是。绑定服务器,指向端点,解析 JSON。
安装
一条命令。一个配置文件。一个标签页。
三种安装方式、同一二进制:Homebrew cask(首屏命令)、不用 Go 也能装 LaunchAgent 的 curl install-release.sh、或从克隆运行 bash install.sh。不需要 sudo;脚本支持 --dry-run 和 --uninstall。
$ brew install --cask francistse/tap/deployboard用 Homebrew cask 装二进制,或用 install-release.sh / install.sh 一并写入带 RunAtLoad + KeepAlive 的 LaunchAgent。不需要 sudo;脚本支持 --dry-run 和 --uninstall。
$ open ~/.config/deployboard/config.json要么在 config.json 的 inventory.ours 下列出你的命名空间,要么把项目文件夹加进 derive_roots,让 plist 路径自己分类。未分类的会出现在「其他」——在界面点一下就固定,config.json 会热重载。
仪表盘在 127.0.0.1 打开。用 Prometheus 抓取 /metrics,用铃铛按应用开关告警,并在系统、浅色、深色之间切换主题。
状态值
上游六个 + disabled
配置热重载
不必重启就能分类新应用
Prometheus 序列家族
任务、runs、退出码、探测
唯一绑定的地址
没有云、没有遥测、没有更新探测
FAQ
一个 fork 必须回答的问题。
包括它和上游是什么关系。
它和所 fork 的 launch-pilot 有什么不同?
上游做的事都还在——任务表、日志跟踪、六项诊断、start/stop/reload。Deployboard 加上清单视图(我的 / 其他 / 噪音,带路径推导)、launchctl print 的补充(runs 和 disabled)、Prometheus 端点、可按应用开关的 Telegram 告警、一条命令的 macOS 安装,以及只读模式。
它怎么知道哪些任务是我的?
两条规则,按顺序:先是你写下的允许列表(inventory.ours),然后是推导——如果 plist 的日志路径、工作目录、program 或参数落在你列于 derive_roots 的目录里,那就是你的任务。plist 指向的一切都是证据,所以没有猜测,也没有域名启发式。
如果它把一个任务判错了呢?
它会出现在「其他」,标成「未分类」,你在该行选择自动 / 我的 / 隐藏。这会写入 config.json,服务器大约两秒内热重载。没有东西会被悄悄藏起来:完整的 launchd 列表永远只隔一个标签。
这些东西会离开我的机器吗?
不会。HTTP 服务器只绑定 127.0.0.1,没有分析统计,也不会回传。整个项目唯一能打出去的,是你明确开启的 Telegram 消息——用的令牌存在你自己的钥匙串里。
可以只用指标、不要告警吗?
可以。告警默认对你自己的应用开启,对其余的关闭;可以按应用、按类别,或整个关掉。/metrics 和 JSON API 照样可用,而 --read-only 会拒绝每一次会改变状态的调用。
为什么它从不修改我的 plist?
有意为之。Deployboard 读取 launchd 状态,也可以请 launchctl 对一个 label 做 start、stop 或 reload,但不会改写磁盘上的 plist。如果你要的是 plist 编辑器,LaunchControl 是现成的付费选择;这个工具关心的是,你的部署正在做什么。