Python使用logging模塊的SMTPHandler發送告警日誌郵件

Synopsis: 如果你想使用 Python 的內置模塊 logging 中的 SMTPHandler 將出錯時的日誌,通過郵件的方式發送給管理員的話,可能你會遇到很多坑,本文將解決諸如 socket.timeout: timed out 和 smtplib.SMTPServerDisconnected: Connection unexpectedly closed: timed out 等錯誤,親測有效

創建 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.qq.com', 25),
        fromaddr='[email protected]',
        toaddrs='接收報警郵件的地址',
        subject='服務器出現問題啦!!!',
        credentials=('[email protected]', '客戶端授權密碼'))
    # 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()

運行腳本後,郵箱未收到信息,但是在垃圾箱中有新的出現。

郵箱信息

郵箱的 SMTP 服務器地址爲 smtp.qq.com,其中 非 SSL 協議端口號 爲 25, SSL 協議端口號 爲 465/587,此api不支持協議端口,於是對其進行了修改:

import logging
import sys
from logging.handlers import SMTPHandler
import time


class CompatibleSMTPSSLHandler(SMTPHandler):
    """
    官方的SMTPHandler不支持SMTP_SSL的郵箱,這個可以兩個都支持,並且支持郵件發送頻率限制
    """

    def __init__(self, mailhost, fromaddr, toaddrs: tuple, subject,
                 credentials=None, secure=None, timeout=5.0, is_use_ssl=True, mail_time_interval=0):
        """

        :param mailhost:
        :param fromaddr:
        :param toaddrs:
        :param subject:
        :param credentials:
        :param secure:
        :param timeout:
        :param is_use_ssl:
        :param mail_time_interval: 發郵件的時間間隔,可以控制日誌郵件的發送頻率,爲0不進行頻率限制控制,如果爲60,代表1分鐘內最多發送一次郵件
        """
        super().__init__(mailhost, fromaddr, toaddrs, subject,
                         credentials, secure, timeout)
        self._is_use_ssl = is_use_ssl
        self._time_interval = mail_time_interval
        self._msg_map = dict()  # 是一個內容爲鍵時間爲值得映射

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

        Format the record and send it to the specified addressees.
        """
        from threading import Thread
        if sys.getsizeof(self._msg_map) > 10 * 1000 * 1000:
            self._msg_map.clear()
        Thread(target=self.__emit, args=(record,)).start()

    def __emit(self, record):
        if record.msg not in self._msg_map or time.time() - self._msg_map[record.msg] > self._time_interval:
            try:
                import smtplib
                from email.message import EmailMessage
                import email.utils
                t_start = time.time()
                port = self.mailport
                if not port:
                    port = smtplib.SMTP_PORT
                smtp = smtplib.SMTP_SSL(self.mailhost, port,
                                        timeout=self.timeout) if self._is_use_ssl else 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()
                print('{}發送郵件給 {} 成功,用時{} ,發送的內容是--> {}\033[0;35m!!!請去郵箱檢查,可能在垃圾郵件中\033[0m'.format(self.fromaddr,
                                                                                                   self.toaddrs, round(
                        time.time() - t_start, 2), record.msg))
                self._msg_map[record.msg] = time.time()
            except Exception:
                self.handleError(record)

        else:
            pass
            print('郵件發送太頻繁,此次不發送這個郵件內容:{}'.format(record.msg))
  1. 增加一個頻率控制的參數,比如要設置一個報警郵件,異常時候通知我們,但假設1分鐘內異常幾千次,那是不需要發幾千次相同日誌的,handler自帶頻率限制,使用的時候一秒鐘調用運行logger.waning(‘某某報警’)幾萬次都沒問題,直接自動忽略相同內容的報警信息,不會發送郵件。
  2. 發郵件時候另開線程,發郵件是需要一段時間的,運氣不好時候發郵件需要兩三秒,把主線程阻塞兩三秒了,另開線程不會有阻塞

將原來SMTPHandler替換爲CompatibleSMTPSSLHandler即可,
這樣就可以正常接受郵箱信息了

發佈了22 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章