PulseGuard 是一个面向 SaaS 多租户的 告警推送基建平台:用户在平台上配置自己的 Telegram / Lark 机器人、消息模板、推送通道(Channel)和 Starlark 自定义命令,监控/CI/业务系统通过通用 HTTP Webhook (POST /api/v1/push/{token}) 把告警 payload 推入平台,后台 Outbox Worker 异步消费、渲染、限流、去重、按平台调用对应 Bot API 投递。
整套系统打成 单可执行文件 + 一份 SQLite,无 Redis / 无 MQ / 无外部依赖,可以塞进一台 1C1G 的 VPS。
设计目标:让一个对监控告警有需求的小团队,在 5 分钟内拿到一个有"重试 / 死信 / 限流 / 历史 / 多租户 / 邀请码注册 / 自定义命令"的生产级告警通道。
- 单 binary 部署:Go 1.25 + 纯 Go SQLite (
modernc.org/sqlite),无 cgo,跨平台 - 多租户:邀请码注册,admin 可签发 / 撤销邀请码
- 通用 Webhook:
POST /api/v1/push/{push_token}收任意 JSON - 异步 Outbox 管线:HTTP 立即返回 202,后台 N 个 worker 消费投递
- 重试 / 死信:指数退避
[1s, 5s, 15s, 60s, 5m, 15m],最终失败入 DLQ,UI 一键重投 - Token Bucket 限流:channel 级
rate_per_min,超额自动延后 - 去重窗口:
dedup_key或 payload 指纹,窗口期内重复消息自动丢弃 - 模板系统:Go
text/template全语法,支持 MarkdownV2 / HTML / 纯文本,内置escMD/escHTML转义
- Telegram:长轮询 listener,setMyCommands 注册命令菜单,editMessageText 状态机折叠重复告警,inline keyboard + callback /ack
- Lark / 飞书 Webhook:Custom Bot Webhook URL,post / interactive card 富文本 pass-through
- Lark / 飞书 App Bot:tenant_access_token OAuth + 事件订阅签名校验(HMAC-SHA256)+ IM API 主动发送
- 每个 Bot 独立的 Starlark 命令空间:同名
/查询在 Bot A 和 Bot B 可以指向不同脚本 - 沙箱执行:10s 超时、8KB 输出、64KB 代码上限、HTTP 模块带 SSRF 防护(拒私网/链路本地/metadata、不跟 redirect)
- 内置命令:
/start,/chatid,/commands,/unsubscribe,/ack <fp>,/silence <pattern> <duration>,/silence_list,/unsilence
- Bot Token / App Secret / Encrypt Key 全部 AES-GCM 加密入库,启动时校验 32B master_key
- HMAC-bound CSRF 双 token + HttpOnly + Secure + SameSite=Lax session cookie
- IP 限流(公开端点)+ 5 req/min/IP 登录注册限流
- CSP
script-src 'self'(不含 unsafe-inline)+ X-Frame-Options DENY + HSTS(生产) - Lark 事件 401 统一响应(不暴露 app_id 是否存在)+ ±5min 时间戳重放窗口
- 多租户隔离:所有 repo 强制
tenant_id过滤 + repo 层 ownership check
- Graceful Shutdown:SIGTERM → 关 listener → flush WAL → 退出,崩溃恢复回收 in_flight
- TG / Lark API 错误分类:Transient / PermanentClient / PermanentServer,429 自动
retry_after - Bot 健康面板:updates_received / commands_dispatched / last_error 实时跟踪
- 401 自动 disable bot listener(避免无效 token 反复重试)
docker run -d --name pulseguard \
-p 8080:8080 \
-v $(pwd)/data:/var/lib/pulseguard \
-e PULSEGUARD_SECURITY_MASTER_KEY_B64=$(openssl rand -base64 32) \
-e PULSEGUARD_BOOTSTRAP_INITIAL_ADMIN_EMAIL=admin@example.com \
-e PULSEGUARD_BOOTSTRAP_INITIAL_ADMIN_PASSWORD=ChangeMeNow \
ghcr.io/prowendi/pulseguard:latest打开 http://localhost:8080/ui/login,用上面的 admin 邮箱/密码登录,立即开始配置 bot/template/channel/command。
生产部署务必:(1) 用
openssl rand -base64 32生成长期密钥并妥善保管;(2) 在反向代理(Caddy/Nginx)上做 TLS 终止;(3) 把PULSEGUARD_SECURITY_COOKIE_SECURE=true。
前置:Go 1.25+。
git clone https://github.com/prowendi/PulseGuard.git
cd PulseGuard
make build
./pulseguard -config config.example.yamlconfig.example.yaml 自带可直接运行的 dev 默认值(cookie_secure=false、bootstrap admin 已填)。首次启动检测到空库时会按 bootstrap.initial_admin_* 创建第一个 admin 租户。
常用 make targets:
| target | 用途 |
|---|---|
make build |
本平台编译,产物 ./pulseguard |
make test |
全量单测 |
make cover |
带覆盖率 |
make lint |
go vet ./... && go build ./... |
make docker |
docker build -t pulseguard:dev . |
make release |
跨平台编译到 dist/(linux/amd64, linux/arm64, windows/amd64, darwin/amd64, darwin/arm64) |
make smoke |
真实 Telegram 烟雾测试,需要环境变量 PULSEGUARD_SMOKE_BOT_TOKEN 和 PULSEGUARD_SMOKE_CHAT_ID |
Tenant ──┬── Bot (bot_token / app_secret / encrypt_key 加密入库;platform = telegram | lark)
├── Template (Go text/template,parse_mode = MarkdownV2 / HTML / None)
├── Channel (1 push_token → 1 bot + 1 chat_id + N 模板绑定)
└── Command (Starlark 脚本,绑定到具体 Bot;通过 /<name> 在对话中触发)
| 对象 | 说明 |
|---|---|
| Tenant | 一个邮箱 = 一个租户。所有 Bot/Template/Channel/Command 都属于某个 tenant,强隔离 |
| Bot | 机器人凭证。platform 区分 telegram / lark;bot_kind 进一步区分 lark 的 webhook / app 两种形态。bot_token / app_secret / encrypt_key AES-GCM 加密落库,API 永不回吐明文 |
| Template | 消息模板,支持条件 / 循环 / escMD / escHTML 转义。编辑器内嵌 MarkdownV2 速查面板 + 工具栏插入按钮 |
| Channel | 推送通道,唯一 push_token 是对外的 webhook 入口;可绑定多模板按条件分发 |
| Command | 绑定到具体 Bot 的 Starlark 脚本,触发即沙箱执行并把结果回传到对话 |
| push_token | 32 字符 URL-safe 随机串,channel 级隔离,UI 可一键轮换 |
| Outbox | 推送写入 push_outbox 表,状态机 pending → in_flight → sent / retry / dead |
| DLQ | 终态失败的消息进 dead_letters,UI 可重投回 outbox |
完整流程:登录 → 建 bot → 建 template → 建 channel → 推消息。
BASE=http://localhost:8080
# 1) 登录,拿 session cookie
curl -c cookie.txt -X POST $BASE/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"ChangeMeNow!2026"}'
# 2) 创建一个 Telegram bot(bot_token 入库即加密)
curl -b cookie.txt -X POST $BASE/api/v1/bots \
-H 'Content-Type: application/json' \
-d '{"name":"ops-bot","platform":"telegram","bot_token":"123456:ABC-DEF..."}'
# 2b) 或创建一个 Lark Webhook bot
curl -b cookie.txt -X POST $BASE/api/v1/bots \
-H 'Content-Type: application/json' \
-d '{"name":"lark-ops","platform":"lark","bot_token":"https://open.feishu.cn/open-apis/bot/v2/hook/KEY"}'
# 2c) 或创建一个 Lark App bot(带事件订阅)
curl -b cookie.txt -X POST $BASE/api/v1/bots \
-H 'Content-Type: application/json' \
-d '{
"name":"lark-app","platform":"lark","bot_kind":"app",
"app_id":"cli_xxxx","app_secret":"...",
"verify_token":"...","encrypt_key":"..."
}'
# 3) 创建一个模板
curl -b cookie.txt -X POST $BASE/api/v1/templates \
-H 'Content-Type: application/json' \
-d '{
"name":"alert-md",
"parse_mode":"MarkdownV2",
"body":"{{ if eq .level \"critical\" }}\\ud83d\\udea8{{ else }}\\u26a0\\ufe0f{{ end }} *{{ .title | escMD }}*\\n\\nHost: `{{ .host | escMD }}`\\nValue: *{{ .value | escMD }}*"
}'
# 4) 创建 channel(绑定 bot_id + chat_id),返回 push_token
curl -b cookie.txt -X POST $BASE/api/v1/channels \
-H 'Content-Type: application/json' \
-d '{
"name":"prod-cpu","bot_id":1,
"chat_id":"-1001234567890",
"rate_per_min":60,"dedup_window_s":300,
"templates":[{"template_id":1,"is_default":true}]
}'
# → 201 { "id":1, "push_token":"K8x...abc", ... }
# 5) (可选)创建一个绑定到 bot 1 的 Starlark 命令
curl -b cookie.txt -X POST $BASE/api/v1/commands \
-H 'Content-Type: application/json' \
-d '{
"bot_id":1,
"name":"echo",
"code":"def handle(args):\n return \"hi: \" + \" \".join(args)"
}'
# 在 Bot 私聊里发 /echo hello 即触发
# 6) 推一条告警(无需登录,凭 push_token 鉴权)
curl -X POST $BASE/api/v1/push/K8x...abc \
-H 'Content-Type: application/json' \
-d '{
"title":"CPU High","host":"db01.prod",
"level":"critical","value":"95%",
"dedup_key":"cpu_db01"
}'
# → 202 { "push_id":12345, "status":"queued" }完整 API 文档在 /ui/apidocs(登录后可访问)。
完整字段表(值取自 config.example.yaml,env 覆盖规则 PULSEGUARD_<SECTION>_<FIELD>):
| 字段 | 默认 | 说明 |
|---|---|---|
listen_addr |
:8080 |
HTTP 监听地址 |
base_url |
http://localhost:8080 |
对外 URL,用于 UI 展示 push_token 完整链接 |
read_timeout |
10s |
请求读超时 |
write_timeout |
30s |
请求写超时 |
shutdown_timeout |
15s |
graceful shutdown 等 worker 收尾的上限 |
| 字段 | 默认 | 说明 |
|---|---|---|
path |
./data/pulseguard.db |
SQLite 文件路径,目录自动创建 |
busy_timeout |
5s |
PRAGMA busy_timeout,写锁争用退让窗口 |
| 字段 | 默认 | 说明 |
|---|---|---|
master_key_b64 |
REQUIRED | 32B base64 主密钥,用于 AES-GCM 加密 bot_token / app_secret / encrypt_key |
session_ttl |
336h (14d) |
session cookie TTL |
cookie_secure |
true |
生产 https=true,本地 http=false |
bcrypt_cost |
10 |
bcrypt 哈希轮数 |
| 字段 | 默认 | 说明 |
|---|---|---|
count |
4 |
outbox worker goroutine 数 |
poll_interval |
1s |
空闲轮询休眠 |
max_attempts |
6 |
单条最大尝试次数,超过入 DLQ |
inflight_reclaim_after |
60s |
僵尸 in_flight 行回收阈值 |
backoff_schedule |
[1s, 5s, 15s, 60s, 5m, 15m] |
第 N 次失败的退避间隔 |
| 字段 | 默认 | 说明 |
|---|---|---|
api_base |
https://api.telegram.org |
TG Bot API 基础 URL |
http_timeout |
10s |
TG API 整体超时 |
| 字段 | 默认 | 说明 |
|---|---|---|
initial_admin_email |
REQUIRED | 空库时自动创建的 admin 邮箱 |
initial_admin_password |
REQUIRED | 初始 admin 密码,首次登录后建议立即修改 |
| 字段 | 默认 | 说明 |
|---|---|---|
level |
info |
debug / info / warn / error |
format |
json |
json 或 text |
| 字段 | 默认 | 说明 |
|---|---|---|
push_logs_keep_days |
30 |
push_logs 保留天数 |
dedup_keys_sweep_interval |
1h |
过期 dedup_keys 清理周期 |
sessions_sweep_interval |
1h |
过期 sessions 清理周期 |
- 在飞书群里 "添加机器人 → 自定义机器人",复制 webhook URL
- PulseGuard 建 Bot:platform=
lark,bot_token 填完整 URL - Channel.chat_id 在 webhook 下无意义(URL 已绑定群)
- 飞书开发者后台创建企业自建应用
- PulseGuard 建 Bot:platform=
lark, bot_kind=app,填 app_id / app_secret / verify_token / encrypt_key - 在飞书 "事件订阅" 填回调地址
https://<your-host>/api/v1/lark/events - 开启
im.message.receive_v1事件,PulseGuard 会自动校验签名 + 派发 Starlark 命令
logging.format=json 时输出标准 slog JSON,可直接被 vector / promtail / fluentbit 抓取。敏感字段(bot_token, app_secret, encrypt_key, push_token, password, master_key)自动脱敏。
PulseGuard 不内置备份,运维方按文件系统/卷快照的方式备份:
# 单文件冷备(停服)
systemctl stop pulseguard
cp /var/lib/pulseguard/pulseguard.db /backup/$(date +%F).db
systemctl start pulseguard
# 在线热备(推荐,不停服)
sqlite3 /var/lib/pulseguard/pulseguard.db ".backup /backup/$(date +%F).db"master_key_b64 用于加密 bot_token / app_secret / encrypt_key。轮转步骤(停服窗口):
- 用旧 key 启动 PulseGuard,把所有需要保留的 bot/app 凭证记录下来
- 生成新 key:
openssl rand -base64 32 - 停服,备份 db,清空 bot 加密字段
- 用 新 key 启动后逐个重新提交 bot 凭证
- 验证发送
当前 MVP 没有内置「双 key 滚动解密」,因此轮转需要短暂停服。
单 binary 替换即可,schema migration 在启动时自动 idempotent 跑:
systemctl stop pulseguard
cp pulseguard-linux-amd64 /usr/local/bin/pulseguard
systemctl start pulseguard
journalctl -fu pulseguard
⚠️ Migration 0011 (commands → per-bot 绑定) 会清空 commands + subscribers 表。带数据升级需要先设PULSEGUARD_DEV_RESET=1显式确认(或回滚到不带 0011 的 binary)。
V1 已落地,仍在迭代。非目标 / 暂未实现:
- Channel Group / fan-out(一对多推送)
- 值班排班 / on-call
- Prometheus Alertmanager 协议兼容
- SMTP 接收(邮件转推送)
- 邮箱验证 / 密码重置邮件
- OAuth (GitHub / Google) 登录
- Telegram client 集群化
/metricsPrometheus 暴露(端点占位,未实现)- 计费 / 配额硬上限
- 国际化 / i18n
MIT (TODO: 正式发布前补 LICENSE 文件)