【譯】Python3.8官方Logging文檔(完整版)

注:文章很長,約一萬字左右,可以先收藏慢慢看喲


01、基礎部分

日誌是用來的記錄程序運行事件的工具。當程序員可以通過添加日誌打印的代碼來記錄程序運行過程中發生的某些事件時。這些事件包含了諸如變量數據在內的描述信息,當然也包括開發人員認爲重要的諸如日誌級別等信息。

 

什麼時候使用Loggin包

針對簡單的場景,Loggin包提供了一系列方便的函數。其中包括debug()、info()、warning()、error()以及 critical()。下面的表格列舉了什麼函數適用於什麼場景。

 

需求場景

適用工具

在命令行場景下,需要在控制檯打印輸出信息

print()

需要記錄一些程序正常運行的事件信息(例如運行狀態或者調試信息)

logging.info() (爲了記錄非常詳細的調試信息也可以使用 logging.debug() )

擔心程序運行時可能發生的某些告警

  • warnings.warn() 適用於可以通過調整代碼主動避免問題的場景

  • logging.warning() 適用於不需要調整代碼僅記錄問題信息的場景

記錄特定的錯誤信息

拋異常

記錄那些不需要拋異常的錯誤信息(例如捕捉服務進程長時間運行期間的錯誤信息)

logging.error(), logging.exception() 或者 logging.critical() 都可以適用

 

日誌函數的命名依據於約定俗成的事件級別。不同事件級別的適用範圍可以參考下面的這個表格(按照級別的嚴重程度升序排列):

事件級別

適用範圍

DEBUG

通常用在調試程序時輸出詳細信息

INFO

確認程序在正常運行

WARNING

在程序依舊能夠正常運行的情況下:記錄某個期望外的運行事件;記錄一些達到臨界值的運行信息(例如磁盤空間不足)

ERROR

因爲某些嚴重的運行錯誤,影響程序的某些功能使用

CRITICAL

運行錯誤存在導致程序不能繼續運行的場景

缺省的事件級別爲WARNING,也就是說只有WARNING級別已經高於WARNING級別的事件可以被捕捉到。如果需要其他級別的信息,可以顯式的設置日誌級別。

針對捕捉到的運行事件有多種的處理措施,最簡單的措施就是在控制檯打印出來,當然也可以把這些事件信息記錄到磁盤。

 

簡單演示

下面是一個非常簡單的用例:

import logginglogging.warning('Watch out!')  # 信息會被輸出到控制檯logging.info('I told you so')  # 這時不會打印任何信息

運行上述代碼,執行結果如下:

WARNING:root:Watch out!

INFO級別的消息沒有輸出是因爲缺省的日誌級別是WARNING級別。觀察輸出信息,其中包含了日誌級別以及調用日誌函數時的入參信息(示例中爲‘Watch out!’)。你可能會對中間的root有些疑惑,稍後會解釋的。實際使用時,你可以根據需要靈活調整輸出格式,對於輸出格式稍後也會進一步解釋。

 

把日誌寫入文件

這一部分是介紹比較實用的一種形式——是把日誌記錄在文件中。如果你打算運行下面的代碼示例,記得新打開一個Python運行界面,而不是繼續使用上面運行過的。

import logginglogging.basicConfig(filename='example.log',level=logging.DEBUG)logging.debug('This message should go to the log file')logging.info('So should this')logging.warning('And this, too')

執行完這段代碼之後,打開日誌文件就會發現多了這些日誌內容:

DEBUG:root:This message should go to the log fileINFO:root:So should thisWARNING:root:And this, too

上面的實例也展示了可以在一開始通過配置改變日誌級別。因爲一開始就把日誌級別設置爲了DEBUG,所以對應的日誌也被記錄了下來。

 

如果你你希望通過以下方式在命令行配置日誌級別:

--log=INFO

如果向--log傳遞了符合規範的變量值,可以使用:

getattr(logging, loglevel.upper())

