Python 內置logging 使用詳細講

  • 2022 年 7 月 11 日
  • 筆記

logging 的主要作用

提供日誌記錄的介面和眾多處理模組,供用戶存儲各種格式的日誌,幫助調試程式或者記錄程式運行過程中的輸出資訊。

logging 日誌等級

logging 日誌等級分為五個等級,優先順序從高到低依次是 :

**CRITICAL; ** 程式嚴重錯誤

**ERROR; ** 程式錯誤/部分功能錯誤

**WARNING; ** 程式有發生錯誤的可能

**INFO; ** 程式正常運行時的資訊

DEBUG 程式調試資訊

默認的日誌的記錄等級為 WARNING, 即當日誌的等級大於獲等於 WARNING 時才會被記錄。

一般常用的記錄等級為 INFO,其用於記錄程式的正常運行的一些資訊(類似於print)。

當日誌的等級達到 WARNING 以上時,表明此時程式不能正常運行;

logging 的基礎函數

logging.basicConfig(**kwargs)

在沒有顯式的進行創建記錄器(logger)時,會默認創建一個root logger,而logging.basicConfig(**kwargs) 可以創建帶有默認的Formatter的streamHandle並將其添加到根日誌記錄器中來初始化基本配置。

比如

import logging

logging.debug('Debug code!')
logging.info('Run code!')
logging.warning('Watch out!')  
logging.error('This is an error')
logging.critical('This is a ciritical')

上面程式碼中 logging 並沒有顯式的創建logger( logging.getLogger ), 其在直接使用debug(), info(), warning(), error(), critical() 時會使用默認的 root logger,並會自動調用 自定義的或者默認的logging.basicConfig(**kwargs) 初始化 root logger。

自定義的 logging.basicConfig(**kwargs) 中的參數 有以下的主要的選項:

參數 功能
filename 指定保存日誌的文件名,用指定文件名創建一個FileHandler,記錄的日誌會保存到該文件中
format 指定輸出的格式和內容,默認是以冒號分割的levalname、name 和 message
datefmt 使用指定的日期/時間格式,與 time.strftime() 所接受的格式相同。
level 指定根日誌記錄器級別,默認為 logging.WARNING
stream 指定日誌的輸出流,可以指定輸出到sys.stderr,std.stdout 或 文件,默認輸出到sys.stderr。使用指定的流初始化StramHandler,注意:stream和filename參數不兼容,如果兩者同時使用,則會引發ValueError 錯誤

例如下面通過自定義 logging.basicConfig(**kwargs) 來初始化 root logger 來獲得DEBUG級別及以上的日誌記錄並保存到 log.txt 文件中。

import logging

logging.basicConfig(filename='./log.txt',
                        format='%(asctime)s-%(name)s-%(levelname)s-%(message)s-%(funcName)s:%(lineno)d',
                        level=logging.DEBUG)
 
logging.debug('Debug code!')
logging.info('Run code!')
logging.warning('Watch out!')  
logging.error('This is an error')
logging.critical('This is a ciritical')

logging 的四大組件(類)

Logger

除了根記錄器(root logger)外,最主要的是可以自己創建日誌記錄器。

通過模組級別的函數 logging.getLogger(name) 實例化記錄器

默認情況下,記錄器採用層級結構,通過 . 來區分不同的層級。比如 有個名叫 foo 的記錄器 則 foo.afoo.b 都是 foo 的子級記錄器。當然,最開始的或者說最上層的記錄器就是 root logger。如果 name=None,構建的是root logger。

可以直接用當前模組的名稱當作記錄器的名字 logging.getLogger(__name__)

子級記錄器通常不需要單獨設置日誌級別以及 Handler,如果子級記錄器沒有單獨設置,則它的行為會委託給父級。比如說,記錄器foo的級別為INFO,而foo.afoo.b 都不設置日誌級別。此時foo.afoo.b 會遵循foo 的級別設置,即只記錄大於等於INFO級別的日誌;而如果foo也沒設置的話,就會找到根記錄器root logger,root默認的級別為WARGING。

