Python 內建logging 使用詳細講

jacknie23 發表於 2022-07-11
Python

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不是原子操作,也就是說其不是執行緒安全的)

主要參考資料

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

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