本文來源于對 py2.7.9 docs 中 howto-logging 部分加之源代碼的理解。官方文檔鏈接如下,我用的是下載的 pdf 版本,應該是一致的:https://docs.python.org/2/howto/logging.html
我們不按照文檔上由淺入深的講解順序,因為就這么點東西不至于有“入”這個動作。
使用 logging 模塊記錄日志涉及四個主要類,使用官方文檔中的概括最為合適:
logger提供了應用程序可以直接使用的接口;
handler將(logger創(chuàng)建的)日志記錄發(fā)送到合適的目的輸出;
filter提供了細度設備來決定輸出哪條日志記錄;
formatter決定日志記錄的最終輸出格式。
寫 log 的一般順序為:
一、創(chuàng)建logger:
我們不要通過 logging.Logger 來直接實例化得到 logger,而是需要通過 logging.getLogger("name")來生成 logger 對象。
不是說我們不能實現(xiàn) Logger 的實例化,而是我們期待的是同一個 name 得到的是同一個 logger,這樣多模塊之間可以共同使用同一個 logger,getLogger 正是這樣的解決方案,它內(nèi)部使用 loggerDict 字典來維護,可以保證相同的名字作為 key 會得到同一個 logger 對象。我們可以通過實例來驗證一下:
#test_logger1.py
#coding:utf-8
import logging
print logging.getLogger("mydear")
import test_logger2
test_logger2.run() #調(diào)用文件 2 中的函數(shù),保證兩個模塊共同處于生存期
#test_logger2.py
#coding:utf-8
import logging
def run():
print logging.getLogger("mydear")
輸出:
<logging.Logger object at 0x00000000020ECF28>
<logging.Logger object at 0x00000000020ECF28>
結(jié)果表明兩個文件中通過"mydear"調(diào)用 getLogger 可以保證得到的 logger 對象是同一個。而分別進行 Logger 類的實例化則不能保證。
有了 logger 之后就可以配置這個 logger,例如設置日志級別 setLevel,綁定控制器 addHandler,添加過濾器 addFilter 等。
配置完成后,就可以調(diào)用 logger 的方法寫日志了,根據(jù) 5 個日志級別對應有 5 個日志記錄方法,分別為logger.debug,logger.info,logger.warning,logger.error,logger.critical。
二、配置 Logger 對象的日志級別:
logger.setLevel(logging.DEBUG) #DEBUG 以上的日志級別會被此 logger 處理
三、創(chuàng)建 handler 對象
handler 負責將 log 分發(fā)到某個目的輸出,存在多種內(nèi)置的 Handler 將 log 分發(fā)到不同的目的地,或是控制臺,或是文件,或是某種形式的 stream,或是 socket 等。一個 logger 可以綁定多個 handler,例如,一條日志可以同時輸出到控制臺和文件中。
以 FileHandler 和 StreamHandler 為例:
logfile= logging.FileHandler("./log.txt") #創(chuàng)建一個handler,用于將日志輸出到文件中
console = logging.StreamHandler() #創(chuàng)建另一個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 創(chuàng)建時使用的關(guān)鍵字,最后會以列表的形式展現(xiàn),這不是重點。
四、綁定 handler 到 logger 中
至此 handlers 和 logger 已經(jīng)準備好了,下面我們就將 handlers 綁定到 logger 上,一個 logger 對象可以綁定多個 handler。
logger.addHandler(logfile) #logger 是通過 getLogger 得到的 Logger 對象
logger.addHandler(console)
五、使用 logger 真正寫日志
logger.debug("some debug message.")
logger.info("some info message.")
看上去,中間步驟(創(chuàng)建 handler,設置日志級別,設置輸出格式等)更像是配置 Logger,一旦配置完成則直接調(diào)用寫日志的接口即可,稍后這些日志將按照先前的配置輸出。
嗚呼,好多內(nèi)容啊,來點簡單的吧.
下面的代碼,是最簡單的。導入 logging 之后就進行了寫日志操作:
#coding:utf-8
import logging
logging.debug("debug mes")
logging.info("info mes")
logging.warning("warn mes")
控制臺輸出如下:
WARNING:root:warn mes
咦?發(fā)生了什么情況,為什么只輸出了 warning?handler、logger、formatter 去哪兒了?
-_-!說好的最簡單的呢?為了讓自己講信用,我盡可能把它解釋成“最簡單的”。
知識點 1:logger 間存在繼承關(guān)系
logger 通過名字來決定繼承關(guān)系,如果一個 logger 的名字是 "mydest",另一個 logger 的名字是"mydest.dest1"(getLogger("mydest.dest1")),那么就稱后者是前者的子 logger,會繼承前者的配置。上面的代碼沒有指定 logger,直接調(diào)用 logging.debug 等方法時,會使用所有 logger 的祖先類 RootLogger。
從上面的代碼運行結(jié)果可以猜測出,該 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,就會不帶參數(shù)運行 basicConfig 函數(shù)(*請看知識點 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()
因為參數(shù)為空,所以我們就看出了,該 rootLoger 使用了不帶參數(shù)的 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
不帶參數(shù)的StreamHandler將會把日志流定位到sys.stderr流,標準錯誤流同樣會輸出到控制臺
知識點 2:basicConfig 函數(shù)用來配置 RootLogger
basicConfig 函數(shù)僅用來配置 RootLogger,rootLogger 是所有 Logger 的祖先 Logger,所以其他一切 Logger 會繼承該 Logger 的配置。
從上面的 basicConfig 源碼看,它可以有六個關(guān)鍵字參數(shù),分別為:
filename:執(zhí)行使用該文件名為 rootLogger 創(chuàng)建 FileHandler,而不是 StreamHandler
filemode:指定文件打開方式,默認是"a"
stream:指定一個流來初始化 StreamHandler。此參數(shù)不能和 filename 共存,如果同時提供了這兩個參數(shù),則 stream 參數(shù)被忽略
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 配置的繼承關(guān)系
首先準備下繼承條件:log2 繼承自 log1,logger 的名稱可以隨意,要注意‘.’表示的繼承關(guān)系。
#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 寫日志時,優(yōu)先使用本身設置了的 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.level
logger = logger.parent
return NOTSET
handler 的繼承
原則:先將日志對象傳遞給子 logger 的所有 handler 處理,處理完畢后,如果該子 logger 的 propagate 屬性沒有設置為 0,則將日志對象向上傳遞給第一個父 Logger,該父 logger 的所有 handler 處理完畢后,如果它的 propagate 也沒有設置為 0,則繼續(xù)向上層傳遞,以此類推。最終的狀態(tài),要么遇到一個 Logger,它的 propagate 屬性設置為了 0;要么一直傳遞直到 rootLogger 處理完畢。
在上面實例代碼的基礎(chǔ)上,我們再添加一句代碼,即:
#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?
看到下面調(diào)用 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
...
額,最簡單的樣例牽引出來這么多后臺的邏輯,不過我們懂一下也是有好處的。
下面,我們將一些零碎的不是很重要的東西羅列一下,這篇就結(jié)束了。
#假設 loglevel 代表用戶設置的 level 內(nèi)容
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, ...)
%(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
logging.warn("%s am a hero", "I") #1 %格式以參數(shù)形式提供實參
logging.warn("%s am a hero" % ("I",)) #2 直接提供字符串,也可以使用format,template
logging.warn("%(name)s am a hero", {'name':"I"}) #關(guān)鍵字參數(shù)
logging.warn("%(name)s am a hero" % {'name':"I"}) #甚至這樣也可以
logging.warn("%(name)s am a hero, %(value)s" % {'name':"I", 'value':'Yes'}) #原來%也能解析關(guān)鍵字參數(shù),不一定非是元組
如果關(guān)鍵字和位置參數(shù)混用呢,%應該不會有什么作為了,最強也就能這樣:
logging.warn("%(name)s am a hero, %()s" % {'name':"I" ,'': 'Yes'})#也是字典格式化的原理
上面已經(jīng)講了如果配置 handler,綁定到 logger。如果需要一個稍微龐大的日志系統(tǒng),可以想象,我們會使用好多的 addHandler,SetFormatter 之類的,有夠煩了。幸好,logging 模塊提供了兩種額外配置方法,不需要寫眾多代碼,直接從配置結(jié)構(gòu)中獲悉我們的配置意圖
方式一:使用配置文件
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的內(nèi)容
[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 文檔,鏈接:
StreamHandler,F(xiàn)ileHandler,NullHandler,RotatingFileHandler,TimedRotatingFileHandler,SocketHandler,DatagramHandler,SMTPHandler,SysLogHandler,NTEventLogHandler,MemoryHandler,HTTPHandler,WatchedFileHandler,
其中前三種在 logging 模塊中給出,其他的在 logging.handlers 模塊中給出。