來獲取入參,進而傳遞給basicConfig()函數。或許你也想到了需要對入參進行校驗,下面提供了一個校驗入參的示例:

# assuming loglevel is bound to the string value obtained from the# command line argument. Convert to upper case to allow the user to# specify --log=DEBUG or --log=debugnumeric_level = getattr(logging, loglevel.upper(), None)if not isinstance(numeric_level, int):    raise ValueError('Invalid log level: %s' % loglevel)logging.basicConfig(level=numeric_level, ...)

 

basicConfig()函數的調用必須在debug()、info()以及其他日誌輸出函數之前。必須指出的是,baiscConfig()函數僅在第一次調用的時候生效,再次調用將不會產生任何作用。

 

如果你多次運行了上面的代碼,你會發現日誌文件裏保存了多次運行的日誌信息。可能你不需要把每次運行的日誌信息都記錄下來,而是僅保留最新的一次,那麼你可以通過改變filemode 入參來實現,下面是一個示例:

 

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

這樣話,雖然多次運行都是一樣的日誌,但是隻有最新一次的運行日誌被保留下來,之前的都會被刷掉。

 

在多個模塊中使用Logging

如果你的程序包含多個模塊,你可以參考下面的示例來組織你的代碼:

# myapp.pyimport loggingimport mylibdef main():    logging.basicConfig(filename='myapp.log', level=logging.INFO)    logging.info('Started')    mylib.do_something()    logging.info('Finished')if __name__ == '__main__':    main()


 


# mylib.py
import loggingdef do_something():    logging.info('Doing something')

運行myapp.py後,你會發現myapp.log記錄的日誌信息如下:

INFO:root:StartedINFO:root:Doing somethingINFO:root:Finished

實際結果和你期望的一樣。mylib.py的示例你可以靈活運用在你自己的多模塊程序中。不過,需要注意的是,在這個簡單的示例中,不同py文件的日誌被穿插保存。《高級部分》(老張後面的推送會更新)部分會提供給你進一步的區分不同py文件日誌信息的方法。

 

記錄變量信息

通過格式化字符串的方式可以將想要保存的描述信息以及變量方便的保存下來。舉個例子:

 

import logginglogging.warning('%s before you %s', 'Look', 'leap!')

運行結果如下:

 

WARNING:root:Look before you leap!

和你看到的一樣,將變量格式化到描述信息的方式是使用的%這種舊的形式。這是爲了向後兼容:logging包比新的格式化方式(比如str.format()、string.Template)誕生的要早。雖然logging也支持新的格式化方式,但是這裏不做介紹。

 

自定義日誌信息的格式

如果有需要,你可以自定義日誌消息的格式:

import logginglogging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)logging.debug('This message should appear on the console')logging.info('So should this')logging.warning('And this, too')

運行結果如下:

DEBUG:This message should appear on the consoleINFO:So should thisWARNING:And this, too

可以看到之前一直在日誌消息裏面出現的“root”消失了。還有其他好多配置項,但是在簡單場景下,你需要也許僅是levelname(級別),message(事件描述、變量信息),以及事件的發生時間。下面一章介紹時間配置。

 

展示時間信息

你可以通過增加‘%(asctime)s’來讓你的程序輸出時間信息:

import logginglogging.basicConfig(format='%(asctime)s %(message)s')logging.warning('is when this event was logged.')運行結果如下:
2010-12-12 11:41:42,612 is when this event was logged.

如上所示,默認的時間輸出格式是ISO8601或者RFC 3339標準。如果你需要自定義時間格式,可以在調用basicConfig顯示傳入參數datefmt,示例如下:

import logginglogging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')logging.warning('is when this event was logged.')

運行結果如下:

12/12/2010 11:46:36 AM is when this event was logged.

datefmt的格式配置信息同time.strftime()一樣。

 

總結

