前两篇介绍了用 Alloy + Loki Ruler 实现日志告警:
- CouchDB 日志监控:文本日志的 LogQL 告警
- Windmill 日志监控:JSON 日志的 LogQL 告警
两条链路最终都汇入 Alertmanager,由它统一分派到飞书。但 Alertmanager 原生没有飞书 receiver——它的 webhook 输出的是原始 JSON,飞书看不懂。中间需要一层适配器来转换格式。这个适配器就是 alert-transformer。
整体架构 链接到标题
告警规则触发"] --> B["Alertmanager
分组/去重/限流"] B -->|"POST /alertmanager
标准 Webhook JSON"| C["alert-transformer
格式化/过滤/路由"] C -->|"POST /hooks/agent
纯文本 + Agent 参数"| D["OpenClaw Gateway
Agent Turn"] D -->|"飞书消息"| E["用户手机"]
Alertmanager 做告警分组和去重,transformer 做格式转换和路由,OpenClaw 做最终投递。三者各司其职。
Alertmanager 端 链接到标题
Alertmanager 在告警触发时向配置的 webhook URL 发送 POST 请求。先看它的配置:
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'openclaw'
receivers:
- name: 'openclaw'
webhook_configs:
- url: 'http://alert-transformer:9091/alertmanager'
send_resolved: true
关键参数:
| 参数 | 作用 | 值 |
|---|---|---|
group_by |
按 alertname 合并同类告警 | 一个 Webhook 请求可能包含多条相同告警 |
group_interval |
合并窗口 10s | 避免瞬时多次触发 |
repeat_interval |
未恢复时重新通知间隔 | 1 小时 |
send_resolved |
同时也发送恢复通知 | 让飞书能收到"已恢复"消息 |
Alertmanager 发出的请求体结构如下:
{
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "HighCPUUsage",
"severity": "critical",
"hostname": "tank"
},
"annotations": {
"summary": "CPU usage > 90%",
"description": "CPU usage on tank is 95%"
},
"startsAt": "2026-05-28T10:00:00Z",
"endsAt": "2026-05-28T10:05:00Z"
}
]
}
这是一个标准格式,包含告警状态、标签、注解和时间戳。
alert-transformer 核心 链接到标题
transformer 是一个 Node.js(Fastify)服务,源码 125 行,核心逻辑按以下顺序执行。
接收与拆分 链接到标题
收到 Alertmanager Webhook 后,按 status 拆分为 firing(触发中)和 resolved(已恢复):
const { alerts } = request.body || {};
if (!alerts || alerts.length === 0) return { ok: true };
const firing = alerts.filter(a => a.status === 'firing');
const resolved = alerts.filter(a => a.status === 'resolved');
过滤 链接到标题
跳过不需要处理的告警:
function shouldSkip(severity) {
if (!skipSeverity) return false;
const levels = ['info', 'warning', 'critical'];
return levels.indexOf(severity) < levels.indexOf(skipSeverity);
}
if (shouldSkip(a.labels.severity)) return; // 跳过低级别
if (ignoreAlerts.includes(a.labels.alertname)) return; // 跳过黑名单
过滤机制由两个环境变量控制:
| 变量 | 值 | 效果 |
|---|---|---|
SKIP_SEVERITY=info |
只在配置低于 info 时跳过 | 实际跳过 info 级别(因为 info 比 warning 低) |
IGNORE_ALERTS=Watchdog,DeadMansSwitch |
逗号分隔的黑名单 | 内置心跳告警不推送 |
格式化 链接到标题
Firing 告警格式化为中文文本:
function buildFiringMessage(alerts) {
if (alerts.length === 1) {
const a = alerts[0];
const sev = a.labels.severity || 'info';
const cfg = SEVERITY_CONFIG[sev] || SEVERITY_CONFIG.info;
return '告警信息\n名称: ' + a.labels.alertname
+ '\n级别: ' + sev
+ '\n主机: ' + (a.labels.hostname || a.labels.instance || '')
+ '\n开始时间: ' + formatTime(a.startsAt)
+ '\n摘要: ' + (a.annotations.summary || '')
+ '\n描述: ' + (a.annotations.description || '')
+ '\n\n' + cfg.prompt;
}
// 多条告警合并
const lines = ['批量告警 (' + alerts.length + ' 条)'];
alerts.forEach((a, i) => {
lines.push((i + 1) + '. ' + a.labels.alertname + ' | ' + (a.labels.hostname || ''));
});
return lines.join('\n');
}
严重级别配置影响消息尾部的提示词和超时时间:
const SEVERITY_CONFIG = {
critical: { prefix: '[CRITICAL]', prompt: '请立即处理!', timeoutSeconds: 120 },
warning: { prefix: '[WARNING]', prompt: '请关注处理。', timeoutSeconds: 60 },
info: { prefix: '[INFO]', prompt: '', timeoutSeconds: 30 }
};
Resolved 告警格式更简洁:
function buildResolvedMessage(alert) {
return '告警恢复\n名称: ' + alert.labels.alertname
+ '\n主机: ' + (alert.labels.hostname || alert.labels.instance || '')
+ '\n恢复时间: ' + formatTime(alert.endsAt);
}
路由与转发 链接到标题
如何决定告警发给哪个 OpenClaw Agent:
function routeToAgent(alertname) {
if (agentRouteTable[alertname]) return agentRouteTable[alertname];
return DEFAULT_AGENT;
}
路由表通过环境变量配置,例如:
AGENT_ROUTE_TABLE={"HighCPUUsage":"infra-agent","DatabaseDown":"db-agent"}
发送到 OpenClaw 的代码:
async function sendToOpenClaw(payload) {
const response = await fetch(OPENCLAW_URL + '/hooks/agent', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + OPENCLAW_TOKEN,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...payload,
channel: payload.channel || DEFAULT_CHANNEL
})
});
return response.json();
}
并发控制使用 p-limit 限制同时最多 3 个请求:
const limit = pLimit(QUEUE_CONCURRENCY);
const promises = [];
firingGroup && promises.push(limit(() => sendToOpenClaw({
message: buildFiringMessage(firingGroup),
name: 'PrometheusAlert',
agentId: routeToAgent(firingGroup[0].labels.alertname),
wakeMode: 'now',
deliver: true,
timeoutSeconds: getTimeoutBySeverity(firingGroup[0].labels.severity),
channel: DEFAULT_CHANNEL
})));
resolved.forEach(a => {
promises.push(limit(() => sendToOpenClaw({
message: buildResolvedMessage(a),
name: 'PrometheusAlertResolved',
...
})));
});
await Promise.all(promises).catch(e => fastify.log.error(e));
配置汇总 链接到标题
所有配置通过环境变量传入,无需配置文件:
| 变量 | 默认值 | 说明 |
|---|---|---|
OPENCLAW_URL |
http://openclaw-gateway:18789 |
OpenClaw 网关地址 |
OPENCLAW_TOKEN |
— | Hook 认证 Token |
QUEUE_CONCURRENCY |
3 |
并发上限 |
DEFAULT_CHANNEL |
feishu |
通知渠道 |
DEFAULT_AGENT |
main |
默认 Agent |
SKIP_SEVERITY |
"" |
跳过低于此级别的告警 |
IGNORE_ALERTS |
"" |
逗号分隔的黑名单 |
AGENT_ROUTE_TABLE |
{} |
alertname → agentId 映射 |
OpenClaw 端 链接到标题
transformer 发出的请求到达 OpenClaw 的 /hooks/agent 端点,创建一次 Agent Turn。
为什么不直接用 Wake?参考 Agent Turn 与 Wake 的区别:Agent Turn 创建隔离的执行回合,不污染用户的对话历史,适合系统通知推送。
OpenClaw 收到请求后的处理:
- 验证
Authorization: Bearer <token> - 创建一个隔离的 Agent 回合
- 因为
deliver: true+channel: feishu,消息直接投递到飞书
告警效果 链接到标题
飞书上的呈现效果:

Docker 部署 链接到标题
transformer 是一个简单的 Node.js 服务,打包为 Docker 镜像部署,与 Alertmanager 同机运行:
services:
alert-transformer:
build: ./alert-transformer
container_name: alert-transformer
restart: unless-stopped
environment:
OPENCLAW_URL: http://openclaw-gateway:18789
OPENCLAW_TOKEN: your-hook-token-here
DEFAULT_CHANNEL: feishu
DEFAULT_AGENT: main
SKIP_SEVERITY: info
IGNORE_ALERTS: Watchdog,DeadMansSwitch
AGENT_ROUTE_TABLE: '{"HighCPUUsage":"infra-agent","DatabaseDown":"db-agent"}'
ports:
- "9091:9091"
OPENCLAW_URL 指向实际 OpenClaw 网关地址。如果 transformer 和 Alertmanager 在同一台机器上,走 Docker 内部网络;如果 OpenClaw 在另一台机器,写实际 IP。
总结 链接到标题
| 组件 | 角色 | 输入 | 输出 |
|---|---|---|---|
| Alertmanager | 告警分组/去重 | Prometheus rule 触发 | Webhook JSON |
| alert-transformer | 格式转换/过滤/路由 | Alertmanager Webhook | Agent Turn 请求 |
| OpenClaw | 最终投递 | Hook 请求 | 飞书消息 |
与其他两篇日志监控文章的关系:
- 日志监控(CouchDB / Windmill)解决的是怎么发现告警
- 本篇解决的是告警发现了怎么通知到人
三者构成了完整的"发现 → 告警 → 通知"闭环。