30 python Howto 之 logging 模塊

本文來源於對 py2.7.9 docs 中 howto-logging 部分加之源代碼的理解。官方文檔鏈接如下,我用的是下載的 pdf 版本,應該是一致的:https://docs.python.org/2/howto/logging.html

我們不按照文檔上由淺入深的講解順序,因為就這麼點東西不至於有「入」這個動作。

使用 logging 模塊記錄日誌涉及四個主要類,使用官方文檔中的概括最為合適:

logger提供了應用程序可以直接使用的接口;
handler將(logger創建的)日誌記錄發送到合適的目的輸出;
filter提供了細度設備來決定輸出哪條日誌記錄;
formatter決定日誌記錄的最終輸出格式。

寫 log 的一般順序為:

一、創建logger:

我們不要通過 logging.Logger 來直接實例化得到 logger,而是需要通過 logging.getLogger(\"name\")來生成 logger 對象。

不是說我們不能實現 Logger 的實例化,而是我們期待的是同一個 name 得到的是同一個 logger,這樣多模塊之間可以共同使用同一個 logger,getLogger 正是這樣的解決方案,它內部使用 loggerDict 字典來維護,可以保證相同的名字作為 key 會得到同一個 logger 對象。我們可以通過實例來驗證一下:

 #test_logger1.py #coding:utf-8import loggingprint logging.getLogger("mydear")    import test_logger2test_logger2.run   #調用文件 2 中的函數,保證兩個模塊共同處於生存期 #test_logger2.py #coding:utf-8import loggingdef run:    print logging.getLogger("mydear")  

輸出:<logging.Logger object at 0x00000000020ECF28>
<logging.Logger object at 0x00000000020ECF28>

結果表明兩個文件中通過\"mydear\"調用 getLogger 可以保證得到的 logger 對象是同一個。而分別進行 Logger 類的實例化則不能保證。

有了 logger 之後就可以配置這個 logger,例如設置日誌級別 setLevel,綁定控制器 addHandler,添加過濾器 addFilter 等。

配置完成後,就可以調用 logger 的方法寫日誌了,根據 5 個日誌級別對應有 5 個日誌記錄方法,分別為logger.debug,logger.info,logger.warning,logger.error,logger.critical。

二、配置 Logger 對象的日誌級別:

logger.setLevel(logging.DEBUG) #DEBUG 以上的日誌級別會被此 logger 處理

三、創建 handler 對像

handler 負責將 log 分發到某個目的輸出,存在多種內置的 Handler 將 log 分發到不同的目的地,或是控制台,或是文件,或是某種形式的 stream,或是 socket 等。一個 logger 可以綁定多個 handler,例如,一條日誌可以同時輸出到控制台和文件中。

以 FileHandler 和 StreamHandler 為例:

logfile= logging.FileHandler(\"./log.txt\") #創建一個handler,用於將日誌輸出到文件中
console = logging.StreamHandler #創建另一個handler,將日誌導向流

handler 對象也需要設置日誌級別,由於一個 logger 可以包含多個 handler,所以每個 handler 設置日誌級別是有必要的。用通俗的話講,比如,我們需要處理 debug 以上級別的消息,所以我們將 logger 的日誌級別定為 DEBUG;然後我們想把 error 以上的日誌輸出到控制台,而 DEBUG 以上的消息輸出到文件中,這種分流就需要兩個 Handler 來控制。

logfile.setLevel(logging.DEBUG)console.setLevel(logging.ERROR)  

除了對 handler 對像設置日誌級別外,還可以指定 formatter,即日誌的輸出格式。對 handler 對像設置日誌格式,說明了可以將一條記錄以不同的格式輸出到控制台,文件或其他目的地。

formatter = logging.Formatter(\'%(asctime)s - %(name)s - %(levelname)s - %(message)s\')logfile.setFormatter(formatter) #設置 handler 的日誌輸出格式  

formatter 創建時使用的關鍵字,最後會以列表的形式展現,這不是重點。

四、綁定 handler 到 logger 中

至此 handlers 和 logger 已經準備好了,下面我們就將 handlers 綁定到 logger 上,一個 logger 對象可以綁定多個 handler。

    logger.addHandler(logfile)  #logger 是通過 getLogger 得到的 Logger 對像    logger.addHandler(console)  

五、使用 logger 真正寫日誌

    logger.debug("some debug message.")    logger.info("some info message.")  

看上去,中間步驟(創建 handler,設置日誌級別,設置輸出格式等)更像是配置 Logger,一旦配置完成則直接調用寫日誌的接口即可,稍後這些日誌將按照先前的配置輸出。

嗚呼,好多內容啊,來點簡單的吧.

下面的代碼,是最簡單的。導入 logging 之後就進行了寫日誌操作:

 #coding:utf-8import logginglogging.debug("debug mes")logging.info("info mes")logging.warning("warn mes")  

控制台輸出如下:

    WARNING:root:warn mes  

咦?發生了什麼情況,為什麼只輸出了 warning?handler、logger、formatter 去哪兒了?

-_-!說好的最簡單的呢?為了讓自己講信用,我盡可能把它解釋成「最簡單的」。

知識點 1:logger 間存在繼承關係

logger 通過名字來決定繼承關係,如果一個 logger 的名字是 \"mydest\",另一個 logger 的名字是\"mydest.dest1\"(getLogger(\"mydest.dest1\")),那麼就稱後者是前者的子 logger,會繼承前者的配置。上面的代碼沒有指定 logger,直接調用 logging.debug 等方法時,會使用所有 logger 的祖先類 RootLogger。

從上面的代碼運行結果可以猜測出,該 RootLogger 設置的日誌級別是 logging.WARN,輸出目的地是標準流。從源碼可以更清楚的看出來:

root = RootLogger(WARNING)  #設置 WARNING 的級別  

至於 rootLogger 的輸出目的地的配置,我們跟蹤 logging.debug 的源代碼來看一下:

```
def debug(msg, *args, **kwargs): \"\"\" Log a message with severity \'DEBUG\' on the root logger. \"\"\" if len(root.handlers) == 0:basicConfig root.debug(msg, *args, **kwargs)

大約可以看到,如果 rootLogger 沒有配置 handler,就會不帶參數運行 basicConfig 函數(*請看知識點 2),我們看一下 basicConfig 的源代碼:  
def basicConfig(**kwargs): _acquireLock try:if len(root.handlers) == 0: filename = kwargs.get(\"filename\") if filename:mode = kwargs.get(\"filemode\", \'a\')hdlr = FileHandler(filename, mode) else:stream = kwargs.get(\"stream\")hdlr = StreamHandler(stream) fs = kwargs.get(\"format\", BASIC_FORMAT) dfs = kwargs.get(\"datefmt\", None) fmt = Formatter(fs, dfs) hdlr.setFormatter(fmt) root.addHandler(hdlr) level = kwargs.get(\"level\") if level is not None:root.setLevel(level) finally:_releaseLock

因為參數為空,所以我們就看出了,該 rootLoger 使用了不帶參數的 StreamHandler,也可以看到諸如 format 之類的默認配置。之後我們跟蹤 StreamHandler(因為我們想看到日誌輸出目的地的配置,而 handler 就是控制日誌流向的,所以我們要跟蹤它)的源代碼:  
class StreamHandler(Handler): \"\"\" A handler class which writes logging records, appropriately formatted, to a stream. Note that this class does not close the stream, as sys.stdout or sys.stderr may be used. \"\"\"

def __init__(self, stream=None):    \"\"\"    Initialize the handler.    If stream is not specified, sys.stderr is used.    \"\"\"    Handler.__init__(self)    if stream is None:stream = sys.stderr  ####    self.stream = stream  

不帶參數的StreamHandler將會把日誌流定位到sys.stderr流,標準錯誤流同樣會輸出到控制台知識點 2:basicConfig 函數用來配置 RootLoggerbasicConfig 函數僅用來配置 RootLogger,rootLogger 是所有 Logger 的祖先 Logger,所以其他一切 Logger 會繼承該 Logger 的配置。從上面的 basicConfig 源碼看,它可以有六個關鍵字參數,分別為:filename:執行使用該文件名為 rootLogger 創建 FileHandler,而不是 StreamHandler
filemode:指定文件打開方式,默認是"a"
stream:指定一個流來初始化 StreamHandler。此參數不能和 filename 共存,如果同時提供了這兩個參數,則 stream 參數被忽略
format:為 rootLogger 的 handler 指定輸出格式
datefmt:指定輸出的日期時間格式
level:設置 rootLogger 的日誌級別
使用樣例:
logging.basicConfig( filename = \'./log.txt\', filemode = \'a\', #stream = sys.stdout, format = \'%(levelname)s:%(message)s\', datefmt = \'%m/%d/%Y %I:%M:%S\', level = logging.DEBUG )

知識點 3 通過示例詳細討論 Logger 配置的繼承關係首先準備下繼承條件:log2 繼承自 log1,logger 的名稱可以隨意,要注意『.』表示的繼承關係。  
#coding:utf-8

import logginglog1 = logging.getLogger(\"mydear\")log1.setLevel(logging.WARNING)log1.addHandler(StreamHandler)log2 = logging.getLogger(\"mydear.app\")log2.error(\"display\")log2.info(\"not display\")

level 的繼承原則:子 logger 寫日誌時,優先使用本身設置了的 level;如果沒有設置,則逐層向上級父 logger 查詢,直到查詢到為止。最極端的情況是,使用 rootLogger 的默認日誌級別 logging.WARNING。從源代碼中看更為清晰, 感謝 python 的所見即所得:  
def getEffectiveLevel(self):\"\"\"Get the effective level for this logger.

    Loop through this logger and its parents in the logger hierarchy,    looking for a non-zero logging level. Return the first one found.    \"\"\"    logger = self    while logger:if logger.level:    return logger.levellogger = logger.parent    return NOTSET  

handler 的繼承原則:先將日誌對像傳遞給子 logger 的所有 handler 處理,處理完畢後,如果該子 logger 的 propagate 屬性沒有設置為 0,則將日誌對像向上傳遞給第一個父 Logger,該父 logger 的所有 handler 處理完畢後,如果它的 propagate 也沒有設置為 0,則繼續向上層傳遞,以此類推。最終的狀態,要麼遇到一個 Logger,它的 propagate 屬性設置為了 0;要麼一直傳遞直到 rootLogger 處理完畢。 在上面實例代碼的基礎上,我們再添加一句代碼,即:  
#coding:utf-8

import logginglog1 = logging.getLogger(\"mydear\")log1.setLevel(logging.WARNING)log1.addHandler(StreamHandler)log2 = logging.getLogger(\"mydear.app\")log2.error(\"display\")log2.info(\"not display\")print log2.handlers #打印log2綁定的handler

輸出如下:  
display

說好的繼承,但是子 logger 竟然沒有綁定父類的 handler,what\'s wrong?看到下面調用 handler 的源代碼,就真相大白了。可以理解成,這不是真正的(類)繼承,只是"行為上的繼承":  
def callHandlers(self, record):\"\"\"Pass a record to all relevant handlers.

    Loop through all handlers for this logger and its parents in the    logger hierarchy. If no handler was found, output a one-off error    message to sys.stderr. Stop searching up the hierarchy whenever a    logger with the \"propagate\" attribute set to zero is found - that    will be the last logger whose handlers are called.    \"\"\"    c = self    found = 0    while c:for hdlr in c.handlers: #首先遍歷子 logger 的所有 handler    found = found + 1    if record.levelno >= hdlr.level:hdlr.handle(record)if not c.propagate:     #如果 logger 的 propagate 屬性設置為 0,停止    c = None    #break out else:   #否則使用直接父 logger    c = c.parent    ...  

額,最簡單的樣例牽引出來這麼多後台的邏輯,不過我們懂一下也是有好處的。下面,我們將一些零碎的不是很重要的東西羅列一下,這篇就結束了。1. 幾種 LogLevel 是全局變量,以整數形式表示,也可以但是不推薦自定義日誌級別,如果需要將 level 設置為用戶配置,則獲取 level 和檢查 level 的一般代碼是:  
#假設 loglevel 代表用戶設置的 level 內容

numeric_level = getattr(logging, loglevel.upper, None)if not isinstance(numeric_level, int): raise ValueError(\'Invalid log level: %s\' % loglevel)logging.basicConfig(level=numeric_level, ...)

2. format 格式,用於創建 formatter 對象,或者 basicConfig 中,就不翻譯了  
%(name)s Name of the logger (logging channel) %(levelno)s Numeric logging level for the message (DEBUG, INFO,WARNING, ERROR, CRITICAL) %(levelname)s Text logging level for the message (\"DEBUG\", \"INFO\",\"WARNING\", \"ERROR\", \"CRITICAL\") %(pathname)sFull pathname of the source file where the loggingcall was issued (if available) %(filename)sFilename portion of pathname %(module)s Module (name portion of filename) %(lineno)d Source line number where the logging call was issued(if available) %(funcName)sFunction name %(created)f Time when the LogRecord was created (time.timereturn value) %(asctime)s Textual time when the LogRecord was created %(msecs)d Millisecond portion of the creation time %(relativeCreated)d Time in milliseconds when the LogRecord was created,relative to the time the logging module was loaded(typically at application startup time) %(thread)d Thread ID (if available) %(threadName)s Thread name (if available) %(process)d Process ID (if available) %(message)s The result of record.getMessage, computed just asthe record is emitted

3. 寫日誌接口  
logging.warn(\"%s am a hero\", \"I\") #1 %格式以參數形式提供實參 logging.warn(\"%s am a hero\" % (\"I\",)) #2 直接提供字符串,也可以使用format,template logging.warn(\"%(name)s am a hero\", {\'name\':\"I\"}) #關鍵字參數
logging.warn(\"%(name)s am a hero\" % {\'name\':\"I\"}) #甚至這樣也可以 logging.warn(\"%(name)s am a hero, %(value)s\" % {\'name\':\"I\", \'value\':\'Yes\'}) #原來%也能解析關鍵字參數,不一定非是元組 如果關鍵字和位置參數混用呢,%應該不會有什麼作為了,最強也就能這樣: logging.warn(\"%(name)s am a hero, %s\" % {\'name\':\"I\" ,\'\': \'Yes\'})#也是字典格式化的原理

4. 配置 logging:上面已經講了如果配置 handler,綁定到 logger。如果需要一個稍微龐大的日誌系統,可以想像,我們會使用好多的 addHandler,SetFormatter 之類的,有夠煩了。幸好,logging 模塊提供了兩種額外配置方法,不需要寫眾多代碼,直接從配置結構中獲悉我們的配置意圖方式一:使用配置文件  
import loggingimport logging.configlogging.config.fileConfig(\'logging.conf\') # create logger

logger = logging.getLogger(\'simpleExample\') # \'application\' code

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=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=```

方式二:使用字典

請參閱 python2.7.9 Library 文檔,鏈接:

https://docs.python.org/2/library/logging.config.html?highlight=dictconfig#configuration-dictionary-schema

  1. 眾多的 handler 滿足不同的輸出需要

StreamHandler,FileHandler,NullHandler,RotatingFileHandler,TimedRotatingFileHandler,SocketHandler,DatagramHandler,SMTPHandler,SysLogHandler,NTEventLogHandler,MemoryHandler,HTTPHandler,WatchedFileHandler,

其中前三種在 logging 模塊中給出,其他的在 logging.handlers 模塊中給出。

《Python實戰-從菜鳥到大牛的進階之路》