通過對《基礎部分》的學習,你應該掌握瞭如何在程序裏添加日誌代碼。logging還提供了高階用法,爲了能夠掌握高階用法,你應該繼續閱讀《高級部分》(老張這裏順道挖個坑)。

但是如果你僅僅是想跟上面介紹一樣,簡單的在程序裏面使用日誌,並且你有什麼不明白的地方,可以把問題發在:

 

https://groups.google.com/forum/#!forum/comp.lang.python

你會很快收到回覆的。

 

 

02、高級部分

Logging庫包含了模塊化的方法,提供包含loggers, handlers, filters以及formatters在內的若干組件:

  • loggers對外暴露了可以直接使用的接口。

  • handlers處理日誌記錄(由logger生產)的流向。

  • filters很便捷的決定日誌記錄是否能夠被輸出。

  • formatters包含了日誌記錄的輸出格式。

事件信息會以LogRecord實例的形式在loggers、handlers、filters以及formatters之間傳遞。

 

Logging的入口爲Logger類(以下統稱loggers)的實例方法。每個實例都有一個命名,他們共同存在於一個由點符號分割的命名空間內。舉個例子,一個被命名爲‘scan’的logger實例是‘scan.text’實例的上層實例。你可以隨意命名logger實例,並且可以在日誌消息裏面顯示調用程序的層級。

 

一個比較好的做法是利用模塊層級來組織logger實例的命名,模塊裏面的命名方式如下:

logger = logging.getLogger(__name__)

這樣使得logger的命名可以正確反映包(以及模塊)的層級,使得可以通過日誌記錄裏面的logger命名直觀的追溯到代碼位置。

 

在層級根部實例化的logger被命名爲root。同所有logger實例一樣,root實例提供了debug(), info(), warning(), error()以及critical()函數。這些函數共享簽名。root實例在打印日誌信息時會攜帶‘root’字符串。

 

一條日誌消息可以有多個流向。logging包提供的處理方式包括:寫入文件、發送Get或者Post形式的HTTP報文、SMTP形式的電子郵件、通用的套接字、隊列以及不同操作系統的日誌處理機制(諸如系統日誌、Windows NT事件日誌)。日誌消息的流向由handler類來處理。如果內置的handler類不能滿足你的需求,你也可以自定義handler。

 

缺省狀態下並不會設置任何日誌流向。你可以通過《基礎部分》裏面提到的basicConfig()函數設置諸如控制檯、文件在內的日誌流向。當你調用debug()等日誌方法時,這些方法會檢查你是否指定了日誌流向,如果你沒有指定的話,這些方法會默認指定控制檯(sys.stderr)爲日誌流向、使用默認的日誌格式,然後纔將日誌消息傳遞到logger類的root實例,最終生成你看到的日誌消息。

 

basicConfig()的缺省日誌格式爲:

severity:logger name:message

你也可以顯式的通過format參數顯式的的指定日誌格式。關於日誌格式的構造選項,請參考過於formatter類的文檔說明。

 

Logging包的處理流程

下圖是關於一條日誌消息在loggers和handlers之間怎樣被處理的流程圖:

 

Loggers

logger對象有三部的工作。第一,它對外暴露了若干方法,使得外部程序可以在運行的時候記錄日誌信息。第二,logger對象可以根據日誌級別來決定是否需要過濾掉一條日誌消息。第三,logger對象會將日誌消息傳遞給已關聯的handlers。

 

logger對象有兩類使用最廣泛的方法:配置以及發送日誌消息。

下面是最常見的配置方式:

  • Logger.setLevel()可以配置允許生效的最低級別。在內置的日誌級別中,DEBUG級別是最低級的,CRITICAL是最高級別。舉個例子,配置的級別是INFO,那麼logger實例只會處理INFO、WARNING、ERROR以及CRITICAL級別的日誌消息,而DEBUG級別的會被過濾掉。

  • Logger.addHandler()和Logger.removerHandler()爲logger實例提供了增、刪handler對象的途徑。稍後會詳細介紹handler對象。

  • Logger.addFilter()和Logger.removerFilter()爲logger實例提供了增、刪filter對象的途徑。

     

