【python介面自動化】- logging日誌模組

miki_peng發表於2020-08-02

前言:我們之前執行程式碼時都是將日誌直接輸出到控制檯,而實際專案中常常需要把日誌儲存到檔案,便於查閱,如執行時間、描述資訊以及錯誤或者異常發生時候的特定上下文資訊。

logging模組介紹

​ Python中自帶的logging模組提供了標準的日誌介面,在debug時使用往往會事半功倍。為什麼不直接使用print去輸出呢?這種方式對簡單的指令碼來說有用,對於複雜的系統來說相當於一個花瓶擺設,大量的print輸出很容易被遺忘在程式碼裡,並且print是標準輸出,這很難從一堆資訊裡去判斷哪些是你需要重點關注的。

​ logging的優勢就在於可以控制日誌的級別,把不需要的資訊進行過濾,且可以決定它輸出到什麼地方、如何輸出,還可以通過控制等級把特定等級的資訊輸出到特定的位置等。logging一共分為四個部分:

  • ? Loggers:日誌收集器,可供程式直接呼叫的介面,app通過呼叫提供的api來記錄日誌
  • ? Handlers:日誌處理器, 決定將日誌記錄分配至正確的目的地
  • ? Filters:日誌過濾器,對日誌資訊進行過濾, 提供更細粒度的日誌是否輸出的判斷
  • ? Formatters:日誌格式器,制定最終記錄列印的格式佈局

日誌等級

​ logging將logger的等級劃分成5個level,由低到高分別是DEBUG、INFO、WARNING、ERROE、CRITICAL,預設是WARNING級別,CRITICAL最高,相關等級說明如下:

Level 說明
DEBUG 輸出詳細的執行資訊,主要用於除錯,追蹤問題時使用
INFO 輸出正常的執行的資訊,一切按預期進行的情況
WARNING 一些意想不到的或即將會發生的情況,比如警告:記憶體空間不足,但不影響程式執行
ERROR 由於某些問題,程式的一些功能會受到影響,還可以繼續執行
CRITICAL 一個嚴重的錯誤,表明程式本身可能無法繼續執行

​ 這些等級的日誌中低包含高,比如INFO,會收集INFO及以上等級的日誌,DEBUG等級的日誌將不進行收集。下面我們來輸出這5個等級的日誌:

import logging

"""
logging模組預設收集的日誌是warning以上等級的

"""

a = 100
logging.debug(a)
logging.info('這是INFO等級的資訊')
logging.warning('這是WARNING等級的資訊')
logging.error('這是ERROR等級的資訊')
logging.critical('這是CRITICAL等級的資訊')

​ 輸出結果:

C:\software\python\python.exe D:/learn/test.py
WARNING:root:這是WARNING等級的資訊
ERROR:root:這是ERROR等級的資訊
CRITICAL:root:這是CRITICAL等級的資訊

Process finished with exit code 0

日誌收集器

​ 日誌是怎麼被收集和輸出的呢?答案就是日誌收集器,設定一個收集器,把指等級的日誌資訊輸出到指定的地方,控制檯或檔案等,其工作過程大致如下:

【python介面自動化】- logging日誌模組

​ logging中預設的日誌收集器是root,收集等級預設是WARNING,我們可以通過setLevel來改變它的收集等級。

# 獲取預設的日誌收集器root
my_log = logging.getLogger()
# 設定預設的日誌收集器等級
my_log.setLevel("DEBUG")  # 日誌將全部被收集

a = 100
logging.debug(a)
logging.info('這是INFO等級的資訊')
logging.warning('這是WARNING等級的資訊')
logging.error('這是ERROR等級的資訊')
logging.critical('這是CRITICAL等級的資訊')

​ 輸出結果:

C:\software\python\python.exe D:/learn/test.py
DEBUG:root:100
INFO:root:這是INFO等級的資訊
WARNING:root:這是WARNING等級的資訊
ERROR:root:這是ERROR等級的資訊
CRITICAL:root:這是CRITICAL等級的資訊

Process finished with exit code 0

​ 除了使用預設的日誌收集器,我們也可以自己建立一個收集器logging.getLogger,如下:

import logging

my_logger = logging.getLogger('my_logger')	# 建立logging物件
my_logger.setLevel('INFO')	# 設定日誌收集等級

a = 100
logging.debug(a)
logging.info('這是INFO等級的資訊')
logging.warning('這是WARNING等級的資訊')
logging.error('這是ERROR等級的資訊')
logging.critical('這是CRITICAL等級的資訊')

​ 輸出結果:

C:\software\python\python.exe D:/learn/test.py
WARNING:root:這是WARNING等級的資訊
ERROR:root:這是ERROR等級的資訊
CRITICAL:root:這是CRITICAL等級的資訊

Process finished with exit code 0

日誌處理器

​ 上面例子中設定的收集器都是輸出到控制檯,除此我們還可以輸出到檔案中。

​ Handlers(處理器)的作用就是將logger發過來的資訊進行準確地分配,送往正確的地方。比如,送往控制檯、檔案或者是兩者。它決定了每個日誌收集器的行為,是建立收集器之後需要配置的重點區域。每個Handler同樣有一個日誌級別,一個logger可以擁有多個handler也就是說logger可以根據不同的日誌級別將日誌傳遞給不同的handler。當然也可以相同的級別傳遞給多個handler,這就根據需求來靈活的配置了。

​ 下面例項中設定了兩個handler,一個是輸出到控制檯,一個是輸出到檔案中。關鍵程式碼:

  • logging.StreamHandler:輸出到控制檯的處理器
  • logging.FileHandler:輸出到檔案的處理器
  • addHandler:新增處理器
  • removeHandler:移除處理器
import logging

my_logger = logging.getLogger('my_logger')
my_logger.setLevel('INFO')

# 建立一個輸出到控制檯的處理器
sh = logging.StreamHandler()
sh.setLevel("ERROR")    # 設定處理器的輸出等級
my_logger.addHandler(sh)    # 將處理器繫結到日誌收集器上

# 建立一個輸出到檔案的處理器
fh = logging.FileHandler("logs.logs", encoding="utf8")
fh.setLevel("INFO")
my_logger.addHandler(fh)
# my_logger.removeHandler(fh)	# 移除處理器

a = 100
my_logger.debug(a)
my_logger.info('這是INFO等級的資訊')
my_logger.warning('這是WARNING等級的資訊')
my_logger.error('這是ERROR等級的資訊')
my_logger.critical('這是CRITICAL等級的資訊')

執行結果:

C:\software\python\python.exe D:/learn/test.py
這是ERROR等級的資訊
這是CRITICAL等級的資訊

Process finished with exit code 0
【python介面自動化】- logging日誌模組

日誌過濾器

​ Filters可以實現比level更復雜的過濾功能,限制只有滿足過濾規則的日誌才會被輸出。比如我們定義了filter = logging.Filter('A.B'),並將這個Filter新增到了一個Handler上,則使用該Handler的Logger中只有名字帶A.B字首的Logger才能輸出其日誌。下面是一個簡單例項:

import logging

# 這是logger1
my_logger = logging.getLogger('A.C,B')
my_logger.setLevel('INFO')

# 這是logger2
my_logger2 = logging.getLogger('A.B')
my_logger2.setLevel('INFO')

# 建立一個處理器,兩個logger都使用這個處理器
sh = logging.StreamHandler()
sh.setLevel("ERROR")
my_logger.addHandler(sh)
my_logger2.addHandler(sh)

# 建立一個過濾器綁到處理器上
my_filter = logging.Filter(name='A.B')
sh.addFilter(my_filter)    # 把過濾器新增到處理器上
# sh2.removeFilter(my_filter)   # 移除過濾器

my_logger.debug('這是logger1-DEBUG等級的資訊')
my_logger.info('這是logger1-INFO等級的資訊')
my_logger.warning('這是logger1-WARNING等級的資訊')
my_logger.error('這是logger1-ERROR等級的資訊')
my_logger.critical('這是logger1-CRITICAL等級的資訊')

