notify.py 10.6 KB
Newer Older
Q
qinyening 已提交
1 2 3 4 5 6
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys
import json
import os
import smtplib
7
import time
U
UlricQin 已提交
8
import requests
Q
qinyening 已提交
9 10 11
from email.mime.text import MIMEText
from email.header import Header

12 13 14
reload(sys)                      # reload 才能调用 setdefaultencoding 方法
sys.setdefaultencoding('utf-8')  # 设置 'utf-8'

Q
qinyening 已提交
15 16 17 18 19
# 希望的demo实现效果:
# 1. 从stdin拿到告警信息之后,格式化为一个有缩进的json写入一个临时文件
# 2. 文件路径和名字是.alerts/${timestamp}_${ruleid}
# 3. 调用SMTP服务器发送告警,微信、钉钉、飞书、slack、jira、短信、电话等等留给社区实现

20 21 22
# 脚本二开指南
# 1. 可以根据下面的TEST_ALERT_JSON 中的结构修改脚本发送逻辑,定制化告警格式格式如下
"""
ning1875's avatar
ning1875 已提交
23 24 25 26 27 28 29 30
[告警类型:prometheus]
[规则名称:a]
[是否已恢复:已触发]
[告警级别:1]
[触发时间:2021-07-02 16:05:14]
[可读表达式:go_goroutines>0]
[当前值:[vector={__name__="go_goroutines", instance="localhost:9090", job="prometheus"}]: [value=33.000000]]
[标签组:instance=localhost:9090 job=prometheus]
31 32
"""
# 2. 每个告警会以json文件的格式存储在LOCAL_EVENT_FILE_DIR 下面,文件名为 filename = '%d_%d_%d' % (rule_id, event_id, trigger_time)
ning1875's avatar
ning1875 已提交
33 34 35
# 3. 告警通道需要自行定义Send类中的send_xxx同名方法,反射调用:举例 event.notify_channels = [qq dingding] 则需要Send类中 有 send_qq send_dingding方法
# 4. im发群信息,比如钉钉发群信息需要群的webhook机器人 token,这个信息可以在user的contacts map中,各个send_方法处理即可
# 5. 用户创建一个虚拟的用户保存上述im群 的机器人token信息 user的contacts map中
36

Q
qinyening 已提交
37 38 39 40 41 42 43 44 45 46 47 48
mail_host = "smtp.163.com"
mail_port = 994
mail_user = "ulricqin"
mail_pass = "password"
mail_from = "ulricqin@163.com"

# just for test
mail_body = """
<p>邮件发送测试</p>
<p><a href="https://www.baidu.com">baidu</a></p>
"""

49 50 51 52
# 本地告警event json存储目录
LOCAL_EVENT_FILE_DIR = ".alerts"
NOTIFY_CHANNELS_SPLIT_STR = " "

ning1875's avatar
ning1875 已提交
53 54 55
# dingding 群机器人token 配置字段
DINGTALK_ROBOT_TOKEN_NAME = "dingtalk_robot_token"
DINGTALK_API = "https://oapi.dingtalk.com/robot/send"
U
UlricQin 已提交
56 57 58 59

WECOM_ROBOT_TOKEN_NAME = "wecom_robot_token"
WECOM_API = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send"

