本文翻譯自logging howto
基礎教程
日志是跟蹤軟件運行時發生事件的一種手段。Python開發者在代碼中調用logging模塊的方法,用于記錄某些事件已經發生。事件一般是描述性信息,其中可能會包含一些數據。事件中也包含了開發者對事件的重視程度;重視程度也可以稱為級別或者嚴重性。
記錄時機
logging模塊提供了一組紀錄日志的功能,有debug(), info(), warning(),error()和critical()。下表說明了這些功能的使用場景。
任務 | 該使用的功能 |
---|---|
顯示控制臺輸出,用于一般性的命令行腳本和程序 | print() |
報告程序正常運行期間發生的事件 | logging.info()或者logging.debug() |
特定運行事件的警告 | 如果問題是可以避免的,并且應該修改客戶端應用程序以消除警告,使用warnings.warn(); 客戶端不需要作出改變,但是事件仍然應該注意,使用logging.warning() |
報告運行時的錯誤 | 拋出異常 |
非異常形式報告運行時的錯誤 | logging.error()、logging.exception()或者logging.critical() |
logging模塊中的函數根據事件的嚴重性命名,如下表。
級別 | 使用時機 |
---|---|
DEBUG | 獲取詳細信息,通常時診斷問題時 |
INFO | 確認程序按預期工作 |
WARNING | 表明發生了一些意外的事情,或者用來指示在不久的將來會發生的問題。軟件依然如預期工作 |
ERROR | 因為更嚴重的問題,軟件無法執行某些功能 |
CRITICAL | 一個嚴重的問題,表示程序本身可能無法繼續運行下去 |
默認的級別是WARNING
,這意味著除非logging模塊有其他的配置,否則只記錄此級別或以上級別的事件。
記錄的事件可以用不同的方式處理。最簡單的方法是將這些事件打印到控制臺。另外一個常用的方法是將事件記錄到文件中。
簡單的例子
一個非常簡單的例子如下:
import logging
logging.warning('Watch out!') # 控制臺打印
logging.info('I told you so') # 不會打印任何信息
如果運行上面的腳本,會在控制臺輸出如下信息:
WARNING:root:Watch out!
注意INFO
級別信息沒有打印出來,因為默認的級別是WARNING
。打印的消息包括日志級別和描述的信息。先不用管其中的"root"信息,后面會解釋。如果需要的話,可以配置輸出格式;格式也會在后面提到。
寫入文件
將日志寫入文件是非常常見的場景,請看下面的例子。一定要在新的Python解釋器中運行下面的腳本:
import logging
logging.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 file
INFO:root:So should this
WARNING:root:And this, too
這個例子還顯示了如何設置日志的級別。由于級別設置為DEBUG
,所有的信息都會記錄下來。
如果想要通過類似--log=INFO
命令行參數設置日志級別,并且將傳入的級別存入loglevel
變量,然后通過getattr(logging, loglevel.upper())
獲取級別變量,并傳入basicConfig(),可以通過下面的代碼對參數進行校驗:
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numberic_level, int):
raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)
應該在debug(), info()等方法之前調用basicConfig()。因為它是一次性的配置,只有第一次的調用是有效的。
如果多次運行上面的腳本,日志將會追加到文件example.log
中。可以通過修改filemode
參數修改這一行為。
logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)
輸出和之前是一樣的,但是每次運行會丟失之前的消息。
多模塊日志
如果程序由多個模塊組成,可以參考下面的例子組織日志
# myapp.py
import logging
import mylib
def 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 logging
def do_something():
logging.info('Doing something')
運行myapp.py
,將會在myapp.log
中看到:
INFO:root:Started
INFO:root:Doing something
INFO:root:Finished
可以通過類似的模式將其推廣到多個模塊。不過,對于這種簡單的使用,只能通過查看事件描述來判斷日志的來源。
記錄變量
要記錄變量數據,需要使用事件描述消息的格式,并附加變量作為參數。比如:
import logging
logging.warning('%s before you %s', 'Look', 'leap!')
會顯示
WARNING:root:Look before you leap!
可以看到,使用的是%風格的字符串格式。這是為了向后兼容。logging模塊也支持更新的格式化選項,但是探索這些不在本教程的范圍之內。
改變日志格式
要改變日志格式,需要指定新的格式:
import logging
logging.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 console
INFO:So should this
WARNING:And this, too
請注意,前面例子中的"root"已經消失了。完整的格式說明見LogRecord attribute,但是對于簡單的應用來說,只需要記錄事件級別,信息和時間就足夠了。
消息中顯示時間信息
要展示事件的時間,可以在格式串中加入'%(asctime)s':
import logging
logging.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。如果需要更多的控制,可以通過傳遞datafmt
參數給basicConfig
來實現,比如:
import logging
logging.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模塊的基礎教程,掌握上面的內容就可以進行常規日志記錄。logging模塊提供了更多的功能,想要充分利用它,請看下面的章節。
進階教程
logging模塊采用模塊化的設計,并提供了幾類組件:記錄器,處理器,過濾器和格式器。
- 記錄器提供應用程序代碼使用的接口。
- 處理器將記錄器生成的記錄發送到相應的位置。
- 過濾器對記錄進行過濾,決定哪些日志會被輸出。
- 格式器決定輸出的格式。
日志事件以LogRecord實例的形式在記錄器,處理器,過濾器和格式器之間傳遞。
一般通過調用Logger實例(下面簡稱為記錄器)中的方法來記錄日志。每個實例都有名字,這些名字在命名空間層級中由點連接。比如,名為'scan'的記錄器是'scan.text','scan.html'和'scan.pdf'的父記錄器。記錄器的名字可以任意取。
一個命名記錄器的良好習慣是使用模塊名,比如下面:
logger = logging.getLogger(__name__)
使用這種命名可以記錄模塊的繼承關系。
記錄器層次結構的根被稱之為根記錄器。函數debug(), info(), warning(), error(), critical()其實調用的就是根記錄器中同名的方法。這些函數和方法記錄的日志中,記錄器名字為'root'。
可以將不同的日志信息記錄到不同的地方。logging模塊支持將日志寫入多個目的位置,其中包括文件、HTTP GET/POST地址、郵件(SMTP協議)、通用socket、操作系統日志機制(比如syslog和Windows NT的事件記錄)。目的位置由處理器來指定。可以根據需求構造自己的處理器類。
默認情況下,任何日志都不設置目的地。可以通過basicConfig()指定一個目的地。debug(), info(), warning(), error()和critical()會在被調用時檢查是否設置了目的地;如果沒有設置,則在將消息委托給根記錄器之前,將目的器設置為控制臺(sys.stderr),并使用默認格式輸出消息。
basicConfig()的默認設置是:
日志級別:記錄器名字:消息
日志流
記錄器和處理器對日志事件的處理流程如下圖:
記錄器
Logger對象有三個功能。第一,提供接口給應用程序代碼,應用程序代碼在運行時通過調用這些接口記錄日志。第二,根據設置的日志等級(默認過濾規則)或者過濾器來確定輸出的日志消息。第三,將日志消息傳遞給所有與之相關的處理器。
記錄器對象中使用最廣泛的方法分為兩類:配置和發送消息。
下面是最常用的配置方法:
- Logger.setLevel()指定記錄器處理的日志級別,內置的最低級別是debug,最高級別是critical。比如,如果配置的日志級別是INFO,那么記錄器只會處理INFO,WARNING,ERROR和CRITICAL級別的日志,DEBUG級別的日志將被自動忽略。
- Logger.addHandler()和Logger.removeHandler()會從記錄器對象中添加或者移除處理器。
- Logger.addFilter()和Logger.removeFilter()方法會從記錄器對象中添加或者移除過濾器。
不需要每次創建記錄器時都調用這些方法。
配置好記錄器對象后,可以通過下面的方法創建日志消息:
-
Logger.debug(), Logger.info(), Logger.warning(), Logger.error()和 Logger.critical()都用來創建和方法名對應級別的日志記錄。消息實際上是格式字符串,可能包含標準字符串替換語法%s, %d, %f等等。其余的參數是消息中的替換字段對應的對象列表。這些方法只關心
**kwargs
中的exc_info
關鍵字,并使用它來確定是否記錄異常信息。 - Logger.exception()和Logger.error()差不多。不過Logger.exception()會轉儲堆棧跟蹤信息。只能在異常處理中調用這個方法。
- Logger.log()將日志級別作為顯式參數。和上面列出的方法相比,這個方法有點啰嗦,不過可以使用該方法記錄自定義級別日志。
getLogger()返回具有指定名字的記錄器的引用,如果沒有指定名字,則返回root
。用'.'連接命名空間的層次構成了這些名字。使用同樣的名字多次調用getLogger()會返回指向同一記錄器的引用。分層列表中位置靠下的記錄器是位置靠上的記錄器的子記錄器。比如,對名字為foo
的記錄器來說,名字為foo.bar
, foo.bar.baz
和foo.bam
的記錄器是它的子記錄器。
記錄器有有效級別的概念。如果沒有顯式設置級別,則使用父記錄器的級別作為其有效級別。如果父記錄器也沒有設置,則一直向上尋找,直到找到顯式設置的級別。根記錄器始終具有顯式級別設置(默認為WARNING)。在決定是否處理事件時,使用記錄器的有效級別來確定是否將事件傳遞給處理器。
子記錄器將消息傳播到與其祖先記錄器相關聯的處理器。因此,無需為所有的日志記錄器定義和配置處理器。為頂級記錄器配置處理器,并根據需要創建子記錄器就足夠了。(不過,也可以將propagate
屬性設置為False
來關閉這一行為)。
處理器
處理器負責將適當的日志消息(基于日志的級別)分派到指定的目標。Logger對象可以通過使用addHandler()方法添加零或者更多的處理器對象。比如,應用程序可能希望將所有的日志消息發送到日志文件,error及更高級別的日志輸出到stdout,critical級別的日志通過郵件告警。這個場景需要三個單獨的處理器,其中每個處理器負責將特定級別的消息發送到特定位置。
標準庫包含相當多不同類型的處理器(參閱Useful Handlers)。
開發者需要關心的處理器方法很少。使用內置處理器需要關心以下的配置方法:
- setLevel()方法,和記錄器中的同名方法一樣,這個方法設置日志能分派到目的地的最低級別。
- setFormatter()設置處理器使用的格式器。
- addFilter()和removeFilter()分別用于在配置中添加和移除過濾器對象。
應用代碼不應該直接初始化并使用Handler
類的實例。Handler
類是一個基類,用于定義所有處理器應具有的接口,并定義一些子類可以使用(或覆蓋)的默認行為。
格式器
格式器配置日志消息顯示的最終順序,結構和內容。和基礎的logging.Handler
類不同,應用程序代碼可以實例化格式器類。如果應用程序需要特殊行為,可以自定義子類。構造函數使用兩個可選參數--消息格式字符串和日期格式字符串。
logging.Formatter.__init__(fmt=None, datefmt=None)
如果沒有消息格式字符串,默認使用原始消息。如果沒有日期格式字符串,默認的格式為:
%Y-%m-%d %H:%M:%S
下面的消息格式字符串記錄的順序為:人類可讀的時間格式,消息的級別,消息的內容。
'%(asctime)s - %(levelname)s - %(message)s'
配置
程序員可以通過三種方式配置日志記錄:
- 在代碼中顯示創建記錄器,處理器和格式器
- 創建配置文件,并用fileConfig()函數讀取
- 創建配置字典,并傳入dictConfig()方法。
后兩種方法的文檔見Configuration functions。下面的示例使用Python代碼配置了一個非常簡單的記錄器,一個控制臺處理器和一個簡單的格式器:
# coding=utf8
import logging
# 創建記錄器
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# 創建控制臺處理器,將級別設置為DEBUG
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# 創建格式器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 處理器中添加格式器
ch.setFormatter(formatter)
# 記錄器中添加處理器
logger.addHandler(ch)
# 應用代碼
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
運行上面的腳本會產生如下輸出:
$ python simple_logging.module.py
2017-04-25 15:59:13,321 - simple_example - DEBUG - debug message
2017-04-25 15:59:13,321 - simple_example - INFO - info message
2017-04-25 15:59:13,321 - simple_example - WARNING - warn message
2017-04-25 15:59:13,321 - simple_example - ERROR - error message
2017-04-25 15:59:13,321 - simple_example - CRITICAL - critical message
下面的Python模塊創建一個記錄器,處理器和格式器,除了對象名字和上面例子不同外,其他都一樣:
import logging
import logging.config
logging.config.fileConfig('logging.conf')
logger = logging.getLogger('simpleExample')
logger.debug('debug message')
logger.info('info message')
logger.warn('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=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
產生的輸出和上面例子相同:
$ python simple_logging_config.py
2017-04-25 16:08:46,631 - simpleExample - DEBUG - debug message
2017-04-25 16:08:46,631 - simpleExample - INFO - info message
2017-04-25 16:08:46,631 - simpleExample - WARNING - warn message
2017-04-25 16:08:46,631 - simpleExample - ERROR - error message
2017-04-25 16:08:46,631 - simpleExample - CRITICAL - critical message
可以看到,相比使用Python代碼,配置文件有隔離配置與代碼和易于修改這些優點。
沒有配置的情況
如果沒有配置日志,可能會出現需要輸出日志但是沒有處理器的情況。在這種情況下,logging模塊的行為取決于Python版本。
對于Python2.x版本,行為如下:
- 如果
logging.raiseExceptions
是False
(生產模式),日志事件被丟棄。 - 如果
logging.raiseExceptions
是True
(開發模式),會打印消息'No handlers could be found for logger X.Y.Z'。
為庫配置日志
在開發的庫中記錄日志時,應該留下文檔,注明記錄日志的方式--比如所使用記錄器的名字。同時也需要考慮日志的配置。如果應用程序沒有配置日志,而庫代碼又調用了logging接口,那么一條錯誤消息將被打印到sys.stderr
。
如果不希望打印這條消息,可以在庫的頂層記錄器中添加一個空處理器。日志事件總是會找到處理器,而空處理器不會產生任何輸出,因此可以避免打印錯誤消息。如果庫的使用者在應用中配置了日志并且添加了處理器,那么庫中的日志調用將會被發送到這些處理器。
logging模塊包塊了空處理器:NullHandler(Python2.7版本或以上)。如果要在不配置日志的情況下阻止錯誤信息發送到sys.stderr
,可以將該處理器實例添加到命名空間中的頂層記錄器中。如果foo
庫的記錄器名為'foo.x','foo.x.y'之類的話,那么下面的代碼
import logging
logging.getLogger('foo').addHandler(logging.NullHandler())
將會有預期的效果。如果組織生成多個庫,則指定記錄器的名字應該是'orgname.foo'。
日志級別
日志級別的數值如下表所示。如果要自定義級別,并且需要自定義級別具有特定值,那么需要關注這些。如果自定義的級別和預定義級別有相同的數值,它將覆蓋預定義的值;預定義的名字將會丟失。
名字 | 數值 |
---|---|
CRITICAL | 50 |
ERROR | 40 |
WARNING | 30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
級別還可以和記錄器相關聯,由開發人員設置或者通過加載保存的日志記錄配置。如果記錄器的日志級別高于方法調用的級別,則不會生成日志消息。這是控制日志輸出的基本機制。
日志消息封裝在LogRecord對象中。當記錄器決定記錄事件時,會創建一個LogRecord實例。
消息由處理器(Handler
子類)來調度處理。處理器負責確保日志(以LogRecord的形式)位于特定位置(或一組位置),這對于該日志的目標受眾(例如終端用戶,支持人員,系統管理員,開發人員)是很有用的。處理器被用于將LogRecord傳遞到特定位置。每個記錄器都可以有零個,一個或多個與它相關聯的處理器(通過Logger的addHandler()方法)。除了與記錄器直接關聯的處理器外,還會調用與該記錄器的祖先記錄器關聯的所有處理器來分派消息(除非記錄器的傳播標識設置為false值)。
就像記錄器一樣,處理器可以具有與它們相關聯的級別。處理器的級別和記錄器的級別都用作過濾。如果處理器決定調度事件,emit()方法會將消息發送到其目標。絕大多數Handler
的子類需要重寫emit()方法。
自定義級別
可以自定義日志級別,但是沒有必要那樣做,因為現有的級別都是根據實際經驗選擇的。不過,如果確實需要自定義級別,那么在操作的時候應該特別小心,如果正在開發一個庫,自定義級別可能是一個非常糟糕的主意。這是因為如果多個庫作者都自定義級別,那么這種多個庫的記錄輸出可能會一起使用,這對開發人員來說是非常難控制和解釋的,因為給定的數值在不同的庫中可能有不同的意義。
有用的處理器
除了基類Handler
,logging模塊也提供了很多的子類:
- StreamHandler實例將消息發送到流(類文件對象)。
- FileHandler實例將消息發送到磁盤文件。
-
BaseRotatingHandler
是處理器的基類。一般不直接使用這個類。而是使用RotatingFileHandler或者TimedRotatingFileHandler。 - RotatingFileHandler實例將日志發送到磁盤文件,同時支持最大文件大小和日志文件輪轉。
- TimedRotatingFileHandler實例將日志發送到磁盤文件,并在給定時間間隔內輪轉日志文件。
- SocketHandler實例通過TCP/IP套接字發送日志。
- DatagramHandler實例通過UDP套接字發送日志。
- SMTPHandler實例將日志發送到指定的電子郵件地址。
- SysLogHandler實例將日志發送到Unix syslog守護程序中,這個守護程序可能在遠程計算機上。
- NTEventLogHandler實例將日志發送到Windows NT/2000/XP的日志事件中。
- MemoryHandler實例將日志發送到內存緩沖區中,滿足特定條件會刷新該緩沖區。
-
HTTPHandler實例使用
GET
或者POST
語意向HTTP服務器發送日志。 - WatchedFileHandler實例監視正在記錄日志的文件。如果文件有了更改,則使用文件名關閉并重新打開。此處理器僅適用于類Unix系統;Windows不支持使用這樣的機制。
- NullHandler實例對錯誤消息什么都不做。當庫的開發者想使用logging,但是想避免'No handlers could be found for logger XXX'時,可以在記錄器上添加這個處理器。
NullHandler在Python2.7中加入。
NullHandler、StreamHandler和FileHandler在logging核心部分中定義。其他的處理器在logging.handlers這個字模塊中定義。
日志在通過Formatter類的格式化后進行展示。
可以使用BufferingFormatter
批量格式化多個消息。除了格式化字符串外,還提供了處理頭部和尾部字符串格式的方法。
當基于日志級別的過濾滿足不了需求時,可以通過addFilter()方法為Logger實例添加Filter實例。在決定進一步處理消息前,記錄器和處理器都會查閱所有過濾器的權限。如果任何過濾器返回false值,則不會進一步處理該消息。
Filter的基本功能允許通過特定名字的記錄器進行過濾。如果使用此功能,則只有指定名字的記錄器和其子孫的消息將通過,其他的消息將被丟棄。
異常處理
設計上,logging模塊在生產模式下不會拋出異常。這樣的好處是,在logging模塊出現配置錯誤,網絡或者其他類似的錯誤時,不會導致應用程序過早終止。
SystemExit
和KeyboardInterrupt
會正常拋出。其他Handler
子類的emit()方法拋出的異常會傳遞給handleError()方法。
handleError()的默認實現會檢查raiseExceptions
變量是否設置。如果設置,棧回溯信息將打印到sys.stderr。如果沒有設置,則不會拋出異常。
記錄任意對象
在前面的例子中,假定了記錄事件時傳遞的消息是一個字符串。但是,這不是唯一的可能性。可以將任意對象作為消息傳遞,當日志系統需要將其轉換成字符串表示時,將調用對象的__str__()方法。
優化
消息將會最后被格式化輸出。如果記錄器只是丟棄事件,那么調用帶參數的logging方法開銷可能會很大。要避免這樣的開銷,可以使用isEnabledFor()方法。該方法接受級別作為參數,如果記錄器創建該級別的參數,則返回true。可以通過下面的方式調用:
if logger.isEnabledFor(logging.DEBUG):
logger.debug('Message with %s, %s', expensive_func1(), expensive_func2())
當記錄器的級別設置為DEBUG
時,expensive_func1
和expensive_func2
不會被調用。
對于需要對采集日志有更精確控制的應用程序,還有其他的優化策略。對于一些開發者可能不需要的信息,下表給出了一些優化建議:
不想采集的信息 | 優化策略 |
---|---|
不想知道調用的來源 | 將logging._srcfile 設置為None 。這個配置會避免sys._getframe()被調用,在PyPy環境下運行的代碼會更快 |
線程信息 | 將logging.logThreads 設置為0 |
進程信息 | 將logging.logProcesses 設置為0 |