本文來源於對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物件。我們可以通過例項來驗證一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#test_logger1.py #coding:utf-8 import logging print logging.getLogger("mydear") import test_logger2 test_logger2.run() #呼叫檔案2中的函式,保證兩個模組共同處於生存期 #test_logger2.py #coding:utf-8 import logging def 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之後就進行了寫日誌操作:
1 2 3 4 5 6 |
#coding:utf-8 import logging logging.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,輸出目的地是標準流。從原始碼可以更清楚的看出來:
1 |
root = RootLogger(WARNING) #設定WARNING的級別 |
至於rootLogger的輸出目的地的配置,我們跟蹤logging.debug的原始碼來看一下:
1 2 3 4 5 6 7 |
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的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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就是控制日誌流向的,所以我們要跟蹤它)的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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函式用來配置RootLogger
basicConfig函式僅用來配置RootLogger,rootLogger是所有Logger的祖先Logger,所以其他一切Logger會繼承該Logger的配置。
從上面的basicConfig原始碼看,它可以有六個關鍵字引數,分別為:
filename:執行使用該檔名為rootLogger建立FileHandler,而不是StreamHandler
filemode:指定檔案開啟方式,預設是”a”
stream:指定一個流來初始化StreamHandler。此引數不能和filename共存,如果同時提供了這兩個引數,則stream引數被忽略
format:為rootLogger的handler指定輸出格式
datefmt:指定輸出的日期時間格式
level:設定rootLogger的日誌級別
使用樣例:
1 2 3 4 5 6 7 8 |
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的名稱可以隨意,要注意‘.’表示的繼承關係。
1 2 3 4 5 6 7 8 9 |
#coding:utf-8 import logging log1 = 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的所見即所得:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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.level logger = logger.parent return NOTSET |
handler的繼承
原則:先將日誌物件傳遞給子logger的所有handler處理,處理完畢後,如果該子logger的propagate屬性沒有設定為0,則將日誌物件向上傳遞給第一個父Logger,該父logger的所有handler處理完畢後,如果它的propagate也沒有設定為0,則繼續向上層傳遞,以此類推。最終的狀態,要麼遇到一個Logger,它的propagate屬性設定為了0;要麼一直傳遞直到rootLogger處理完畢。
在上面例項程式碼的基礎上,我們再新增一句程式碼,即:
1 2 3 4 5 6 7 8 9 10 |
#coding:utf-8 import logging log1 = 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的原始碼,就真相大白了。可以理解成,這不是真正的(類)繼承,只是”行為上的繼承”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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的一般程式碼是:
1 2 3 4 5 |
#假設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)s Full pathname of the source file where the logging
call was issued (if available)
%(filename)s Filename portion of pathname
%(module)s Module (name portion of filename)
%(lineno)d Source line number where the logging call was issued
(if available)
%(funcName)s Function name
%(created)f Time when the LogRecord was created (time.time()
return 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 as
the 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模組提供了兩種額外配置方法,不需要寫眾多程式碼,直接從配置結構中獲悉我們的配置意圖
方式一:使用配置檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import logging import logging.config logging.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=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= |
方式二:使用字典
請參閱python2.7.9 Library文件,連結:
5.眾多的handler滿足不同的輸出需要
StreamHandler,FileHandler,NullHandler,RotatingFileHandler,TimedRotatingFileHandler,SocketHandler,DatagramHandler,SMTPHandler,SysLogHandler,NTEventLogHandler,MemoryHandler,HTTPHandler,WatchedFileHandler,
其中前三種在logging模組中給出,其他的在logging.handlers模組中給出。