my_logger2.debug('這是logger2-DEBUG等級的資訊')
my_logger2.info('這是logger2-INFO等級的資訊')
my_logger2.warning('這是logger2-WARNING等級的資訊')
my_logger2.error('這是logger2-ERROR等級的資訊')
my_logger2.critical('這是logger2-CRITICAL等級的資訊')

​ 因為只有logger2滿足過濾器的條件,因此只會輸出logger2的日誌,執行結果如下:

C:\software\python\python.exe D:/learn/test.py
這是logger2-ERROR等級的資訊
這是logger2-CRITICAL等級的資訊

Process finished with exit code 0

​ filter方法用於具體控制傳遞的record記錄是否能通過過濾,如果該方法返回值為0表示不能通過過濾,非0表示可以通過過濾。

日誌格式器

​ 顧名思義,對日誌進行格式化,因為常規的日誌輸出並不直觀美觀,通過美化日誌的輸出格式,可以讓我們閱讀起來更加舒服。

​ format常用格式如下:

  • %(name)s: 列印收集器名稱
  • %(levelno)s: 列印日誌級別的數值
  • %(levelname)s: 列印日誌級別名稱
  • %(pathname)s: 列印當前執行程式的路徑,其實就是sys.argv[0]
  • %(filename)s: 列印當前執行程式名
  • %(funcName)s: 列印日誌的當前函式
  • %(lineno)d: 列印日誌的當前行號
  • %(asctime)s: 列印日誌的時間
  • %(thread)d: 列印執行緒ID
  • %(threadName)s: 列印執行緒名稱
  • %(process)d: 列印程式ID
  • %(message)s: 列印日誌資訊
import logging

my_logger = logging.getLogger('A.C,B')
my_logger.setLevel('INFO')

# 建立一個處理器
sh = logging.StreamHandler()
sh.setLevel("ERROR")
my_logger.addHandler(sh)
# 設定一個格式,並設定到處理器上
formatter = logging.Formatter('%(asctime)s - [%(filename)s-->line:%(lineno)d] - %(levelname)s: %(message)s')
sh.setFormatter(formatter)

my_logger.debug('這是logger1-DEBUG等級的資訊')
my_logger.info('這是logger1-INFO等級的資訊')
my_logger.warning('這是logger1-WARNING等級的資訊')
my_logger.error('這是logger1-ERROR等級的資訊')
my_logger.critical('這是logger1-CRITICAL等級的資訊')

​ 執行結果:

C:\software\python\python.exe D:/learn/test.py
2020-08-01 18:28:43,645 - [test.py-->line:17] - ERROR: 這是logger1-ERROR等級的資訊
2020-08-01 18:28:43,645 - [test.py-->line:18] - CRITICAL: 這是logger1-CRITICAL等級的資訊

Process finished with exit code 0

日誌滾動

​ 如果你用 FileHandler 儲存日誌,檔案的大小會隨著時間推移而不斷增大,最終有一天它會佔滿你所有的磁碟空間。Python 的 logging 模組提供了兩個支援日誌滾動的 FileHandler 類,分別是 RotatingFileHandler 和 TimedRotatingFileHandler,它就可以解決這個尷尬的問題。

  • ? RotatingFileHandler 的滾動時刻是日誌檔案的大小達到一定值,當達到指定值的時候,RotatingFileHandler會將日誌檔案重新命名存檔,然後開啟一個新的日誌檔案。
  • ? TimedRotatingFileHandler 是當某個時刻到來時就進行滾動,同 RotatingFileHandler 一樣,當滾動時機來臨時,TimedRotatingFileHandler 會將日誌檔案重新命名存檔,然後開啟一個新的日誌檔案。

​ 在實際應用中,我們通常會根據時間進行滾動,以下例項也以時間滾動為例,按大小滾動的同理:

import logging
from logging.handlers import TimedRotatingFileHandler

my_logger = logging.getLogger('A.C,B')
my_logger.setLevel('INFO')

# 建立一個處理器,使用時間滾動的檔案處理器
log_file_handler = TimedRotatingFileHandler(filename='log.log', when='D', interval=1, backupCount=10)
# log_file_handler.suffix = "%Y-%m-%d"
# log_file_handler.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}.log$")
log_file_handler.setLevel("ERROR")
my_logger.addHandler(log_file_handler)