logger類的一些常用的方法

方法 功能描述
Logger.setLevel() 設置日誌器(Logger)將會處理的日誌消息級別
Logger.addHandler() 添加一個handler對象
Logger.removeHandler() 移除一個handler對象
Logger.addFilter() 添加一個filter對象
Logger.removeFilter() 移除一個filter對象
Logger.debug() 設置DEBUG級別的日誌記錄
Logger.info() 設置INFO級別的日誌記錄
Logger.warning() 設置WARNING級別的日誌記錄
Logger.error() 設置ERROR級別的日誌記錄
Logger.critical() 設置CRITICAL級別的日誌記錄
Logger.exception() 輸出堆棧追蹤資訊
Logger.log() 設置一個自定義的level參數來創建一個日誌記錄

logger 結合 後面要介紹的其他的三個組件可以實現以下的功能:

  • Logger需要通過handler將日誌資訊輸出到目標位置,目標位置可以是sys.stdout和文件等(這與logging.basicConfig(**kwargs) 設置中不太一致)。
  • 一個Logger可以設置不同的Handler,而不同的Handler可以將日誌輸出到不同的位置(不同的日誌文件),並且每個Handler都可以設置自己的filter從而實現日誌過濾,保留實際項目中需要的日誌。同時每個Handler也可以設置不同的Formatter,在每個Formatter實現同一條日誌以不同的格式輸出到不同的地方。

Handle

處理器;其可以控制記錄的日誌輸出到什麼地方(標準輸出/文件/…),同時處理器也可以添加 過濾器(filter)和格式控制器(formatter)來控制輸出的內容和輸出的格式。

其具有幾種常見的處理器:

  • logging.StreamHandler 標準串流處理器,將消息發送到標準輸出流、錯誤流 –> logging.StreamHandler(sys.stdout) # sys.stdout 表示的是指向控制台即標準輸出;當我們在 Python 中列印對象調用 print obj 時候,事實上是調用了 sys.stdout.write(obj+’\n’)。

    print 將你需要的內容列印到了控制台,然後追加了一個換行符

  • logging.FileHandler 文件處理器,將消息發送到文件 –> logging.FileHandler(log_path)

  • logging.RotatingFileHandler 文件處理器,文件達到指定大小後,啟用新文件存儲日誌

  • logging.TimedRotatingFileHandler 文件處理器,日誌以特定的時間間隔輪換日誌文件

handle 類的一些常用的方法

Handler.setLevel() 設置處理器將會處理的日誌消息的最低嚴重級別
Handler.setFormatter() 為處理器設置一個格式對象
Handler.addFilter() 為處理器添加一個過濾器對象
Handler.removeFilter() 為處理器刪除一個過濾器對象
logging.StramHandler() 將日誌消息發送到輸出Stream,如std.out,std.err
logging.FilterHandler() 將日誌消息發送到磁碟文件,默認情況文件大小會無線增長
RotationFileHandler() 將日誌消息發送到磁碟文件,支援日誌文件按大小切割
TimeRotatingFileHandler() 將日誌消息發送到磁碟文件,並支援日誌文件按時間切割
logging.handers.HTTPHandler() 將日誌消息通過GET或POST的方式發送給一個HTTP伺服器
logging.handlers.SMTPHandler() 將日誌消息發送email地址

Filter

filter組件用來過濾 logger 對象,一個 filter 可以直接添加到 logger對象上,也可以添加到 handler 對象上,而如果在logger和handler中都設置了filter,則日誌是先通過logger的filter,再通過handler的filter。由於所有的資訊都可以經過filter,所以filter不僅可以過濾資訊,還可以增加資訊。

Filter 類的實例化對象可以通過 logging.Filter(name) 來創建,其中name 為 記錄器的名字,如果沒有創建過該名字的記錄器,就不會輸出任何日誌:

filter = logging.Filter("foo.a")

