Python实现钉钉通报和邮件通知
概述
监控预警是一个自动化系统中常见的模块,我们希望系统出问题时能够主动把问题推送给维护人员,而不是要维护人员定时主动到服务端去查看各项服务的运行状态。
本文实现两种类型的消息通知功能:钉钉机器人通报和邮件通报。
基于Webhook的钉钉机器人推送消息
准备步骤
-
首先创建一个钉钉群聊,在群聊添加自定义机器人[
-
机器人安全设置
目前支持3种安全设置:
-
自定义关键词:消息中至少包含其中1个关键词才可以发送成功,最多可以设置10个关键词
-
加签:根据钉钉机器人服务提供的密钥,结合时间戳生成签名字符串,使用 HmacSHA256算法 计算签名,然后依次进行Base64 Encode、urlEncode,得到最终的签名(需要使用UTF-8字符集)
-
IP地址(段)。
-
以加签为例,机器人安全设置中勾选加签,系统会生成一个密钥,同意条款点击完成则会得到一个webhook接口,使用该接口并通过post方法附带要发送的消息内容及相关参数即可实现钉钉同步。
代码示例
利用时间戳和密钥生成签名字符串(timestamp+"\n"+secret)
#python 3.8
import time
import hmac
import hashlib
import base64
import urllib.parse
timestamp = str(round(time.time() * 1000))
secret = 'this is secret'
secret_enc = secret.encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, secret)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
url = f"{webhook}×tamp={timestamp}&sign={sign}"
# 钉钉文本消息格式
data = {
"at": {
"isAtAll": at_all
},
"text": {
"content": "测试通知"
},
"msgtype": "text"
}
response = requests.post(url=url, data=json.dumps(data), headers=headers).textp)
print(response)
更多消息类型
钉钉自定义机器人支持文本(text)、链接(link)、markdown、ActionCard、FeedCard消息类型。根据钉钉开放平台的说明/建议:
每个机器人每分钟最多发送20条。消息发送太频繁会严重影响群成员的使用体验,
大量发消息的场景 (譬如系统监控报警) 可以将这些信息进行整合,通过markdown消息
以摘要的形式发送到群里。
-
text: 文本类型
{ "at": { "atMobiles":[ "180xxxxxx" ], "atUserIds":[ "user123" ], "isAtAll": false }, "text": { "content":"我就是我, @XXX 是不一样的烟火" }, "msgtype":"text" }
-
link:链接类型
{ "msgtype": "link", "link": { "text": "这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林", "title": "时代的火车向前开", "picUrl": "", "messageUrl": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI" } }
-
markdown类型
{ "msgtype": "markdown", "markdown": { "title":"杭州天气", "text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n" }, "at": { "atMobiles": [ "150XXXXXXXX" ], "atUserIds": [ "user123" ], "isAtAll": false } }
【注:只支持markdown语法的子集,详情参考文档】
-
整体跳转ActionCard类型
{ "actionCard": { "title": "乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身", "text": "![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png) ### 乔布斯 20 年前想打造的苹果咖啡厅 Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划", "btnOrientation": "0", "singleTitle" : "阅读全文", "singleURL" : "https://www.dingtalk.com/" }, "msgtype": "actionCard" }
-
独立跳转ActionCard
{ "msgtype": "actionCard", "actionCard": { "title": "我 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身", "text": "![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) \n\n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划", "btnOrientation": "0", "btns": [ { "title": "内容不错", "actionURL": "https://www.dingtalk.com/" }, { "title": "不感兴趣", "actionURL": "https://www.dingtalk.com/" } ] } }
-
FeedCard消息类型
{ "msgtype":"feedCard", "feedCard": { "links": [ { "title": "时代的火车向前开1", "messageURL": "https://www.dingtalk.com/", "picURL": "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" }, { "title": "时代的火车向前开2", "messageURL": "https://www.dingtalk.com/", "picURL": "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" } ] } }
常见错误
// 消息内容中不包含任何关键词
{
"errcode":310000,
"errmsg":"keywords not in content"
}
// timestamp 无效
{
"errcode":310000,
"errmsg":"invalid timestamp"
}
// 签名不匹配
{
"errcode":310000,
"errmsg":"sign not match"
}
// IP地址不在白名单
{
"errcode":310000,
"errmsg":"ip X.X.X.X not in whitelist"
}
如时间戳超时,会收到如下报错信息:
参考文档
邮件通知
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
my_sender = 'test@foxmail.com' # 发件人邮箱账号
my_pass = 'XXXXXXXXXXX' # 发件人邮箱密码,若使用腾讯邮箱,则填对应的SMTP服务授权码
my_user = 'test@foxmail.com' # 收件人邮箱账号,我这边发送给自己
def mail():
ret = True
try:
msg = MIMEText('数据采集正常', 'plain', 'utf-8')
msg['From'] = formataddr(["通知助手", my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['To'] = formataddr(["FK", my_user]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['Subject'] = "[任务执行成功] -------" # 邮件的主题,也可以说是标题
server = smtplib.SMTP_SSL("smtp.qq.com", 465) # 发件人邮箱中的SMTP服务器,端口是25
server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码
server.sendmail(my_sender, [my_user, ], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.quit() # 关闭连接
except Exception as e: # 如果 try 中的语句没有执行,则会执行下面的 ret=False
ret = False
print(e)
return ret
ret = mail()
if ret:
print("邮件发送成功")
else:
print("邮件发送失败")
参考文档
封装重构
考虑到消息通知可能有多个渠道,针对同一个消息有时我们想同时通过钉钉、短信、邮件等途径进行通报,因此可考虑将每一个具体通知渠道抽象成一个消息处理单元(Handler),通过通知器(Noticer)进行来执行通报逻辑,当Handler作为Noticer的成员属性,当Noticer启用了什么类型的消息处理单元(Handler),就会进行什么类型的消息通报。一个简单的示例代码如下:
import json
import time
import hmac
import hashlib
import base64
import urllib.parse
import requests
def create_noticer(enable_mail=False, enable_dingtalk=True):
noticer = Noticer()
if enable_mail:
noticer.add_handler(MailHandler())
if enable_dingtalk:
noticer.add_handler(DingTalkHandler())
return noticer
class Handler:
"""通知处理程序"""
def send(self, msg):
"""作为接口"""
pass
class DingTalkHandler(Handler):
"""钉钉通知处理程序"""
def __init__(self):
self.init_variables()
def init_variables(self):
# 密钥和webhook可抽出到配置文件中
self.secret = "XXX"
self.webhook = 'https://oapi.dingtalk.com/robot/send?access_token=XXX'
def send(self, msg, at_all=False):
"""
发送消息,当前只实现文本类型消息发送
:param msg:
:param at_all:
:return:
"""
headers = {'Content-Type': 'application/json', "Charset": "UTF-8"}
# webhook 地址
timestamp = str(round(time.time() * 1000))
# 加签密钥
secret_enc = self.secret.encode("utf-8")
string_to_sign = '{}\n{}'.format(timestamp, self.secret)
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
url = f"{self.webhook}×tamp={timestamp}&sign={sign}"
# 钉钉消息格式,其中 msg 就是我们要发送的具体内容
data = {
"at": {
"isAtAll": at_all
},
"text": {
"content": msg
},
"msgtype": "text"
}
return requests.post(url=url, data=json.dumps(data), headers=headers).text
class MailHandler(Handler):
"""邮件通知处理程序"""
pass
class Noticer:
"""
消息通知器
"""
def __init__(self):
self.handlers = []
def notice(self, msg):
# 对msg进行一定的封装、定制、格式化,此处省略
# ...
self.handle(msg)
def handle(self, msg):
for handler in self.handlers:
handler.send(msg)
def add_handler(self, hdlr):
"""
添加处理程序
WARNING: 如果Handler使用了文件或数据库,则该方法线程不安全,需针对数据同步问题进行优化
:param hdlr:
:return:
"""
if not (hdlr in self.handlers):
self.handlers.append(hdlr)
def remove_handler(self, hdlr):
"""
移除处理程序
WARNING: 如果Handler使用了文件或数据库,则该方法线程不安全
:param hdlr:
:return:
"""
if hdlr in self.handlers:
self.handlers.remove(hdlr)