django捕获异常并将异常内容通过邮件发给管理员

112 阅读7分钟

日常生活中,我们无法时刻关注服务器运行状态,这就需要服务器触发特定情况或者报错时主动通知开发人员.....
常见主动通知有打电话、发短信、发邮件等,但是大部分都要支付相应的费用,但邮件免费又好用。本文将用django搭建一个调用api时发生异常时给开发人员发送邮件的demo

youjian.png

环境准备

  • python 3.6+
  • django 3+

创建项目

django-admin startproject ApiErrorEmail

终端执行命令后,应该会出现下面这种目录树
tu_1.png

尝试运行项目

python manage.py runserver 0.0.0.0:8000

出现下图情况就说明项目能够正常运行
tu_2.png

发送邮件模块

  • 打开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
    

    yooujian.png

日志模块处理

  • 想要将报错日志发送到邮件,就必须先拿到报错信息的文本
    我们可以重写logger模块的HTTPHandler实现这一步操作,HTTPHandler是专门将日志通过指定url发送到相应服务器进行处理的类
    我们在项目根目录新建log_tool.py模块重写一个HTTPHandler

    import 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),
    ]
    
  • 做完上述步骤,就依次测试四个接口调用时,日志输出情况以及邮件发送情况



demo代码地址、文档引用地址