Python使用logging模块的SMTPHandler发送告警日志邮件
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 服务,如下图所示:
然后,你在 Python 3 代码中想使用 163 邮箱来发送邮件的话,认证的用户名就是你的邮箱地址,但是密码不是你的登录密码,而是 客户端授权密码
,所以需要你先开启并生成这个密码,如下图所示:
注意:
163 邮箱的 SMTP 服务器地址为 smtp.163.com
,其中 非 SSL 协议端口号 为 25
, SSL 协议端口号 为 465/994
2.2 本地测试
运行此脚本:
然后你应该就会收到一封包含应用程序出错信息的邮件:
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,也能正常收到代码出错的告警邮件了!
0 条评论
评论者的用户名
评论时间暂时还没有评论.