基本過濾器類只允許低於指定的日誌記錄器層級結構中低於特定層級的事件,例如 這個用 foo.a 初始化的過濾器,則foo.a.b;foo.a.c 等日誌記錄器記錄的日誌都可以通過過濾器,而foo.c; a.foo 等就不能通過。如果name為空字元串,則所有的日誌都能通過。

Filter 類 有 三個方法 :

  • addFilter(filter) : 為 logger(logger..addFilter(filter)) 或者 handler(handler..addFilter(filter)) 增加過濾器
  • removeFilter(filter) : 為 logger 或者 handler 刪除一個過濾器
  • filter(record) : 表示是否要記錄指定的記錄?返回零表示否,非零表示是。一般自定義Filter需要繼承Filter基類,並重寫filter方法

Formatter

格式化日誌的輸出;實例化:formatter = logging.Formatter(fmt=None,datefmt=None); 如果不指明 fmt,將默認使用 『%(message)s』 ,如果不指明 datefmt,將默認使用 ISO8601 日期格式。

其中 fmt 參數 有以下選項:

%(name)s Logger的名字
%(levelno)s 數字形式的日誌級別
%(levelname)s 文本形式的日誌級別;如果是logger.debug則它是DEBUG,如果是logger.error則它是ERROR
%(pathname)s 調用日誌輸出函數的模組的完整路徑名,可能沒有
%(filename)s 調用日誌輸出函數的模組的文件名
%(module)s 調用日誌輸出函數的模組名
%(funcName)s 調用日誌輸出函數的函數名
%(lineno)d 調用日誌輸出函數的語句所在的程式碼行
%(created)f 當前時間,用UNIX標準的表示時間的浮 點數表示
%(relativeCreated)d 輸出日誌資訊時的,自Logger創建以 來的毫秒數
%(asctime)s 字元串形式的當前時間。默認格式是 「2003-07-08 16:49:45,896」。逗號後面的是毫秒
%(thread)d 執行緒ID。可能沒有
%(threadName)s 執行緒名。可能沒有
%(process)d 進程ID。可能沒有
%(message)s 用戶輸出的消息; 假如有logger.warning(“NO Good”),則在%(message)s位置上是字元串NO Good

例如:

formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')		# -表示右對齊 8表示取8位
handler.formatter = formatter

datefmt 參數 有以下選項:

參數 含義
%y 兩位數的年份表示(00-99)
%Y 四位數的年份表示(000-9999)
%m 月份(01-12)
%d 月內中的一天(0-31)
%H 24小時制小時數(0-23)
%I 12小時制小時數(01-12)
%M 分鐘數(00=59)
%S 秒 (00-59)

例子:

formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s","%Y%m%d-%H:%M:%S")
handler.formatter = formatter