你並不需要每次創建logger實例時都調用它們。

 

對於給定的logger實例,下面的方法會生產一條日誌消息:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), 以及 Logger.critical()都會生成一條日誌記錄,該記錄包含日誌消息和方法名對應的日誌級別。這個消息實際上是一個格式化的字符串,可能包含標準的字符串格式符符號(比如  %s, %d, %f等)。剩下的入參可能包含一些在日誌消息中預格式化的對象。對於**kwargs形式的關鍵字入參,日誌函數只關心exc_info對應的變量值,它將決定是否記錄異常信息。

  • Logger.exception()和Logger.error()生成的日誌消息相似,他們的區別在Logger.exception()攜帶棧信息。確保只在異常處理時調用該函數。

  • Logger.log()需要指定日誌級別作爲入參。相比上面提到的開箱即用的日誌函數,它顯得有些繁瑣,但是可以適用於需要自定義日誌級別的場景。

     

Handlers

handler對象負責根據日誌級別分配日誌消息的最終流向。Logger對象默認不包含handler對象,但是可用通過addHandler()方法添加。拿一個應用場景來舉例:假設你的應用程序需要將所有日誌消息保存在日誌文件中;把ERROR級別及以上的日誌打印在標準輸出;所有的CIRITCAL級別的日誌通過電子郵件發給你。整個場景需要三個handler實例,每個實例都會根據不同的日誌級別採取不同的方式處理日誌消息。

 

標準庫只內置少量的handler類型;本文檔主要拿StreamHandler和FileHandler來舉例。

 

開發人員只需要關心Handler對象的少數幾個方法。在內置的handler對象(非自定義的handler)裏面,跟開發人員密切相關的配置方法如下所示:

  • setLevel()方法跟logger對象的方法一樣,配置handler會處理的最低日誌級別。爲什麼會有兩個setLevel()方法呢?logger對象設置的日誌級別決定了日誌能夠被傳遞到handler對象。而handler對象設置的日誌級別決定了日誌消息是否會被記錄下來。

  • setFormaterr()可以爲handler對象配置Formatter對象。

  • addFilter()和removerFilter()可以增刪filter對象。

 

應用程序不應該直接實例化Handler對象。因爲Handler對象是一個基類,它定義了所有Handler子類都應該繼承或者複寫的接口方法。

 

Formatters

formatter對象決定了一條的日誌消息的順序、結構以及內容。不同於logger.Handler是基類,應用程序需要自己實例化Formatter類。當然如果你有特殊需求,也可以實例化Formatter的子類。它接收三個參數:

  • 一個預格式化的消息字符串

  • 一個預格式化的時間字符串

  • 一個類型符號

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

如果沒有顯式的傳入消息格式,會使用缺省設置。如果沒有顯式的傳入時間格式,缺省的時間格式如下:

%Y-%m-%d %H:%M:%S

