日常生活中,我们无法时刻关注服务器运行状态,这就需要服务器触发特定情况或者报错时主动通知开发人员.....
常见主动通知有打电话、发短信、发邮件等,但是大部分都要支付相应的费用,但邮件免费又好用。本文将用django搭建一个调用api时发生异常时给开发人员发送邮件的demo
环境准备
- python 3.6+
- django 3+
创建项目
django-admin startproject ApiErrorEmail
终端执行命令后,应该会出现下面这种目录树
尝试运行项目
python manage.py runserver 0.0.0.0:8000
出现下图情况就说明项目能够正常运行
发送邮件模块
-
打开QQ邮箱网站,开启POP3/IMAP/SMTP服务,并拿到授权码
点击该地址了解POP3/IMAP/SMTP服务,并拿到QQ邮箱的授权码 wx.mail.qq.com/list/readte… -
在项目根目录创建 email_tool.py 模块,输入下面的代码
import smtplib from email.mime.text import MIMEText from email.utils import formataddr from threading import Thread SENDER_EMAIL = 'youemail@qq.com' # 服务器发信人的邮箱账号 SENDER_NAME = "Jun" # 服务器发信人名字 SENDER_AUTHORIZATION_CODE = "PyGo学习笔记" # 上一步骤拿到的授权码 ERROR_ACCEPT_USER_LIST = [ ("Jun", "开发的邮箱@qq.com"), ("收件人的名字", "可以和服务器发信人同一个邮箱号@qq.com"), ] def async_execute(func): def auth(*args, **kwargs): th = Thread(target=func, args=args, kwargs=kwargs) th.start() return auth @async_execute def send_text_email(accept_email_list, msg): """ 发送文本邮箱 :param accept_email_list: :param msg: :return: """ msg = MIMEText(msg, 'plain', 'utf-8') # 填写邮件内容 msg['From'] = formataddr((SENDER_NAME, SENDER_EMAIL)) # 括号里的对应发件人邮箱昵称、发件人邮箱账号 server = smtplib.SMTP_SSL("smtp.qq.com", 465) # 发件人邮箱中的SMTP服务器 server.login(SENDER_EMAIL, SENDER_AUTHORIZATION_CODE) for accept_email in accept_email_list: msg['To'] = formataddr(accept_email) server.sendmail(SENDER_EMAIL, [accept_email[1]], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件 server.quit() if __name__ == "__main__": send_text_email(ERROR_ACCEPT_USER_LIST, "测试邮件")
由于隐私关系,我将邮箱号、授权码都换了字符串, 读者需要根据上一步骤填入自己的邮件和授权码
换好后请另开一个终端,执行该脚本,并查看收件信箱是否收到了该测试邮件
python email_tool.py
日志模块处理
-
想要将报错日志发送到邮件,就必须先拿到报错信息的文本
我们可以重写logger模块的HTTPHandler实现这一步操作,HTTPHandler是专门将日志通过指定url发送到相应服务器进行处理的类
我们在项目根目录新建log_tool.py模块重写一个HTTPHandlerimport logging import traceback from logging.handlers import HTTPHandler from ApiErrorEmail.settings import LOG_FORMAT from email_tool import send_text_email, ERROR_ACCEPT_USER_LIST class LogHTTPHandler(HTTPHandler): def __init__(self, host, url, method="GET", secure=False, credentials=None, context=None, *args, **kwargs): super(LogHTTPHandler, self).__init__(host, url, method, secure, credentials, context) def emit(self, record): """ 详情可以查阅文档地址:https://docs.python.org/zh-cn/3/library/logging.handlers.html#httphandler 重写该方法,这个方法是将日志通过url发送到别的服务器进行处理 此处只需要将将日志处理并调用之前的发送邮件接口即可 """ data = self.mapLogRecord(record) # 返回日志所有信息 # 增加判断,只有等级大于ERROR的日志才做发送处理 # 日志等级可以查阅该文档:https://docs.python.org/zh-cn/3/library/logging.html#logging-levels if data["levelno"] >= logging.ERROR: # 当有调用堆栈时,stack_info是整个代码调用堆栈,方便报错时快速定位到报错日志 # 可以查阅文档:https://docs.python.org/zh-cn/3/library/logging.html?highlight=stack_info stack_info = data.get('stack_info', None) exc_text = None if not stack_info: exc_text = data.get('exc_text', None) data = LOG_FORMAT % data data += "\n" if stack_info: data += stack_info if exc_text: data += exc_text # 将日志发送到每一个开发人员的邮箱中 send_text_email(ERROR_ACCEPT_USER_LIST, data) -
然后对项目的日志处理进行配置,在ApiErrorEmail/settings.py 文件末尾追加以下内容
import os import sys LOG_PATH = os.path.join(os.path.abspath(".."), "api_error_email_log") if not os.path.exists(LOG_PATH): os.makedirs(LOG_PATH) def debug_filter(record): if record.levelname == 'DEBUG': return True return False def info_filter(record): if record.levelname == 'INFO': return True return False def warning_filter(record): if record.levelname == 'WARNING': return True return False def verify_filter(record): if "cs verify" in record.getMessage(): return True return False # 日志内容输出格式模板 # 详细格式查阅文档 https://docs.python.org/zh-cn/3/library/logging.html#formatter-objects LOG_FORMAT = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d %(funcName)s]:%(message)s' # django日志配置可以查阅:https://docs.djangoproject.com/zh-hans/5.0/howto/logging/#logging-how-to LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'default': { 'format': LOG_FORMAT } }, 'filters': { 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, 'debug_filter': { '()': 'django.utils.log.CallbackFilter', 'callback': debug_filter, }, 'info_filter': { '()': 'django.utils.log.CallbackFilter', 'callback': info_filter, }, 'warning_filter': { '()': 'django.utils.log.CallbackFilter', 'callback': warning_filter, }, 'verify_filter': { '()': 'django.utils.log.CallbackFilter', 'callback': verify_filter, } }, 'handlers': { 'console': { 'level': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'stream': sys.stdout, 'formatter': 'default' }, 'debug': { 'level': 'DEBUG', 'filters': ['require_debug_true', 'debug_filter'], 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join(LOG_PATH, "debug.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, 'verify': { 'level': 'DEBUG', 'filters': ['require_debug_true', 'verify_filter'], 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join(LOG_PATH, "verify.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, 'info': { 'level': 'INFO', 'filters': ['info_filter'], 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join(LOG_PATH, "info.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, 'warning': { 'level': 'WARNING', 'filters': ['warning_filter'], 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join(LOG_PATH, "warning.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, 'error': { 'level': 'ERROR', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join(LOG_PATH, "error.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, 'error_email': { 'level': 'ERROR', 'host': "", 'url': '', 'class': 'log_tool.LogHTTPHandler', # 这个日志处理类指定到我们重写的类里面 'filename': os.path.join(LOG_PATH, "error.log"), 'when': 'midnight', 'interval': 1, 'backupCount': 10, 'formatter': 'default', 'encoding': 'utf-8' }, }, 'loggers': { '': { 'handlers': ['info', 'console', 'error', 'warning', 'debug', 'verify'], 'level': 'DEBUG', }, 'error_email': { 'handlers': ['info', 'console', 'error', 'warning', "error_email"], 'level': 'DEBUG', } }, } -
日志完成了配置,此时所有通过调用 error、critical两个级别的日志输出都将输出到对应日志文件且将报错内容输出到api_error_email_log目录下且发送到每个设定的邮箱中
如果不主动调用error、critical,产生的错误信息只会输入到api_error_email_log,而不会发送到开发者邮件中
通常重要的接口产生错误才需要立即处理,不是很重要的接口,产生的异常就不是那么重要了,不需要那么及时通过邮件发送,只需要创建一个装饰器轻松完成这个需求
在项目根目录创建一个log.py模块import logging from django.http import JsonResponse log = logging.getLogger() log_error_email = logging.getLogger("error_email") def error_send_email(func): def check(request, *args, **kwargs): response = { "code": 500, "message": "服务器出现异常", "data": {} } try: return func(request, **kwargs) except Exception as e: # 此处捕捉日志 exception是等级error接口 log_error_email.exception(f"调用接口{func.__name__},参数 args:{args} kwargs:{kwargs} 失败:{e}") return JsonResponse(response) return check
编写接口,测试效果
-
所有准备完成,我们只需要创建几个接口测试一下,查看完成情况
项目根目录下创建 views.py, 输入以下代码from django.http import JsonResponse from log import log, error_send_email def test_common_require_api(request, *args, **kwargs): """ 微信公众号: PyGo学习笔记 测试普通接口 正常返回数据,输出一条info日志 """ response = { "code": 200, "message": "", "data": { "msg": "已经输出一条info日志" } } log.info("正常输出一条info日志") return JsonResponse(response) def test_common_error_require_api(request, *args, **kwargs): """ 微信公众号: PyGo学习笔记 测试下接口里面遇到报错,查看是否能收到邮件、以及错误日志的情况 """ response = { "code": 200, "message": "", "data": { "msg": "" } } # 此处加一行会报错的代码 a = 1 / 0 return JsonResponse(response) @error_send_email def test_protect_error_require_api(request, *args, **kwargs): """ 微信公众号: PyGo学习笔记 测试增加错误装饰器保护的接口,日志输出情况、以及邮件发送情况 """ response = { "code": 200, "message": "", "data": { "msg": "" } } # 此处加一行会报错的代码 a = 1 / 0 return JsonResponse(response) def test_api_protect_error_require_api(request, *args, **kwargs): """ 微信公众号: PyGo学习笔记 接口内增加保护,并触发异常输出到日志中 """ response = { "code": 200, "message": "", "data": { "msg": "" } } # 此处加一行会报错的代码 try: a = 1 / 0 except Exception as e: log.exception(f"test_api_protect_error_require_api 触发的异常:{e}") return JsonResponse(response) -
将上面三个接口添加到ApiErrorEmail/urls.py里面
from django.urls import path from views import * urlpatterns = [ path('test_common_require_api', test_common_require_api), path('test_common_error_require_api', test_common_error_require_api), path('test_protect_error_require_api', test_protect_error_require_api), path('test_api_protect_error_require_api', test_api_protect_error_require_api), ] -
做完上述步骤,就依次测试四个接口调用时,日志输出情况以及邮件发送情况
-
测试test_common_require_api接口,在浏览器输入地址:http://127.0.0.1:8000/test_common_require_api
就会看到info日志输出下图内容,邮箱并没有收到邮件
-
测试test_common_error_require_api接口,在浏览器输入地址:http://127.0.0.1:8000/test_common_error_require_api
就会看到error输出下图内容,邮箱没有收到邮件 -
测试test_protect_error_require_api接口,在浏览器输入地址:http://127.0.0.1:8000/test_protect_error_require_api
就会看到error输出下图内容,邮箱收到邮件 -
测试test_api_protect_error_require_api接口,在浏览器输入地址:http://127.0.0.1:8000/test_api_protect_error_require_api
就会看到error输出下图内容,邮箱没有收到邮件
-
demo代码地址、文档引用地址
- python日志文档地址: docs.python.org/zh-cn/3/lib…
- django 日志相关配置文档地址: docs.djangoproject.com/zh-hans/5.0…
- 代码下载地址:codeload.github.com/YangJunJ/ap…