logging 的配置

  • conf 形式的配置

    在 loguser.conf 中 寫入相關的資訊

    [loggers]
    keys=root,fileLogger,rotatingFileLogger
    
    [handlers]
    keys=consoleHandler,fileHandler,rotatingFileHandler
    
    [formatters]
    keys=simpleFormatter
    
    [logger_root]
    level=INFO
    handlers=consoleHandler
    
    [logger_fileLogger]
    level=INFO
    handlers=fileHandler
    qualname=fileLogger
    propagate=0
    
    [logger_rotatingFileLogger]
    level=INFO
    handlers=consoleHandler,rotatingFileHandler
    qualname=rotatingFileLogger
    propagate=0
    
    [handler_consoleHandler]
    class=StreamHandler
    level=INFO
    formatter=simpleFormatter
    args=(sys.stdout,)
    
    [handler_fileHandler]
    class=FileHandler
    level=INFO
    formatter=simpleFormatter
    args=("logs/fileHandler_test.log", "a")
    
    [handler_rotatingFileHandler]
    class=handlers.RotatingFileHandler
    level=WARNING
    formatter=simpleFormatter
    args=("logs/rotatingFileHandler.log", "a", 10*1024*1024, 50)
    
    [formatter_simpleFormatter]
    format=%(asctime)s - %(module)s - %(levelname)s -%(thread)d : %(message)s
    datefmt=%Y-%m-%d %H:%M:%S
    

    在使用logger時,直接導入配置文件即可

    from logging import config
    
    with open('./loguser.conf', 'r', encoding='utf-8') as f:
    	## 載入配置
        config.fileConfig(f)
        ## 創建同名Logger,其按照配置文件的handle,formatter,filter方法初始化
        logger = logging.getLogger(name="fileLogger")
    
  • yaml 形式配置文件

    在 loguser.yaml文件 中 配置相關資訊

    version: 1
    disable_existing_loggers: False
    # formatters配置了日誌輸出時的樣式
    # formatters定義了一組formatID,有不同的格式;
    formatters:
      brief:
          format: "%(asctime)s - %(message)s"
      simple:
          format: "%(asctime)s - [%(name)s] - [%(levelname)s] :%(levelno)s: %(message)s"
          datefmt: '%F %T'
    # handlers配置了需要處理的日誌資訊,logging模組的handler只有streamhandler和filehandler
    handlers:
      console:
          class : logging.StreamHandler
          formatter: brief
          level   : DEBUG
          stream  : ext://sys.stdout
      info_file_handler:
          class : logging.FileHandler
          formatter: simple
          level: ERROR
          filename: ./logs/debug_test.log
      error_file_handler:
        class: logging.handlers.RotatingFileHandler
        level: ERROR
        formatter: simple
        filename: ./logs/errors.log
        maxBytes: 10485760 # 10MB #1024*1024*10
        backupCount: 50
        encoding: utf8
    
    loggers:
    #fileLogger, 就是在程式碼中通過logger = logging.getLogger("fileLogger")來獲得該類型的logger
      my_testyaml:
          level: DEBUG
          handlers: [console, info_file_handler,error_file_handler]
    # root為默認情況下的輸出配置, 當logging.getLogger("fileLoggername")裡面的fileLoggername沒有傳值的時候,
    # 就是用的這個默認的root,如logging.getLogger(__name__)或logging.getLogger()
    root:
        level: DEBUG
        handlers: [console]
    

    同樣的可以通過導入 yaml 文件載入配置

    with open('./loguser.yaml', 'r', encoding='utf-8') as f:
            yaml_config = yaml.load(stream=f, Loader=yaml.FullLoader)
            config.dictConfig(config=yaml_config)
    
        root = logging.getLogger()
        # 子記錄器的名字與配置文件中loggers欄位內的保持一致
        # loggers:
        #   my_testyaml:
        #       level: DEBUG
        #       handlers: [console, info_file_handler,error_file_handler]
        my_testyaml = logging.getLogger("my_testyaml")
    

logging 和 print 的區別

看起來logging要比print複雜多了,那麼為什麼推薦在項目中使用 logging 記錄日誌而不是使用print 輸出程式資訊呢。

相比與print logging 具有以下優點:

  • 可以通過設置不同的日誌等級,在 release 版本中只輸出重要資訊,而不必顯示大量的調試資訊;
  • print 將所有資訊都輸出到標準輸出中,嚴重影響開發者從標準輸出中查看其它數據;logging 則可以由開發者決定將資訊輸出到什麼地方,以及怎麼輸出;
  • 和 print 相比,logging 是執行緒安全的。(python 3中 print 也是執行緒安全的了,而python 2中的print不是)(執行緒安全是指在多執行緒時程式不會運行混亂;而python 2 中的print 分兩步列印資訊,第一列印字元串,第二列印換行符,如果在這中間發生執行緒切換就會產生輸出混亂。這就是為什麼python2的print不是原子操作,也就是說其不是執行緒安全的)

主要參考資料

//blog.csdn.net/weixin_41010198/article/details/89356417

//www.cnblogs.com/chenyibai/p/10676574.html