並且會在後面追加毫秒。類型符號可以選擇‘%’、‘{'或者’$‘。缺省的類型符號爲’%’。

  • 如果類型符號爲‘%’,日誌消息的格式化方式採用%(<dictionary key>)s的替換方式;可用的鍵值請單獨翻閱LogRecord的說明。

  • 如果類型符號爲‘{’,日誌消息的格式化方式採用與str.formate()方法兼容的處理(也就是使用關鍵字)。

  • 如果類型符號爲‘$’,日誌消息的格式化方式需要與string.Template.substitute()方法保持一致。

3.2版本的改動說明:增加了style入參。

'%(asctime)s - %(levelname)s - %(message)s'

上面是一個帶有時間的可讀性高的預格式化方式,日誌級別和日誌內容被有序的添加在裏面。

formatters提供了用戶可配置的函數,方便日誌生成時間轉化爲時間元組。默認的是使用time.localtime()。如果想要在formatter實例中自定義,可以通過給 converter屬性賦值的形式改變默認行爲,需要注意的是新賦的值需要是同time.localtime()或者time.gmtime()簽名一致的函數。假設這麼一個場景:你需要是所有的日誌時間都展示爲GMT時區,你可以將Formatter的 converter屬性賦值爲time.gmtime()的形式,改變所有formatter實例行爲。

 

Logging配置

開發人員可以通過以下三種配置來配置logging:

  1. 在代碼中使用前面提到的方式在代碼中依次創建loggers、handlers以及formatters對象。

  2. 新建一個配置文件,並通過fileConfig()函數載入配置。

  3. 新建一個配置文件夾,並通過dictConfig()函數載入配置。

 

關於後兩種配置方式更詳細的說明,請自行查閱Configuration函數文檔。下面是Python代碼示例,它包含了一個簡單的logger實例、一個控制檯handler和一個簡單的formatter:

import logging
​​​​​​​

# create loggerlogger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch
ch.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
 

運行結果如下:

$ python simple_logging_module.py2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message2005-03-19 15:10:26,620 - simple_example - INFO - info message2005-03-19 15:10:26,695 - simple_example - WARNING - warn message2005-03-19 15:10:26,697 - simple_example - ERROR - error message2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

 

下面代碼的效果同上面一樣,也是新建logger實例、handler實例和formatter實例,唯一不同的地方是對象的命名。

import loggingimport logging.configlogging.config.fileConfig('logging.conf')# create loggerlogger = logging.getLogger('simpleExample')# 'application' codelogger.debug('debug message')logger.info('info message')logger.warning('warn message')logger.error('error message')logger.critical('critical message')

logging.conf文件的內容如下:

[loggers]keys=root,simpleExample[handlers]keys=consoleHandler[formatters]keys=simpleFormatter[logger_root]level=DEBUGhandlers=consoleHandler[logger_simpleExample]level=DEBUGhandlers=consoleHandlerqualname=simpleExamplepropagate=0[handler_consoleHandler]class=StreamHandlerlevel=DEBUGformatter=simpleFormatterargs=(sys.stdout,)[formatter_simpleFormatter]format=%(asctime)s - %(name)s - %(levelname)s - %(message)sdatefmt=

執行之後的輸出信息同不用配置文件的差不多:

$ python simple_logging_config.py2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message2005-03-19 15:38:55,979 - simpleExample - INFO - info message2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message2005-03-19 15:38:56,055 - simpleExample - ERROR - error message2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

 

能夠很明顯看到的是,配置文件的內容格式相對Python代碼而言有一些簡化的地方。像這樣將配置和代碼分離,能夠幫助沒有開發經驗的用戶更好的配置日誌行爲。


警告:fileConfig函數有一個特殊的入參——disable_existing_loggers。爲了保持向後兼容,其默認值爲True。這個參數導致的行爲可能會讓你困惑,它會使得在fileConfig()函數調用之前已存在的非root的logger實例失效,除非某個logger同配置文件中的配置同名。如果需要關於它的更多細節,可以自行查閱相關說明。當然你也可以根據實際需要顯式的傳入False。

 

需要注意的是配置文件提到的接口引用,要麼必須是logging包內部的,要麼是可通過import導入的絕對路徑。舉個例子,你可以使用WatchedFileHandler(logging包內部),你也可以用mypackage.mymodule.MyHandler(一個在mypackage包--mymodule模塊定義的類,當然整個路徑必須能夠被import正確導入)。

 

從Python3.2開始,引入了一種新的日誌配置方式--通過目錄組織配置信息。它能夠提供上面其他方式更強大的功能,推薦開發人員在新建項目時使用這種方式。因爲文件夾的配置方式除了正常的配置之外,還可以根據不同的用途靈活的移動文件夾。舉個例子,你可以使用JSON格式添加配置信息,如果你之前接觸過YAML程序開發,你也使用YAML格式。當然,你也可以選擇Python代碼的配置方式、接收套接字的配置方式,或者其他你認爲方便的方式。

 

下面是基於文件夾配置使用YAML格式的配置示例,效果跟之前的示例一樣:

version: 1formatters:  simple:    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'handlers:  console:    class: logging.StreamHandler    level: DEBUG    formatter: simple    stream: ext://sys.stdoutloggers:  simpleExample:    level: DEBUG    handlers: [console]    propagate: noroot:  level: DEBUG  handlers: [console]

如果需要關於文件夾配置的更多資料,可以自行查閱Configration的說明。

如果沒有提供配置信息會發生什麼

如果沒有提供配置信息,大概率會在打印日誌事件的時候發現沒有handlers實例可用。當然,實際會發生什麼跟Python版本有關。

 

對於Python3.2之前的版本,最後會發生:

  • 如果logging.raiseExceptions選項爲False(線上環境),日誌會被丟棄。

  • 如果logging.raiseExceptions選項爲True(開發環境),會在控制檯輸出‘No handlers could be found for logger X.Y.Z’ 

 

Python3.2及以後的版本,最後會發生:

  • 日誌事件會通過loggin.lastResort對象中的兜底handler處理。

    這個內部的handler並沒有被任何logger使用,它的效果跟StreamHandler一樣,會將日誌消息輸出到sys.stderr(所以需要你謹慎的對待可能被改動的重定向)。

    它只會打印出來日誌消息,並沒有攜帶任何格式。

    這個handler的默認級別是DEBUG,所以基本上任何消息都會被打印出來。

如果你希望禁用3.2版本之後的默認行爲,可以將logging.lastResort顯式的的設置爲None。


 

在庫中使用logging

如果你會在自己開發的庫裏面使用logging,那麼你需要仔細確認庫是如何使用logging的,舉個例子:關於loggers的名字。仔細確認如何配置logging是必須的。如果調用方沒有使用logging,但是庫內部使用了logging,WARNING級別及以上的日誌消息將會輸出到sys.stderr。看過了之前的介紹,你應該知道這是默認行爲。

 

如果你期望在沒有主動配置的情況不輸出這些日誌消息,你可以給你的庫裏面最高層的logger實例添加一個沒有任何操作的handler。這樣就可以避免日誌被打印出來,原因就是對於庫內部而言日誌消息都會交給這個handler處理,但是這個handler並不會打印輸出任何東西。一旦,調用程序主動配置添加了handler對象,你的庫內部產生日誌消息也會被處理,如果配置的日誌級別適配,那麼消息將會被打印輸出。

 

在Python3.1版本之後,NullHandler被引入,但是它實際上並不會對日誌消息做任何處理。如果你不希望你的庫內部日誌在配置缺失的情況下被輸出到sys.stderr,你可以實例化一個NullHandler,並把它添加到頂層的logger實例。舉個例子,如果一個名爲foo的庫實例化了諸如‘foo.x’、‘foo.x.y’的logger,你可以這樣:

​​​​​​​

import logginglogging.getLogger('foo').addHandler(logging.NullHandler())

在一個組織提供了多個庫的場景下,這樣做使得實際當中logger的命名爲orgname.foo而不是foo。

提示:對於庫,除了NullHandler之外,不要再添加任何其他handler。這是因爲你應該把控制權交給調用方。只有調用方結合自己的實際情況能夠決定使用什麼樣的handler。如果你在庫裏面添加了handler,你可能會影響調用方的單元測試結果,並且輸出一些他們不需要的東西。

 

 

03、其他說明

 

Logging的日誌級別

下表列出了日誌級別對應的數字。如果你需要自定義日誌界別,你需要避開內置的這些值。一旦你定義的日誌級別和內置的衝突,內置級別會被複寫、抹掉。

 

日誌級別

相應數值

CRITICAL

50

ERROR

40

WARNING

30

INFO

20

DEBUG

10

NOTSET

0

日誌級別跟logger實例關聯方式有兩種:一是開發人員主動配置,二是通過配置文件加載。當logger實例的日誌方法被調用的時候,logger會檢查它的級別配置跟日誌方法是否匹配。如果logger的級別比日誌方法要高,實際上就不會產生任何日誌消息。它從構造機制上避免了產生冗餘的輸出。

 

日誌消息實際上是LogRecord類的實例化。當logger決定生成一條日誌消息的時候,實際上是根據消息內容實例化了LogRecord。

 

日誌消息隨後會由Handler子類的實例來處理。handler實例會根據日誌級別決定一條日誌消息的最終流向(可能會有多個流向,諸如用戶、編輯、管理員、開發人員)。handler會將LogRecord實例傳遞到合適的目的地。每個logger實例都可以有0個、1個或者多個handler(還記得addHandler()函數嗎)。當一個logger實例產生一條日誌消息之後,它的所有handler、它上層logger的所有handler都會被調用(除非這個logger的propagate值被置爲False),分別處理這個日誌消息。

 

logger實例和handler實例的日誌級別可以不一致。它們分別都會根據日誌級別做過濾。在handler最終處理日誌消息的時候,emit()方法將被用來向傳遞消息。大多Handler子類都複寫了emit()方法。

 

自定義日誌級別

你可能會自定義日誌級別,但其實那是沒必要,因爲這些日誌級別就是實踐中總結出來的。如果你確定需要自定義,請務必小心,對於庫開發而言這樣做甚至是有害的。原因是,如果不同的庫均自定義了日誌級別,但是它們之間又不一致(一個數值可能對應多個不同級別),對於調用方的開發人員簡直就是災難。

 

實用的handler

作爲對Handler基類的補充,幾個實用的handler子類如下:

  1. StreamHandler實例會將消息發送到數據流(類似於文件對象)。

  2. FileHandler實例會將消息寫入磁盤文件。

  3. BaseRotatingHandler是滾動日誌的基類,滾動日誌會在指定的時機滾動日誌文件。它不能直接被實例化,實際使用的是RotatingFileHandler或者TimedRotatingHandler。

  4. RotatingFileHandler實例會將消息寫入磁盤文件,並在日誌文件的佔用空間達到最大值的時候滾動日誌文件。

  5. TimedRotatingHandler實例會將消息寫入磁盤文件,並在根據指定的時間間隔滾動日誌文件。

  6. SockerHandler實例會通過TCP/IP套接字發送日誌消息。在3.4版本之後,Unix套接字也開始被支持。

  7. DatagramHandler實例會通過UDP發送日誌消息。在3.4版本之後,Unix套接字也開始被支持。

  8. SMTPHandler實例會將日誌消息發送到指定的電子郵箱。

  9. SysLogHandler實例可以把日誌消息發送到Unix系統日誌守護程序(可能是一臺遠程機器)。

  10. NTEventLogHandler實例會把日誌消息發送到Windows NT/2000/XP系統日誌。

  11. MemoryHandler實例會把日誌消息寫入內存的緩衝區,這意味着可以通過指令清除。

  12. HTTPHandler實例通過GET或POST的方式把日誌消息發到HTTP服務器。

  13. WatchedFileHandler實例可以監控日誌文件。一旦文件發生改動,它會關閉並重新打開一個同名文件。該handler只可以在類Unix系統上使用,因爲Windows系統底層不支持。

  14. QueueHandler實例將日誌消息保存到一個隊列,就像queue或者multiprocessing模塊實現的那樣。

  15. NullHandler實例對於錯誤日誌沒有任何響應。如果在功能庫的開發中想要使用logging,但是又要在沒有配置信息時避免顯示類似‘No handlers could be found for logger XXX’ 這種消息,可以考慮使用NullHandler。

注:NullHandler在3.1版本引入

注:QueueHandler在3.2版本引入

 

其中NullHandler、StreamHandler和FileHandler在logging核心包中定義。其他的handler在子模塊logging.handlers中。(還有一個子模塊logging.config用於配置)

 

已經輸出的日誌消息在在Formatter類內部被格式化的。通過帶有‘%’符號的預格式化的字符串以及一個字典來初始化Formatter對象。

 

如果想要批處理多條消息,可以考慮使用BufferingFormatter實例。爲了方便格式化字符串(即批處理),它提供了針對頭尾字符串格式的要求。

 

如果通過配置logger和handler的日誌級別不能夠滿足你的需要的時候,logger或者handler都可以單獨配置Filter實例(通過調用addFilter()函數)。在進一步處理日誌消息之前,logger和handler都會調用已註冊的filter來判斷是否過濾該消息。如果其中任何一個filter實例返回了False,那麼這條消息都會被丟棄。

 

基本的Filter用法是通過logger名來過濾。如果該配置生效,那麼發送到指定logger,以及其子類傳遞上來的日誌消息均會被丟棄。

 

logging的異常處理

logging具備可以消化自身異常的能力。換句話說,logging本身運行產生的諸如配置錯誤、網絡問題之類的錯誤並不會導致調用程序結束運行。

 

除了SystemExit和KeyboardInterrupt之外,emit()方法產生的其他異常將會被丟給自身的handleError()方法處理。

handlerError()方法會檢查名爲raiseExceptions的配置項,如果設置了該配置項,handler會將錯誤信息輸出到sys.stderr,如果沒有配置該配置項,錯誤信息會被內部消化。

提示:raiseExceptions的缺省值是True。知道你不希望在開發時被這些錯誤信息牽扯精力,所以默認就是True。但是建議你在線上環境要將raiseExceptions設置爲False。

 

在消息裏面使用任何對象

在上面的介紹裏,你可能會發現了所有的日誌消息都是字符串。並不是說明只有字符串才能當作日誌消息傳遞給logging,實際上,你可以使用任何對象。logging內部會調用這些對象的__str__()方法,將他們轉成字符串。你甚至可以完全避免用到字符串,舉個例子:SocketHandler在發送消息的時候用到的二進制表示。

 

優化

雖然只有在最後用到的時候纔會格式化日誌消息,但是針對日誌方法的入參做的運算也是巨大的,你可能期望logging對於被丟棄的消息節省這些計算。你可以通過調用isEnabledFor()方法(該方法接受一個日誌界別的入參)來查看logging的行爲,如果它返回了True即表明這個級別的日誌消息會被Logger實例處理。代碼示例如下:​​​​​​​

if logger.isEnabledFor(logging.DEBUG):    logger.debug('Message with %s, %s', expensive_func1(),                                        expensive_func2())

這樣就可以確定logger實例的不處理DEBUG級別的日誌,expensive_func1()和expensive_func2() 就不會被執行到。

提示:某些情況下isEnabledFor()方法可能比你想象的更昂貴(舉個例子,嵌套的logger中,只有最上層的logger日誌級別是高於入參的)。在這種場景下(或者你只是想要避免嵌套調用一個方法),你可以把首次調用isEnabledFor()的結果緩存下來,後續只調用緩存的本地變量)。這樣你就可以僅在日誌配置動態改變時計算新的變量值(其實這種場景並不常見)。

 

還有一種優化場景就是隻有某種情況下希望能儘可能詳細的收集日誌信息。下表是一些你可以避免的一些資源浪費:

你不希望收集的信息

如何避免

日誌消息在哪產生的

logging._srcfile 設置爲None. 這樣可以避免sys.getframe(),某些環境中(如pypy)可以幫助你的程序運行的更快。(不過支持py3的pypy環境該方法會失效)

線程信息

將 logging.logThreads 設置爲 0.

進程信息

將 logging.logProcesses 設置爲 0.

 

需要提醒的是logging核心模塊僅包含基本的一些handler類。剩下的handler並不直接可用,而是需要你主動導入logging.handlers和logging.config。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章