# 設定一個格式,並設定到處理器上
formatter = logging.Formatter('%(asctime)s - [%(filename)s-->line:%(lineno)d] - %(levelname)s: %(message)s')
log_file_handler.setFormatter(formatter)

my_logger.debug('這是logger1-DEBUG等級的資訊')
my_logger.info('這是logger1-INFO等級的資訊')
my_logger.warning('這是logger1-WARNING等級的資訊')
my_logger.error('這是logger1-ERROR等級的資訊')
my_logger.critical('這是logger1-CRITICAL等級的資訊')

引數說明:

  • filename:日誌檔名;
  • when:是一個字串,用於描述滾動週期的基本單位,字串的值及意義如下:
    • S - Seconds
    • M - Minutes
    • H - Hours
    • D - Days
    • midnight - roll over at midnight
    • W{0-6} - roll over on a certain day; 0 - Monday
  • interval: 滾動週期,單位由when指定,比如:when='D',interval=1,表示每天產生一個日誌檔案;
  • backupCount: 表示日誌檔案的保留個數;

​ 除了上述引數之外,TimedRotatingFileHandler還有兩個比較重要的成員變數,它們分別是suffix和extMatch。suffix是指日誌檔名的字尾,suffix中通常帶有格式化的時間字串,filename和suffix由“.”連線構成檔名(例如:filename="test", suffix="%Y-%m-%d.log",生成的檔名為test.2020-08-01.log。extMatch是一個編譯好的正規表示式,用於匹配日誌檔名的字尾,它必須和suffix是匹配的,如果suffix和extMatch匹配不上的話,過期的日誌是不會被刪除的。比如,suffix=“%Y-%m-%d.log”, extMatch的只能是re.compile(r"^\d{4}-\d{2}-\d{2}.log$")。預設情況下,在TimedRotatingFileHandler物件初始化時,suffxi和extMatch會根據when的值進行初始化:

S:suffix="%Y-%m-%d_%H-%M-%S",extMatch=r"\^d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}";
M:suffix="%Y-%m-%d_%H-%M",extMatch=r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}";
H:suffix="%Y-%m-%d_%H",extMatch=r"^\d{4}-\d{2}-\d{2}_\d{2}";
D:suffxi="%Y-%m-%d",extMatch=r"^\d{4}-\d{2}-\d{2}";
MIDNIGHT:"%Y-%m-%d",extMatch=r"^\d{4}-\d{2}-\d{2}";
W:"%Y-%m-%d",extMatch=r"^\d{4}-\d{2}-\d{2}";

​ 如果對日誌檔名沒有特殊要求的話,可以不用設定suffix和extMatch,如果需要,一定要讓它們匹配上。

模組封裝

​ 一次封裝,一勞永逸,之後直接呼叫即可,封裝內容按需即可。

import logging
from logging.handlers import TimedRotatingFileHandler


class MyLogger(object):

    @staticmethod
    def create_logger():
        my_logger = logging.getLogger("my_logger")
        my_logger.setLevel("DEBUG")
        # 控制檯處理器
        stream_handler = logging.StreamHandler()
        stream_handler.setLevel("ERROR")
        my_logger.addHandler(stream_handler)
        # 使用時間滾動的檔案處理器
        log_file_handler = TimedRotatingFileHandler(filename='log.log', when='D', interval=1, backupCount=10)
        log_file_handler.setLevel("INFO")
        my_logger.addHandler(log_file_handler)
        
        formatter = logging.Formatter('%(asctime)s - [%(filename)s-->line:%(lineno)d] - %(levelname)s: %(message)s')
        stream_handler.setFormatter(formatter)
        log_file_handler.setFormatter(formatter)

        return my_logger


# 呼叫類的靜態方法,建立一個日誌收集器
log = MyLogger.create_logger()
log.info("test-info")
log.error("test-error")

​ 執行結果:

C:\software\python\python.exe D:/learn/test.py
2020-08-01 18:29:45,645 - [test.py-->line:28] - ERROR: test-error

Process finished with exit code 0

相關文章