前两篇介绍了用 Alloy + Loki Ruler 实现日志告警:

两条链路最终都汇入 Alertmanager,由它统一分派到飞书。但 Alertmanager 原生没有飞书 receiver——它的 webhook 输出的是原始 JSON,飞书看不懂。中间需要一层适配器来转换格式。这个适配器就是 alert-transformer

整体架构 链接到标题

flowchart LR A["Prometheus
告警规则触发"] --> 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 收到请求后的处理:

  1. 验证 Authorization: Bearer <token>
  2. 创建一个隔离的 Agent 回合
  3. 因为 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)解决的是怎么发现告警
  • 本篇解决的是告警发现了怎么通知到人

三者构成了完整的"发现 → 告警 → 通知"闭环。