Python使用logging模块的SMTPHandler发送告警日志邮件

  • 原创
  • Madman
  • /
  • /
  • 0
  • 7903 次阅读

Synopsis: 如果你想使用 Python 的内置模块 logging 中的 SMTPHandler 将出错时的日志,通过邮件的方式发送给管理员的话,可能你会遇到很多坑,本文将解决诸如 socket.timeout: timed out 和 smtplib.SMTPServerDisconnected: Connection unexpectedly closed: timed out 等错误,亲测有效

1. SMTPHandler

http://www.madmalls.com/blog/post/logging-for-python/#13-handler 中介绍了 Python 3 内置模块 logging 中的常用 Handler,如果我们既想将 logging.INFO 级别的日志保存到访问日志文件中,又想在应用出错时,将 logging.ERROR 及以上级别的日志通过邮件发送给管理员的话,就需要使用 logging.handlers.SMTPHandler()

2. 163邮箱非SSL(端口号25)

创建 test_smtphandler.py

import logging
from logging.handlers import SMTPHandler
import sys


def main():
    # 1. 创建 logger 实例
    logger = logging.getLogger('test-smtphandler')
    # 2. 设置 logger 实例的日志级别,默认是 logging.WARNING
    logger.setLevel(logging.INFO)
    # 3. 创建 Handler
    # 注意 163 邮箱要求 fromaddr 和你发送邮件的邮箱(即你的邮箱账号)要一致
    mail_handler = SMTPHandler(
        mailhost=('smtp.163.com', 25),
        fromaddr='xxx@163.com',
        toaddrs='接收报警邮件的地址',
        subject='[madmalls.com] 服务器出错了',
        credentials=('xxx@163.com', '客户端授权密码'))
    # 4. 单独设置 mail_handler 的日志级别为 ERROR
    mail_handler.setLevel(logging.ERROR)
    # 5. 将 Handler 添加到 logger 中
    logger.addHandler(mail_handler)

    # 6. 应用的业务代码(故意出错)
    try:
        x = 1 / 0
    except Exception:
        logger.error('[计算出错了] x = 1 / 0', exc_info=sys.exc_info())


if __name__ == '__main__':
    main()

2.1 开启 SMTP 服务

我将使用 stmp.163.com 的邮箱来发送邮件,首先你需要开通 SMTP 功能。访问 https://mail.163.com,登录你的账号之后,依次单击 设置POP3/SMTP/IMAP,开启 SMTP 服务,如下图所示:

1 163邮箱开启SMTP

然后,你在 Python 3 代码中想使用 163 邮箱来发送邮件的话,认证的用户名就是你的邮箱地址,但是密码不是你的登录密码,而是 客户端授权密码,所以需要你先开启并生成这个密码,如下图所示:

2 163邮箱生成授权密码

注意:

163 邮箱的 SMTP 服务器地址为 smtp.163.com,其中 非 SSL 协议端口号25SSL 协议端口号465/994

2.2 本地测试

运行此脚本:

$ python test_smtphandler.py

然后你应该就会收到一封包含应用程序出错信息的邮件:

3 非SSL 25号端口

2.3 腾讯云VPS测试

但是,如果你在腾讯云等 VPS 上运行时,由于它们的防火墙默认关闭了 25 号端口,所以你在执行脚本时会报错 socket.timeout: timed out

$ python3 test-smtphandler.py
--- Logging error ---
Traceback (most recent call last):
  File "test-smtphandler.py", line 26, in main
    x = 1 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/python-3.6/lib/python3.6/logging/handlers.py", line 1010, in emit
    smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout)
  File "/usr/local/python-3.6/lib/python3.6/smtplib.py", line 251, in __init__
    (code, msg) = self.connect(host, port)
  File "/usr/local/python-3.6/lib/python3.6/smtplib.py", line 336, in connect
    self.sock = self._get_socket(host, port, self.timeout)
  File "/usr/local/python-3.6/lib/python3.6/smtplib.py", line 307, in _get_socket
    self.source_address)
  File "/usr/local/python-3.6/lib/python3.6/socket.py", line 724, in create_connection
    raise err
  File "/usr/local/python-3.6/lib/python3.6/socket.py", line 713, in create_connection
    sock.connect(sa)
socket.timeout: timed out
Call stack:
  File "test-smtphandler.py", line 32, in <module>
    main()
  File "test-smtphandler.py", line 28, in main
    logger.error('[计算出错了] x = 1 / 0', exc_info=sys.exc_info())
Message: '[计算出错了] x = 1 / 0'
Arguments: ()

那么此时,你就只能使用 SSL 协议的 465/994 端口号 了,但是你又会遇到问题,请接着看

3. 163邮箱SSL(端口号465/994)

查看 https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SMTPHandler 文档,得知 logging.handlers.SMTPHandler 只支持 TLS 协议,比如:

mail_handler = SMTPHandler(
    mailhost=('smtp.gmail.com', 587),
    fromaddr='xxx@gmail.com',
    toaddrs='接收报警邮件的地址',
    subject='[madmalls.com] 服务器出错了',
    credentials=('xxx@gmail.com', '邮箱密码'),
    secure=())  # TLS协议需要提供 secure

3.1 只修改端口号时

但是,163 邮箱只支持 SSL 协议,如果你强行修改代码为:

mail_handler = SMTPHandler(
    mailhost=('smtp.163.com', 465),
    fromaddr='xxx@163.com',
    toaddrs='接收报警邮件的地址',
    subject='[madmalls.com] 服务器出错了',
    credentials=('xxx@gmail.com', '客户端授权密码'),
    secure=())

你执行脚本时,会报错 smtplib.SMTPServerDisconnected: Connection unexpectedly closed: timed out

$ python stmptest.py
--- Logging error ---
Traceback (most recent call last):
  File "stmptest.py", line 27, in main
    x = 1 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Python36\lib\smtplib.py", line 386, in getreply
    line = self.file.readline(_MAXLINE + 1)
  File "C:\Python36\lib\socket.py", line 586, in readinto
    return self._sock.recv_into(b)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Python36\lib\logging\handlers.py", line 988, in emit
    smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout)
  File "C:\Python36\lib\smtplib.py", line 251, in __init__
    (code, msg) = self.connect(host, port)
  File "C:\Python36\lib\smtplib.py", line 337, in connect
    (code, msg) = self.getreply()
  File "C:\Python36\lib\smtplib.py", line 390, in getreply
    + str(e))
smtplib.SMTPServerDisconnected: Connection unexpectedly closed: timed out
Call stack:
  File "stmptest.py", line 33, in <module>
    main()
  File "stmptest.py", line 29, in main
    logger.error('[计算出错了] x = 1 / 0', exc_info=sys.exc_info())
Message: '[计算出错了] x = 1 / 0'
Arguments: ()

3.2 重写emai()方法,支持SSL协议

怎么办呢?需要修改 logging.handlers.SMTPHandler 类方法 emit() 的实现,我们需要自己创建一个类,继承成 logging.handlers.SMTPHandler,然后覆盖父类的 emit() 方法,让它支持 SSL 即可

可能你的内置模块 logging 版本与我的不一样,无论如何,你只需要将原 emit() 方法中的 smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout) 修改成 smtp = smtplib.SMTP_SSL(self.mailhost, port) 即可

比如,我的 logging.handlers.SMTPHandler 类方法 emit() 代码如下:

def emit(self, record):
    """
    Emit a record.

    Format the record and send it to the specified addressees.
    """
    try:
        import smtplib
        from email.message import EmailMessage
        import email.utils

        port = self.mailport
        if not port:
            port = smtplib.SMTP_PORT
        smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout)
        msg = EmailMessage()
        msg['From'] = self.fromaddr
        msg['To'] = ','.join(self.toaddrs)
        msg['Subject'] = self.getSubject(record)
        msg['Date'] = email.utils.localtime()
        msg.set_content(self.format(record))
        if self.username:
            if self.secure is not None:
                smtp.ehlo()
                smtp.starttls(*self.secure)
                smtp.ehlo()
            smtp.login(self.username, self.password)
        smtp.send_message(msg)
        smtp.quit()
    except Exception:
        self.handleError(record)

所以,我们的 test_smtphandler.py 代码最终如下:

import logging
from logging.handlers import SMTPHandler
import sys


class SSLSMTPHandler(SMTPHandler):
    def emit(self, record):
        '''
        Overwrite the logging.handlers.SMTPHandler.emit function with SMTP_SSL.
        Emit a record.
        Format the record and send it to the specified addressees.
        '''
        try:
            import smtplib
            from email.message import EmailMessage
            import email.utils

            port = self.mailport
            if not port:
                port = smtplib.SMTP_PORT
            smtp = smtplib.SMTP_SSL(self.mailhost, port)  # 修改这一行代码即可
            msg = EmailMessage()
            msg['From'] = self.fromaddr
            msg['To'] = ','.join(self.toaddrs)
            msg['Subject'] = self.getSubject(record)
            msg['Date'] = email.utils.localtime()
            msg.set_content(self.format(record))
            if self.username:
                if self.secure is not None:
                    smtp.ehlo()
                    smtp.starttls(*self.secure)
                    smtp.ehlo()
                smtp.login(self.username, self.password)
            smtp.send_message(msg)
            smtp.quit()
        except Exception:
            self.handleError(record)


def main():
    # 1. 创建 logger 实例
    logger = logging.getLogger('test-smtphandler')
    # 2. 设置 logger 实例的日志级别,默认是 logging.WARNING
    logger.setLevel(logging.INFO)
    # 3. 创建 Handler
    # 注意 163 邮箱要求 fromaddr 和你发送邮件的邮箱(即你的邮箱账号)要一致
    mail_handler = SSLSMTPHandler(
        mailhost=('smtp.163.com', 465),
        fromaddr='xxx@163.com',
        toaddrs='接收报警邮件的地址',
        subject='[madmalls.com] 服务器出错了',
        credentials=('xxx@163.com', '客户端授权密码'))  # 不需要提供 secure 参数
    # 4. 单独设置 mail_handler 的日志级别为 ERROR
    mail_handler.setLevel(logging.ERROR)
    # 5. 将 Handler 添加到 logger 中
    logger.addHandler(mail_handler)

    # 6. 应用的业务代码(故意出错)
    try:
        x = 1 / 0
    except Exception:
        logger.error('[计算出错了] x = 1 / 0', exc_info=sys.exc_info())


if __name__ == '__main__':
    main()

此时,就算你使用腾讯云 VPS,也能正常收到代码出错的告警邮件了!

未经允许不得转载: LIFE & SHARE - 王颜公子 » Python使用logging模块的SMTPHandler发送告警日志邮件

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.