微信接收夜莺告警消息

693次阅读
没有评论

共计 9248 个字符,预计需要花费 24 分钟才能阅读完成。

更新记录

2023-09-06

修改 n9e_wechatbot_text.py​ 代码

  • 原始的提取阈值逻辑有误,原函数如下,只是简单地判断运算符是否出现在 prom_ql 字符串中,而没有考虑到运算符的位置和上下文。当标签中含有运算符时,不会正确提取阈值。
    def extract_threshold(prom_ql):
      # 提取阈值的逻辑
      threshold = ''
      if prom_ql:
          operators = ['!=', '>=', '<=', '>', '<', '=']
          for operator in operators:
              if operator in prom_ql:
                  threshold = operator + ' ' + prom_ql.split(operator)[-1].strip()
                  break
      return threshold
    

  • 改为正则匹配,假设运算符只包含连续的 =​, !=​, >​, <​, >=​, <=​ 字符,并且阈值是一个浮点数或整数。

    def extract_threshold(prom_ql):
      threshold = ''
      if prom_ql:
          pattern = r'([!=<>]+)\s*([\d.]+)'
          match = re.search(pattern, prom_ql)
          if match:
              operator = match.group(1)
              value = match.group(2)
              threshold = operator + ' ' + value
      return threshold
    

2023-09-04

修改 n9e_wechatbot_text.py​ 代码

  • 当告警恢复时,夜莺原始数据 trigger_value​ 携带的不是恢复状态的当前值,而是最后一次触发告警的值。在代码中增加判断,当告警恢复时,不打印当前值。

2023-09-03

修改 n9e_wechatbot_text.py​ 代码

  • 增加提取阈值。原始数据不包含阈值,从 prom_ql 中提取阈值并通知
  • 增加 当告警标题包含 ​​时,将 触发值​​和 阈值​​ 格式化为百分比数
  • 优化告警持续时间的显示格式

效果如图:
微信接收夜莺告警消息

背景

在公司的生产环境中,SRE 有内部开发的告警系统。但是它并不能部署在线下满足个人服务的使用。

最开始我使用了 prometheus​ 的原生告警组件:alertmanger​,大体上能满足我的需求。但是细节方面令人不满意,纯 yaml 的方式也不够直观,维护起来不方便。

个人使用不同于生产环境,我的诉求有以下几点:

  • 支持发送告警到个人微信,我不希望在移动设备上通过其他 APP 来接收告警
  • 支持自定义告警模版,方便在移动设备上一眼可分析告警
  • 有 web 端进行告警的管理和维护
  • 支持从自定义 prometheus 数据源中查询数据

抛开各大云提供商内置的云监控,其实社区上能够选择的告警系统并不算多

  • alertmanger
  • hertzbeat
  • zabbix
  • openfalcon
  • 夜莺

zabbix 不够贴近云原生场景,它更适合于传统 IDC 环境下,对于各物理资源的监控和告警。

openfalcon 基本已经停止维护。

alertmanger 个人使用,不进行二次开发前提下,对于规则的管理比较困难。

hertzbeat 是一个新兴的监控告警解决方案,可惜它的耦合度太高,监控 告警 绘图都必须依赖它自己的组件。而在 prometheus 几乎已经成为云原生监控事实标准的情况下,我并不希望使用它,我仅仅需要一个能够满足接入数据源,方便有效管理告警的设施。

最终我选择了夜莺,实际部署调试后,可以满足我的告警需求。

夜莺是什么

微信接收夜莺告警消息

夜莺对于运维来说肯定不陌生,毕竟社区上成熟的告警方案就那么几家。这里不多赘述,参考官方链接:夜莺官网

部署

参考链接:夜莺官方文档

官方文档提供了三种部署方案:

  • Docker Compose
  • 裸机部署
  • 高级部署:用于下沉告警引擎,适用于复杂机房拓扑的情况

本文选用了裸机部署的方式。

部署环境:

  • 操作系统:CentOS 7.8
  • 夜莺版本:v6.1.0
  • MySQL 版本:5.5.68-MariaDB
  • Redis 版本:3.2.12

其实夜莺部署对于这些组件的版本并没有严格要求,各位自行测试。

MySQL & Redis

因为是个人测试环境,这里使用最简单的方案安装依赖的组件,并不过多考虑版本,密码和其他优化问题。