60 61 62
# stdin 告警json实例
TEST_ALERT_JSON = {
    "event": {
ning1875's avatar
ning1875 已提交
63 64 65 66 67 68
        "alert_duration": 10,
        "notify_channels": "dingtalk",
        "res_classpaths": "",
        "id": 4,
        "notify_group_objs": None,
        "rule_note": "",
69 70
        "history_points": [
            {
ning1875's avatar
ning1875 已提交
71
                "metric": "go_goroutines",
72 73
                "points": [
                    {
ning1875's avatar
ning1875 已提交
74 75
                        "t": 1625213114,
                        "v": 33.0
76 77 78
                    }
                ],
                "tags": {
ning1875's avatar
ning1875 已提交
79 80
                    "instance": "localhost:9090",
                    "job": "prometheus"
81 82 83 84
                }
            }
        ],
        "priority": 1,
ning1875's avatar
ning1875 已提交
85
        "last_sent": True,
86
        "tag_map": {
ning1875's avatar
ning1875 已提交
87 88
            "instance": "localhost:9090",
            "job": "prometheus"
89
        },
ning1875's avatar
ning1875 已提交
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
        "hash_id": "ecb258d2ca03454ee390a352913c461b",
        "status": 0,
        "tags": "instance=localhost:9090 job=prometheus",
        "trigger_time": 1625213114,
        "res_ident": "",
        "rule_name": "a",
        "is_prome_pull": 1,
        "notify_users": "1",
        "notify_groups": "",
        "runbook_url": "",
        "values": "[vector={__name__=\"go_goroutines\", instance=\"localhost:9090\", job=\"prometheus\"}]: [value=33.000000]",
        "readable_expression": "go_goroutines>0",
        "notify_user_objs": None,
        "is_recovery": 0,
        "rule_id": 1
105 106
    },
    "rule": {
ning1875's avatar
ning1875 已提交
107 108 109 110 111
        "alert_duration": 10,
        "notify_channels": "dingtalk",
        "enable_stime": "00:00",
        "id": 1,
        "note": "",
112
        "create_by": "root",
ning1875's avatar
ning1875 已提交
113 114 115 116 117 118
        "append_tags": "",
        "priority": 1,
        "update_by": "root",
        "type": 1,
        "status": 0,
        "recovery_notify": 0,
119
        "enable_days_of_week": "1 2 3 4 5 6 7",
ning1875's avatar
ning1875 已提交
120 121 122 123 124 125 126
        "callbacks": "localhost:10000",
        "notify_users": "1",
        "notify_groups": "",
        "runbook_url": "",
        "name": "a",
        "update_at": 1625211576,
        "create_at": 1625211576,
127 128
        "enable_etime": "23:59",
        "group_id": 1,
ning1875's avatar
ning1875 已提交
129 130 131 132
        "expression": {
            "evaluation_interval": 4,
            "promql": "go_goroutines>0"
        }
133 134 135
    },
    "users": [
        {
ning1875's avatar
ning1875 已提交
136 137 138 139 140
            "username": "root",
            "status": 0,
            "contacts": {
                "dingtalk_robot_token": "xxxxxx"
            },
141
            "create_by": "system",
ning1875's avatar
ning1875 已提交
142 143
            "update_at": 1625211432,
            "create_at": 1624871926,
144 145 146
            "email": "",
            "phone": "",
            "role": "Admin",
ning1875's avatar
ning1875 已提交
147 148 149 150
            "update_by": "root",
            "portrait": "",
            "nickname": "\u8d85\u7ba1",
            "id": 1
151 152 153 154 155
        }
    ]
}


Q
qinyening 已提交
156 157
def main():
    payload = json.load(sys.stdin)
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    trigger_time = payload['event']['trigger_time']
    event_id = payload['event']['id']
    rule_id = payload['rule']['id']
    notify_channels = payload['event'].get('notify_channels').strip().split(NOTIFY_CHANNELS_SPLIT_STR)
    if len(notify_channels) == 0:
        msg = "notify_channels_empty"
        print(msg)
        return
    # 持久化到本地json文件
    persist(payload, rule_id, event_id, trigger_time)
    # 生成告警内容
    alert_content = content_gen(payload)

    for ch in notify_channels:
        send_func_name = "send_{}".format(ch.strip())
        has_func = hasattr(Send, send_func_name)

        if not has_func:
            msg = "[send_func_name_err][func_not_found_in_Send_class:{}]".format(send_func_name)
            print(msg)
            continue
        send_func = getattr(Send, send_func_name)
ning1875's avatar
ning1875 已提交
180
        send_func(alert_content, payload)
181 182 183 184 185 186 187 188 189 190


def content_gen(payload):
    # 生成格式化告警内容
    text = ""
    event_obj = payload.get("event")

    rule_type = event_obj.get("is_prome_pull")
    type_str_m = {1: "prometheus", 0: "n9e"}
    rule_type = type_str_m.get(rule_type)
Q
qinyening 已提交
191

ning1875's avatar
ning1875 已提交
192
    text += "[告警类型:{}]\n".format(rule_type)
Q
qinyening 已提交
193

194
    rule_name = event_obj.get("rule_name")
ning1875's avatar
ning1875 已提交
195
    text += "[规则名称:{}]\n".format(rule_name)
Q
qinyening 已提交
196

197 198 199
    is_recovery = event_obj.get("is_recovery")
    is_recovery_str_m = {1: "已恢复", 0: "已触发"}
    is_recovery = is_recovery_str_m.get(is_recovery)
ning1875's avatar
ning1875 已提交
200
    text += "[是否已恢复:{}]\n".format(is_recovery)
Q
qinyening 已提交
201

202
    priority = event_obj.get("priority")
ning1875's avatar
ning1875 已提交
203
    text += "[告警级别:{}]\n".format(priority)
Q
qinyening 已提交
204

205
    trigger_time = event_obj.get("trigger_time")
ning1875's avatar
ning1875 已提交
206
    text += "[触发时间:{}]\n".format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(trigger_time))))
Q
qinyening 已提交
207

208
    readable_expression = event_obj.get("readable_expression")
ning1875's avatar
ning1875 已提交
209
    text += "[可读表达式:{}]\n".format(readable_expression)
