技术实战:观测云告警到飞书群并精准@相关人

背景:为什么要做

在多数团队的告警通知链路里,飞书群是最常用的触达入口之一。但实际使用中经常遇到两个问题:

  • 群机器人消息容易被刷屏淹没,高优先级告警难以第一时间触达具体责任人。
  • 告警平台输出的是"事件",而飞书机器人要求的是"消息结构",需要一层转换与路由。

本最佳实践给出一套轻量、易维护的实现方式:

  • 观测云产生告警后,通过 Webhook 推送到 DataFlux Func。
  • Func 将告警事件转换为飞书群机器人 Webhook 的消息格式,并在消息中插入 @人员 标签,实现精准提醒。

目标效果:

  • 告警触达飞书群
  • 支持 @单人 / @多人 / @所有人
  • 支持按监控器、维度或告警等级做人员路由扩展

方案:怎么做

链路如下:

核心约定:在观测云告警模板中增加一行稳定可解析的字段,例如:

makefile 复制代码
FEISHU_USER_IDS: ou_xxx,ou_yyy

Func 只做三件事:

  1. 解析 FEISHU_USER_IDS
  2. 组装飞书群机器人所需 JSON
  3. POST 到飞书群机器人 Webhook

关键点:为什么能精准@

飞书群机器人如何实现 @人员

飞书群机器人文本消息中,通过插入以下标签即可渲染出真正的 @人员

ini 复制代码
<at user_id="ou_xxxxxxx"></at>

支持形式:

  • @单人<at user_id="ou_xxx"></at>
  • @多人:多个 <at ...></at> 用空格拼接
  • @所有人<at user_id="all"></at>

为什么需要 Func 做中转

观测云告警 Webhook 的事件结构与飞书机器人消息结构不同;Func 用于完成字段提取、内容清洗、去重/降噪(可选)与消息格式转换。

配置 1:观测云告警模板

建议在告警模板中保留关键字段,并单独增加一行 FEISHU_USER_IDS(便于稳定解析):

makefile 复制代码
告警标题:{{ df_title }}
告警状态:{{ df_status }}
工作空间:{{ df_workspace_name }}
监控器:{{ df_monitor_name }}
检测项:{{ df_monitor_checker_name }}
维度标签:{{ df_dimension_tags }}
检测值:{{ df_monitor_checker_value }}
事件链接:{{ df_event_link }}

告警详情:
{{ df_message }}

FEISHU_USER_IDS: ou_xxx,ou_yyy

约定说明:

  • FEISHU_USER_IDS: 必须单独一行。
  • 多个用户使用英文逗号分隔。
  • 值为飞书用户 user_id(通常以 ou_ 开头),不是姓名或工号。
  • 不需要 @人 时,可以不写这一行。
  • @所有人 可约定为:FEISHU_USER_IDS: all

配置 2:Func 转发与消息重组

环境变量

bash 复制代码
FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx

如飞书机器人启用了签名校验,可在此基础上增加签名逻辑;本文以最常见的基础 Webhook 为主。

最小可用 Python 脚本(可直接用)

功能:接收观测云 Webhook -> 解析 FEISHU_USER_IDS -> 组装 @ 文本 -> 发送到飞书群机器人。

python 复制代码
import json
import re
import requests

FEISHU_WEBHOOK_URL = DFF.ENV.get("FEISHU_WEBHOOK_URL", "")

USER_ID_RE = re.compile(
    r'(?im)^[ \t]*FEISHU_USER_IDS\s*[:=]\s*([0-9A-Za-z,_-\s]+)\s*$'
)


def dedupe_keep_order(items):
    seen = set()
    result = []
    for item in items:
        if item and item not in seen:
            seen.add(item)
            result.append(item)
    return result


def extract_user_ids_and_clean_message(message):
    user_ids = []

    def repl(match):
        raw = match.group(1) or ""
        for item in re.split(r"[,,、\s]+", raw.strip()):
            item = item.strip()
            if item:
                user_ids.append(item)
        return ""

    cleaned = USER_ID_RE.sub(repl, message or "")
    cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
    return dedupe_keep_order(user_ids), cleaned


def build_mentions(user_ids):
    if not user_ids:
        return ""
    return " ".join([f'<at user_id="{user_id}"></at>' for user_id in user_ids])