# 安装和配置mysql
yum -y install mariadb*
systemctl enable mariadb
systemctl restart mariadb
mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('1234');"


# 安装和配置redis
yum install -y redis
systemctl enable redis
systemctl restart redis=

夜莺

部署最新版 v6.10

# 下载安装包
mkdir /usr/local/n9e && cd /usr/local/n9e
wget https://github.com/ccfos/nightingale/releases/download/v6.1.0/n9e-v6.1.0-linux-amd64.tar.gz
tar xf n9e-v6.1.0-linux-amd64.tar.gz

# 查看相关文件
# ls
cli  docker  etc  integrations  n9e  n9e-cli  n9e-edge  n9e.log  n9e.sql  n9e-v6.1.0-linux-amd64.tar.gz  nohup.out

# 导入数据库表结构
mysql -uroot -p1234 < n9e.sql

# 测试启动n9e
./n9e

# 测试通过后,配置systemd管理n9e
# vim /etc/systemd/system/n9e_alert.service
[Unit]
Description=n9e_alert
After=network.target

[Service]
User=root
WorkingDirectory=/usr/local/n9e
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
ExecStart=/usr/local/n9e/n9e > /usr/local/n9e/run.log 2>&1
Restart=always

[Install]
WantedBy=multi-user.target

# 启动和配置开机自启
systemctl enable n9e_alert.service
systemctl restart n9e_alert.service

# 检查服务状态
# systemctl status n9e_alert.service 
● n9e_alert.service - n9e_alert
   Loaded: loaded (/etc/systemd/system/n9e_alert.service; enabled; vendor preset: disabled)
   Active: active (running) since Sat 2023-09-02 13:17:59 CST; 4h 15min ago
 Main PID: 26578 (n9e)
    Tasks: 8
   Memory: 77.7M
   CGroup: /system.slice/n9e_alert.service
           └─26578 /usr/local/n9e/n9e > /usr/local/n9e/run.log 2>&1

至此安装已经完成,访问地址为 `http://$ip:17000`​

调试

配置 nginx 反代

此步骤按照个人需求酌情添加,nginx 配置示例:

upstream n9e {
  server 192.168.2.10:17000;
}

server {
  listen 80;
  server_name n9e.***.com;
  return 301 https://$server_name;
}