Q
qinyening 已提交
210

211
    values = event_obj.get("values")
ning1875's avatar
ning1875 已提交
212
    text += "[当前值:{}]\n".format(values)
Q
qinyening 已提交
213

214
    tags = event_obj.get("tags")
ning1875's avatar
ning1875 已提交
215
    text += "[标签组:{}]\n".format(tags)
Q
qinyening 已提交
216

217 218
    print(text)
    return text
Q
qinyening 已提交
219

220 221 222 223 224 225 226 227 228 229 230 231 232

def persist(payload, rule_id, event_id, trigger_time):
    if not os.path.exists(LOCAL_EVENT_FILE_DIR):
        os.makedirs(LOCAL_EVENT_FILE_DIR)

    filename = '%d_%d_%d' % (rule_id, event_id, trigger_time)
    filepath = os.path.join(LOCAL_EVENT_FILE_DIR, filename)
    with open(filepath, 'w') as f:
        f.write(json.dumps(payload, indent=4))


class Send(object):
    @classmethod
U
UlricQin 已提交
233 234
    def send_email(cls, alert_content, payload):
        users = payload.get("users")
235 236 237
        emails = [x.get("email") for x in users]
        if not emails:
            return
U
UlricQin 已提交
238

239 240
        recipients = emails

U
UlricQin 已提交
241
        message = MIMEText(alert_content, 'html', 'utf-8')
242 243 244 245 246 247 248 249 250 251 252 253
        message['From'] = mail_from
        message['To'] = ", ".join(recipients)
        message["Subject"] = "n9e alert"

        smtp = smtplib.SMTP_SSL(mail_host, mail_port)
        smtp.login(mail_user, mail_pass)
        smtp.sendmail(mail_from, recipients, message.as_string())
        smtp.close()

        print("send_mail_success")

    @classmethod
李伟强 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
    def send_wecom(cls, alert_content, payload):
        users = payload.get("users")

        for u in users:
            contacts = u.get("contacts")
            wecom_robot_token = contacts.get(WECOM_ROBOT_TOKEN_NAME, "")

            if wecom_robot_token == "":
                continue

            wecom_api_url = "{}?key={}".format(WECOM_API, wecom_robot_token)
            atMobiles = [u.get("phone")]
            headers = {'Content-Type': 'application/json;charset=utf-8'}
            payload = {
                "msgtype": "text",
                "text": {
                    "content": alert_content
                },
                "at": {
                    "atMobiles": atMobiles,
                    "isAtAll": False
                }
            }
            res = requests.post(wecom_api_url, json.dumps(payload), headers=headers)
            print(res.status_code)
            print(res.text)
            print("send_wecom")

282 283

    @classmethod
ning1875's avatar
ning1875 已提交
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
    def send_dingtalk(cls, alert_content, payload):
        # 钉钉发群信息需要群的webhook机器人 token,这个信息可以在user的contacts map中

        users = payload.get("users")

        for u in users:
            contacts = u.get("contacts")

            dingtalk_robot_token = contacts.get(DINGTALK_ROBOT_TOKEN_NAME, "")

            if dingtalk_robot_token == "":
                print("dingtalk_robot_token_not_found")
                continue

            dingtalk_api_url = "{}?access_token={}".format(DINGTALK_API, dingtalk_robot_token)
            atMobiles = [u.get("phone")]
            headers = {'Content-Type': 'application/json;charset=utf-8'}
            payload = {
                "msgtype": "text",
                "text": {
                    "content": alert_content
                },
                "at": {
                    "atMobiles": atMobiles,
                    "isAtAll": False
                }
310
            }
ning1875's avatar
ning1875 已提交
311 312 313
            res = requests.post(dingtalk_api_url, json.dumps(payload), headers=headers)
            print(res.status_code)
            print(res.text)
314

ning1875's avatar
ning1875 已提交
315
            print("send_dingtalk")
Q
qinyening 已提交
316 317 318 319 320 321


def mail_test():
    print("mail_test_todo")

    recipients = ["ulricqin@qq.com", "ulric@163.com"]
322

Q
qinyening 已提交
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    message = MIMEText(mail_body, 'html', 'utf-8')
    message['From'] = mail_from
    message['To'] = ", ".join(recipients)
    message["Subject"] = "n9e alert"

    smtp = smtplib.SMTP_SSL(mail_host, mail_port)
    smtp.login(mail_user, mail_pass)
    smtp.sendmail(mail_from, recipients, message.as_string())
    smtp.close()

    print("mail_test_done")


if __name__ == "__main__":
    if len(sys.argv) == 1:
        main()
    elif sys.argv[1] == "mail":
        mail_test()
    else:
342
        print("I am confused")