notify.py 11.2 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
from email.mime.text import MIMEText
from email.header import Header
7
710leo 已提交
11
from bottle import template
Q
qinyening 已提交
12

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

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

21
# 脚本二开指南
7
710leo 已提交
22 23 24
# 1. 可以根据下面的 TEST_ALERT_JSON 中的结构修改脚本发送逻辑,定制化告警格式见tpl下的模块文件
# 2. 每个告警会以json文件的格式存储在 LOCAL_EVENT_FILE_DIR 下面,文件名为 filename = '%d_%d_%d' % (rule_id, event_id, trigger_time)
# 3. 告警通道需要自行定义Send类中的send_xxx的方法,反射调用:举例 event.notify_channels = [邮件] 则需要在NOTIFY_CHANNEL_DICT配置 邮件=email,在Send类中实现 send_email 方法
ning1875's avatar
ning1875 已提交
25 26
# 4. im发群信息,比如钉钉发群信息需要群的webhook机器人 token,这个信息可以在user的contacts map中,各个send_方法处理即可
# 5. 用户创建一个虚拟的用户保存上述im群 的机器人token信息 user的contacts map中
27

Q
qinyening 已提交
28 29 30 31 32 33
mail_host = "smtp.163.com"
mail_port = 994
mail_user = "ulricqin"
mail_pass = "password"
mail_from = "ulricqin@163.com"

34 35 36 37
# 本地告警event json存储目录
LOCAL_EVENT_FILE_DIR = ".alerts"
NOTIFY_CHANNELS_SPLIT_STR = " "

ning1875's avatar
ning1875 已提交
38 39 40
# dingding 群机器人token 配置字段
DINGTALK_ROBOT_TOKEN_NAME = "dingtalk_robot_token"
DINGTALK_API = "https://oapi.dingtalk.com/robot/send"
U
UlricQin 已提交
41 42 43 44

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

7
710leo 已提交
45 46 47 48 49 50 51 52
NOTIFY_CHANNEL_DICT = {
  "email":"email",
  "sms":"sms",
  "voice":"voice",
  "dingtalk":"dingtalk",
  "wecom":"wecom"
}

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


Q
qinyening 已提交
149 150
def main():
    payload = json.load(sys.stdin)
151 152 153 154 155 156 157 158 159 160 161
    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)
    # 生成告警内容
7
710leo 已提交
162
    alert_content = sms_content_gen(values_gen(payload))
163
    for ch in notify_channels:
7
710leo 已提交
164
        send_func_name = "send_{}".format(NOTIFY_CHANNEL_DICT.get(ch.strip()))
165 166 167 168 169 170 171
        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 已提交
172
        send_func(alert_content, payload)
173

7
710leo 已提交
174
def values_gen(payload):
175
    event_obj = payload.get("event")
7
710leo 已提交
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    values = {
        "IsAlert": event_obj.get("is_recovery") == 0,
        "IsMachineDep": event_obj.get("res_classpaths") != "",
        "Status": status_gen(event_obj.get("priority"),event_obj.get("is_recovery")),
        "Sname": event_obj.get("rule_name"),
        "Ident": event_obj.get("res_ident"),
        "Classpath": event_obj.get("res_classpaths"),
        "Metric": metric_gen(event_obj.get("history_points")),
        "Tags": event_obj.get("tags"),
        "Value": event_obj.get("values"),
        "ReadableExpression": event_obj.get("readable_expression"),
        "TriggerTime": time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(event_obj.get("trigger_time"))),
        "Elink": "http://n9e.didiyun.com/strategy/edit/{}".format(event_obj.get("rule_id")),
        "Slink": "http://n9e.didiyun.com/event/{}".format(event_obj.get("id"))
    }

    return values

def email_content_gen(values):
    return template('etc/script/tpl/mail.tpl', values)

def sms_content_gen(values):
    return template('etc/script/tpl/sms.tpl', values)

def status_gen(priority,is_recovery):
    is_recovery_str_m = {1: "恢复", 0: "告警"}
    status = "P{} {}".format(priority, is_recovery_str_m.get(is_recovery))
    return status

def subject_gen(priority,is_recovery,rule_name):
    is_recovery_str_m = {1: "恢复", 0: "告警"}
    subject = "P{} {} {}".format(priority, is_recovery_str_m.get(is_recovery), rule_name)
    return subject

def metric_gen(history_points):
    metrics = [] 
    for item in history_points:
        metrics.append(item.get("metric"))
    return ",".join(metrics)
215 216 217 218 219 220 221 222 223 224 225 226 227

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 已提交
228 229
    def send_email(cls, alert_content, payload):
        users = payload.get("users")
230 231 232
        emails = [x.get("email") for x in users]
        if not emails:
            return
U
UlricQin 已提交
233

234
        recipients = emails
7
710leo 已提交
235 236
        mail_body = email_content_gen(values_gen(payload))
        message = MIMEText(mail_body, 'html', 'utf-8')
237 238
        message['From'] = mail_from
        message['To'] = ", ".join(recipients)
7
710leo 已提交
239
        message["Subject"] = subject_gen(payload.get("event").get("priority"),payload.get("event").get("is_recovery"),payload.get("event").get("rule_name"))
240 241 242 243 244 245 246 247 248

        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
李伟强 已提交
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    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")

277 278

    @classmethod
ning1875's avatar
ning1875 已提交
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    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
                }
305
            }
ning1875's avatar
ning1875 已提交
306 307 308
            res = requests.post(dingtalk_api_url, json.dumps(payload), headers=headers)
            print(res.status_code)
            print(res.text)
309

ning1875's avatar
ning1875 已提交
310
            print("send_dingtalk")
Q
qinyening 已提交
311 312 313 314 315 316


def mail_test():
    print("mail_test_todo")

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

7
710leo 已提交
318 319
    payload =  json.loads(json.dumps(TEST_ALERT_JSON))
    mail_body = email_content_gen(values_gen(payload))
Q
qinyening 已提交
320 321 322
    message = MIMEText(mail_body, 'html', 'utf-8')
    message['From'] = mail_from
    message['To'] = ", ".join(recipients)
7
710leo 已提交
323
    message["Subject"] = subject_gen(payload.get("event").get("priority"),payload.get("event").get("is_recovery"),payload.get("event").get("rule_name"))
Q
qinyening 已提交
324 325 326 327 328 329 330 331 332 333 334 335 336 337

    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:
338
        print("I am confused")
7
710leo 已提交
339