server {
  listen 443 ssl http2;
  server_name n9e.***.com;

  ssl_certificate     /root/.acme.sh/*.***.com/fullchain.cer;
  ssl_certificate_key /root/.acme.sh/*.***.com/*.***.com.key;
  ssl_session_timeout 5m;
  ssl_protocols TLSV1 TLSv1.1 TLSv1.2;
  ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
  ssl_prefer_server_ciphers on;

  location / {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_redirect http:// https://;
    proxy_pass http://n9e;
  }
}

web 端调试

成员配置

默认的账号密码为:root​ , root.2020

登录后自行修改密码和新增需要的用户。

点击 人员组织​ –> 用户管理

微信接收夜莺告警消息

如果要使用企业微信机器人进行告警,截取机器人的 webhook 中的 key,填入到联系方式中。

微信接收夜莺告警消息

微信接收夜莺告警消息

团队管理

新建的成员不能直接接收告警,需要将成员添加在团队中,后续的告警规则配置中再将告警通知媒介设置为指定团队。

点击 人员组织​ –> 团队管理​ –> 新增

微信接收夜莺告警消息

数据源配置

添加 prometheus 数据源,请确保你的 prometheus 能够被 n9e 服务器 IP 访问。

点击 系统配置​ –> 数据源

微信接收夜莺告警消息

告警通知配置

默认所有的 通知媒介​ 在进行 告警规则​ 的时候都可选,但是因为我们不需要,而且也没有在 用户管理​ 进行配置 联系方式​,所以这里隐藏掉不需要的告警方式,仅留下 wecom​。

点击 系统配置​ –> 通知设置​ –> 通知媒介

微信接收夜莺告警消息

告警规则配置

配置一个简单的告警进行验证,例如 由 node_exporter​ 采集的 CPU使用率​。

点击 告警管理​ –> 告警规则​ –> 新增

必填参数和选项有:

  • 规则名称
  • 数据源类型
  • 关联数据源
  • PromQL
  • 触发告警级别
  • 执行频率
  • 持续时长
  • 立即启用
  • 生效时间
  • 通知媒介
  • 告警接收组
  • 启用恢复通知
  • 重复通知间隔
  • 最大发送次数

微信接收夜莺告警消息

此处我有一些不满意,因为可以看到 夜莺 不支持设定查询语句查询到的值类型,这个功能在我之前使用的 alertmanger 中是能够满足的。我希望使用 百分率 方式发出,后续的效果可以看到,只能发出为普通的数值。

模拟告警

刚才的 promql 含义是,当 CPU 使用率 > 1% 即告警,经测试能成功发出告警。

在企业微信中,效果如下:
微信接收夜莺告警消息

其实普通微信,也可以查看企业微信中的消息,具体操作方式参考链接:百度经验

但是实测效果并不好:
微信接收夜莺告警消息

查阅了下原因,是因为内置的通知模版语法为 markdown​ 格式,并且还无法修改

点击 系统配置​ –> 通知模版​ –> wecom

微信接收夜莺告警消息

并且我测试新建告警模版,也无法变更模版格式
微信接收夜莺告警消息

其实到这儿,需求算是可以基本完成。但是我实在不希望单独使用企业微信来接受告警。

个人微信在接收企业微信消息时,只有 text​ 格式能够被正常的展示。

查阅 企业微信的接口文档,发现其实支持 text​ 格式,只是 夜莺 没有做兼容。好的,那我自己来解决。

编写 webhook 程序回调企业微信接口

夜莺支持当触发告警时,不仅将告警消息发动到内置的通知媒介,还支持 webhook 方式回调。

那么我们编写一个 python 程序,用于接受夜莺的告警消息,再将其格式化 text 发送到企业微信机器人接口,就能够满足我们在个人微信接收告警的需求。

Python 脚本配置

程序的功能:

  • 启动 5000 端口用于接受夜莺发出的通知
  • 格式化接受到的数据,原始数据字段繁多 ,提取需要的字段
    • 如果想查看原始数据,修改此 python 脚本,将收到的夜莺通知消息不经过格式化,直接打印出来即可
  • 发送到企业微信机器人 webhook

操作如下:

# 创建工作目录
mkdir /usr/local/wechat-alert/ && cd /usr/local/wechat-alert/

# 安装依赖
pip install flask requests

# 编写 py 程序
# vim n9e_wechatbot_text.py
from flask import Flask, request
import requests
import json
import datetime

app = Flask('n9e_wechatbot_text')

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.get_json()  # 获取POST请求的JSON数据
    send_to_wechat(data)  # 发送消息到企业微信
    return 'OK'

def send_to_wechat(data):
    url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4557416b-d3be-430a-b3dd-50d26d271323'
    headers = {'Content-Type': 'application/json'}
    formatted_data = format_data(data)  # 格式化数据
    payload = {
        'msgtype': 'text',
        'text': {
            'content': formatted_data
        }
    }
    response = requests.post(url, headers=headers, json=payload)
    if response.status_code == 200:
        print('消息发送成功')
    else:
        print('消息发送失败')

def format_data(data):
    is_recovered = data.get('is_recovered', False)
    severity = data.get('severity', '')
    rule_name = data.get('rule_name', '')
    note = data.get('note', '')
    tags = data.get('tags', [])
    trigger_value = data.get('trigger_value', '')
    last_eval_time = format_timestamp(int(data.get('last_eval_time', 0)))
    first_trigger_time = format_timestamp(int(data.get('first_trigger_time', 0)))
    prom_ql = data.get('rule_config', {}).get('queries', [{}])[0].get('prom_ql', '')
    rule_note = data.get('rule_note', '')

    formatted_data = ''
    if is_recovered:
        formatted_data += "✅[已恢复] "
    else:
        formatted_data += "❌[告警中] "

    formatted_data += f"{rule_name}\n告警级别:{severity}"

    if tags:
        formatted_data += "\n标签:"
        for tag in tags:
            if tag.startswith('rulename='):
                continue
            formatted_data += f"\n  - {tag}"

    if rule_note:
        formatted_data += f"\n备注:{rule_note}"

    formatted_data += f"\n\n阈值:{format_value(extract_threshold(prom_ql), rule_name)}"

    if not is_recovered:
        formatted_data += f"\n当前值:{format_value(trigger_value, rule_name)}"
    formatted_data += f"\n\n首次触发时间:{first_trigger_time}"
    formatted_data += f"\n当前时间:{last_eval_time}"
    formatted_data += f"\n持续时间:{format_duration(first_trigger_time, last_eval_time)}"

    if not tags:  # 添加条件判断,当标签列表为空时,返回 None
        return None

    return formatted_data.strip()

def format_value(value, rule_name):
    if '率' in rule_name:
        if value:
            operator, threshold = split_operator_threshold(value)
            threshold = convert_to_percentage(threshold)
            return f"{operator} {threshold}"
    return value

def split_operator_threshold(value):
    operators = ['!=', '>=', '<=', '>', '<', '=']
    for operator in operators:
        if operator in value:
            parts = value.split(operator)
            return operator, parts[-1].strip()
    return '', value

def convert_to_percentage(value):
    try:
        value = float(value)
        percentage = value * 100
        return f"{percentage:.2f}%"
    except ValueError:
        return value

def extract_threshold(prom_ql):
    # 提取阈值的逻辑
    threshold = ''
    if prom_ql:
        operators = ['!=', '>=', '<=', '>', '<', '=']
        for operator in operators:
            if operator in prom_ql:
                threshold = operator + ' ' + prom_ql.split(operator)[-1].strip()
                break
    return threshold

def format_timestamp(timestamp):
    if timestamp:
        dt = datetime.datetime.fromtimestamp(timestamp)
        return dt.strftime('%Y-%m-%d %H:%M:%S')
    return ''

def format_duration(start_time, end_time):
    if start_time and end_time:
        start_datetime = datetime.datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S')
        end_datetime = datetime.datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S')
        duration = end_datetime - start_datetime
        days = duration.days
        hours = duration.seconds // 3600
        minutes = (duration.seconds % 3600) // 60
        seconds = duration.seconds % 60

        formatted_duration = ''
        if days > 0:
            formatted_duration += f"{days}d "
        if hours > 0:
            formatted_duration += f"{hours}h "
        if minutes > 0:
            formatted_duration += f"{minutes}m "
        if seconds > 0 or duration.total_seconds() < 60:  # 添加判断条件,如果持续时间小于一分钟,直接返回秒数
            formatted_duration += f"{seconds}s"

        return formatted_duration.strip()
    return ''

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)


# 配置 systemd 管理此服务
# vim /etc/systemd/system/n9e_wechatbot_text.service 
[Unit]
Description=Flask App - n9e_wechatbot_text
After=network.target

[Service]
User=root
WorkingDirectory=/usr/local/wechat-alert
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
ExecStart=/usr/bin/python3 /usr/local/wechat-alert/n9e_wechatbot_text.py > /usr/local/wechat-alert/run.log 2>&1
Restart=always

[Install]
WantedBy=multi-user.target

# 配置 systemd 管理此服务
systemctl enable n9e_wechatbot_text
systemctl start n9e_wechatbot_text

# 检查服务状态
systemctl status n9e_wechatbot_text

告警规则配置 回调 webhook

可以设置全局的 webhook 回调

点击 系统配置​ –> 通知设置​ –> 回调地址

微信接收夜莺告警消息

也可以设置单个规则的 webhook 回调

点击 告警管理​ –> 告警规则

微信接收夜莺告警消息

模拟告警

还是和之前一样,我们配置将告警规则阈值调低

已经可以在个人微信中正确接收告警消息,效果如下:
微信接收夜莺告警消息

总结

万能的组件永远不会存在,个性化的诉求需要有一定的开发能力做支撑。

对于告警来说,核心的是作为告警引擎的能力。这方面来讲,alertmanger & 夜莺 & zabbix 是经过市场检验的。

但是二开插件的难度,日常使用和维护,也是衡量好与坏的标准。

我目前只将夜莺作为告警引擎来使用,已经完全可以满足我当下的需要。

它的其他功能很多,例如告警自愈,定制采集器等,也值得琢磨。等后续有需求时再进一步的研究。

引用链接

正文完
 
pengyinwei
版权声明:本站原创文章,由 pengyinwei 2023-09-02发表,共计9248字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处:https://www.opshub.cn
评论(没有评论)