Sailboat异常监控和钉钉机器人通知功能的编写
异常信息的捕获代码在执行者的performer() 中,对应的代码如下:
通过communicate() 方法获取到的输出和错误信息是分开的,我们通过if语句便可实现项目运行异常的监控。检测到有异常产生时将异常信息交给信息整理器,最后将整理好的信息通过钉钉机器人接口发送给指定的人。
捕获到的异常信息分为用户主动记录和被动产生,用户主动记录的方法如:
这种方式产生的异常内容大体如下:
这种异常信息的前缀带有明显的标识。被动产生的异常如ValueError、IndexError和ModuleNotFoundError等,内容大体如下:
被动产生的异常中会有Traceback关键词。根据这两个特点,异常信息的提取和分类整理就变得很容易了。考虑到不同团队使用的信息整理方式和消息通知方式差异,这里预留相应的接口,Sailboat将实现简单的异常信息整理和钉钉机器人通知功能,有其他通知需求的团队可以根据Sailboat设定的接口实现通知功能。代码片段6-11为Sailboat设定的异常信息整理和消息通知接口的完整代码。
这些代码将写在sailboat/interface.py文件中。接口的代码逻辑很清晰:
- 警报器中的接收者接收异常信息并将其传递给异常监控器中的接收器。
- 异常监控器中的接收器将消息交给拆分车间和重组车间,最后将信息返回给调用方。
- 警报器中的接收者拿到整理好的异常信息后便交给发送者。
- 警报器中的发送者将整理好的异常信息发送给管理员。
使用接口的方法就是在编写类的时候继承预留的Monitor或者Alarm,并实现约定的方法。接下来我们为Sailboat编写默认的信息整理和消息发送的功能代码。
打开钉钉,组建一个钉钉群。然后点击群右上角的菜单扩展符“…”,并在弹出的面板中选择如图6-24所示的“添加机器人”选项。
选择后会弹出如图6-25所示的机器人选择面板。
选择右下角的“自定义 通过Webhook接入自定义服务”选项。随后会弹出确认对话框,点击“添加”按钮,在弹出的“添加机器人”对话框中为机器人设置名字,并选择合适的安全设置,最后点击“完成”按钮。“添加机器人”对话框如图6-26所示。
三种安全设置中,“自定义关键词”相对简单,推荐选择该选项。建议非常注重信息安全的读者选择“加签”选项。完成机器人基本信息的设置后,会弹出记录着机器人access_token的对话框,如图6-27所示。
此对话框中的Webhook栏里的URL和access_token十分重要,是驱动钉钉群内机器人的凭证。钉钉机器人支持的文本格式和消息发送方式均记录在钉钉开发文档中,点击“设置说明”按钮便会跳转到该文档。我们只需要按照文档给出的方式即可驱动钉钉机器人发送丰富的消息,此处不再赘述。代码片段6-12为Sailboat的异常监控对象MarkdownMonitor的完整代码。
from datetime import datetime
from interface import Monitor
class MarkdownMonitor(Monitor):
def __init__(self):
self.keyword = "Alarm"
self.err_image = ""
self.traceback_iamge = ""
def push(self, txt, occurrence, timer):
"""接收器
被捕获到的异常将会送到这里
"""
# 将信息按行分割
message = []
line = ""
for i in txt:
if i != "\n":
line += i
else:
message.append(line)
line = ""
err, traceback, res = self.extractor(message)
content = self.recombination(err, traceback, res, occurrence, timer)
return content
def extractor(self, message):
"""拆分车间
根据需求拆分异常信息
"""
result = []
err_number = 0
traceback_number = 0
for k, v in enumerate(message):
# 异常分类
if "ERROR" in v:
# 类别数量统计
err_number += 1
# 放入信息队列
result.append(v)
if "Traceback" in v:
# 类别数量统计
traceback_number += 1
# 放入信息队列
result += message[k:]
return err_number, traceback_number, result
def recombination(self, err, traceback, res, occurrence, timer):
"""重组车间
异常信息重组
"""
title = "Traceback" if traceback else "Error"
image = self.traceback_iamge if traceback else self.err_image
err_message = "\n\n > ".join(res)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 按照钉钉文档中MrakDown格式示例构造信息
article = "#### TOTAL -- Error Number: {}, Traceback Number: {} \n".format(err, traceback) + \
"> ![screenshot] ({}) \n\n".format(image) + \
"> **Error message** \n\n" + \
"> {} \n\n".format(err_message) + \
"> **Timer**\n\n {} \n\n".format(timer) + \
"> -------- \n\n" + \
"> **Other information** \n\n" + \
"> occurrence Time: {} \n\n".format(occurrence) + \
"> Send Time: {} \n\n".format(now) + \
"> Message Type: {}".format(self.keyword)
content = {
"msgtype": "markdown",
"markdown": {
"title": title,
"text": article
}
}
return content
这里通过for循环将异常信息进行分类并统计,重组时按照钉钉开发文章中的MarkDown格式构造消息。MarkdownMonitor对象写在sailboat/supervise/monitors.py文件中,它继承了Sailboat预留的接口对象Monitor,并按照约定重写了push()、extractor()和recombination()方法。代码片段6-13为Sailboat的警报器对象DingAlarm的完整代码。
import hashlib
import hmac
import time
import base64
import json
import logging
import requests
from urllib.parse import quote_plus
from interface import Alarm
from supervise.monitors import MarkdownMonitor
class DingAlarm(Alarm):
def __init__(self):
self.access_key = "xxx"
self.secret = "GQSxx"
self.token = "xxx"
self.headers = {
"Content-Type": "application/json;charset=UTF-8"
}
self.monitor = MarkdownMonitor()
def receive(self, txt, occurrence, timer):
"""接收者
接收异常信息,将其进行处理后交给发送者
"""
content = self.monitor.push(txt, occurrence, timer)
self.sender(content)
@staticmethod
def _sign(timestamps, secret, mode=False):
"""钉钉签名计算
根据钉钉文档指引计算签名信息
"""
if not isinstance(timestamps, str):
# 如果钉钉机器人的安全措施为密匙,那么按照文档指引传入的是字符串,反之为数字
# 加密时需要转为字节,所以在这里要确保时间戳为字符串
timestamps = str(timestamps)
mav = hmac.new(secret.encode("utf8"), digestmod=hashlib.sha256)
mav.update(timestamps.encode("utf8"))
result = mav.digest()
# 对签名值进行Base64编码
signature = base64.b64encode(result).decode("utf8")
if mode:
# 可选择是否将签名值进行URL编码
signature = quote_plus(signature)
return signature
def sender(self, message):
"""发送者
将重组后的信息发送到端
"""
timestamps = int(time.time()) * 1000
sign = self._sign(timestamps, self.secret, True)
# 构造链接
url = self.token + "×tamp=%s&sign=%s" % (timestamps, sign)
# 通过钉钉机器人将消息发送到顶顶群
resp = requests.post(url, headers=self.headers, json=message)
# 根据返回的错误判断消息发送状态
err = json.loads(resp.text)
if err.get("errcode"):
logging.warning(err)
return False
else:
logging.info("Message Sender Success")
return True
如果在添加机器人时选择的安全设置选项是“自定义关键词”,那么通过Webhook栏给出的URL便可驱动钉钉机器人。如果选择的是“加签”选项,那么就得按照钉钉开发文档中介绍的签名计算方式计算签名,请求时必须携带计算得到的签名字符串。警报器对象DingAlarm中的sign()方法为对应的签名计算方法,使用时传入约定的参数即可。DingAlarm对象写在sailboat/supervise/alarms.py文件中,它继承了Sailboat预留的接口对象Alarm,并按照约定重写了__init__()、receive()和sender()方法。
异常监控和消息通知的功能启动很简单,只需要在执行者的代码中引入DingAlarm对象,然后加上异常判断的if语句,并将异常信息传给DingAlarm对象的接收者receive()即可。对应的代码改动如下:
至此,我们完成了Sailboat主体功能代码的编写。它现在具备了本节开头时罗列的大部分功能,例如:
- 支持非框架的Python项目,且保留框架项目的接口。
- 与Scrapyd类似的项目部署功能。
- 项目异常监控和通知功能。
- 用户注册和登录。
- 权限管理。
- 动态添加或删除定时任务。
- 项目运行日志的收集。
剩下一些展示类API、数据更新接口和数据删除接口的实现,这些并不是本书的重点,对此感兴趣的读者可以动手完善Sailboat的功能或者对其进行扩展。
本节小结
这一节的内容非常多,收获颇丰。在Sailboat的设计和开发过程中,我们吸取了Scrapyd的很多优点,同时也做了不少的创新,最后打造出一个适用于任何Python项目的项目管理平台。需要注意的是,Scrapyd和Sailboat都是单机服务,单机性能有限且存在宕机风险,更好的选择是分布式调度平台。