def build_text(event, clean_message, user_ids):
    lines = []

    mentions = build_mentions(user_ids)
    if mentions:
        lines.append(mentions)

    lines.extend([
        f'告警标题:{event.get("df_title", "-")}',
        f'告警状态:{event.get("df_status", "-")}',
        f'工作空间:{event.get("df_workspace_name", "-")}',
        f'监控器:{event.get("df_monitor_name", "-")}',
        f'检测项:{event.get("df_monitor_checker_name", "-")}',
        f'维度标签:{event.get("df_dimension_tags", "-")}',
        f'检测值:{event.get("df_monitor_checker_value", "-")}',
    ])

    if clean_message:
        lines.append(f"告警详情:{clean_message}")

    event_link = event.get("df_event_link")
    if event_link:
        lines.append(f"事件链接:{event_link}")

    return "\n".join(lines)


def send_to_feishu_webhook(text):
    payload = {
        "msg_type": "text",
        "content": {"text": text},
    }

    response = requests.post(
        FEISHU_WEBHOOK_URL,
        headers={"Content-Type": "application/json"},
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        timeout=5,
    )
    response.raise_for_status()
    return response.json()


@DFF.API("guance_to_feishu_webhook")
def guance_to_feishu_webhook(**event):
    if not FEISHU_WEBHOOK_URL:
        return 500, {"ok": False, "error": "missing FEISHU_WEBHOOK_URL"}

    raw_message = event.get("df_message") or ""
    user_ids, clean_message = extract_user_ids_and_clean_message(raw_message)
    text = build_text(event, clean_message, user_ids)
    result = send_to_feishu_webhook(text)

    return 200, {"ok": True, "user_ids": user_ids, "result": result}

验证:按这几步一次打通

建议按以下顺序联调,减少排查成本:

  1. 飞书群创建并测试群机器人 Webhook。
  2. 使用 curl 发送最小消息,确认 @人 生效。
  3. 在 Func 中配置 FEISHU_WEBHOOK_URL
  4. 在观测云告警模板中加入 FEISHU_USER_IDS 行。
  5. 部署 Func 脚本并触发一条测试告警。

手工测试飞书 Webhook(最小用例)

vbnet 复制代码
curl -X POST "你的飞书Webhook地址" \
  -H "Content-Type: application/json" \
  -d '{
    "msg_type": "text",
    "content": {
      "text": "<at user_id="ou_xxx"></at> 来自Webhook的测试消息"
    }
  }'

排查:最常见的 3 类问题

消息发送成功但未真正 @ 到人

  • user_id 是否正确(是否为 ou_...all
  • <at user_id="xxx"></at> 是否完整且未被转义
  • 被 @ 的用户是否在当前租户 / 是否在群内

告警到达但解析不到 FEISHU_USER_IDS

  • FEISHU_USER_IDS: 是否单独一行
  • 是否混入中文符号、隐藏空格或额外换行
  • 多用户分隔是否为英文逗号

Func 返回成功但飞书没收到

  • FEISHU_WEBHOOK_URL 是否配置正确
  • Func 网络是否可访问飞书接口
  • 飞书机器人是否启用了关键词 / 签名校验

收益:带来的改变

  • 高优先级告警可精准触达责任人,降低"群消息淹没导致漏处理"的风险。
  • 方案轻量、低侵入,只需告警模板约定 + Func 中转即可快速落地。
  • 后续可扩展为:按监控器/环境/服务路由不同人员,或对接值班表做自动派发。

参考

相关推荐
行百里er6 天前
加个依赖就生效?一行搞定 Spring Boot Starter 自动装配
java·后端·监控
七夜zippoe8 天前
DolphinDB 2.x 集群监控运维平台实战:从指标采集到告警自愈
运维·集群·监控·告警·dolphindb·指标采集
七夜zippoe9 天前
基于 DolphinDB 2.x 的系统性能调优实战:从监控、分析到优化验证的全链路指南
性能调优·监控·分析·dolphindb·优化验证
不积跬步无以至千里9 天前
服务器端性能测试判断磁盘瓶颈的方法
磁盘·监控·性能·分析·lsblk·iostat·瓶颈
lisanmengmeng11 天前
搭建elk环境并接入frostmourne,实现监控报警效果(e二)
elk·监控·日志·日志监控
26519405112648016 天前
08-告警并发与多Master同步
go·监控
lisanmengmeng17 天前
飞书 API使用
飞书·监控·日志·日志监控·监控及日志
三维地图技术社区17 天前
24小时降雨316.1毫米:城市内涝是怎么发生的?排水防涝体系与应急管理一次讲透
监控·图新说·城市内涝·排水防涝·应急管理·三维地下管网·应急预案三维可视化