logging基本介紹
先介紹一下我們為什么要使用日志,平常我們編寫程序為了驗證程序運行與debug,通常會使用print函數來對一些中間結果進行輸出驗證,在驗證成功后再將print語句注釋或刪除掉。這樣做在小型程序中還比較靈活,但是對于大型項目來說,就十分繁瑣了----->所以使用日志log就很自然了,日志可以調整日志級別,來決定我們是否輸出對應級別的日志,同時還可以將日志導入文件記錄下來。
再介紹一下logging中的log級別:
Level
Numeric Value
logging.CRITICAL
50
logging.ERROR
40
logging.WARNING
30
logging.INFO
20
logging.DEBUG
10
實際上這些level都是整數值,可由如type(logging.info)驗證為int類型。
模塊級的使用方法
實際上logging是一個package而不是module,以下對于源碼的討論都是在logging的__init__.py文件中的。
logging模塊的模塊級使用方法就是使用一些模塊級接口函數。而且還有一個比較重要的是對日志的輸出形式和輸出目的地等進行設置。
接口函數也是對應日志級別而輸出信息的:
logging.debug(msg)
logging.info(msg)
logging.warning(msg)
logging.error(msg)
logging.critical(msg)
這幾個函數除了日志級別上的區別,其實都是使用默認的root logger來對信息進行log的,它是處于日志器層級關系最頂層的日志器,且該實例是以單例模式存在的。見源碼:
def info(msg, *args, **kwargs):
if len(root.handlers) == 0:
basicConfig()
root.info(msg, *args, **kwargs)
這里可以見到在logging模塊的info函數中:(1)首先進行了一個對于root logger的handlers屬性的長度判斷是否調用basicConfig函數。(2)之后是調用root logger的info函數來實現功能的。
這里對于第(2)點我們進一下探尋:
root = RootLogger(WARNING)
在logging的源碼中可以看到如上語句,即我們將logging模塊import后,其實已經默認的創建了一個root logger對象,并且logging自己維護有一個Logger實例的hierarchy(通過Manager實例對象,它和root logger一樣也是單例的使用模式),我們自己創建的logger實例對象,都是是root logger的孩子。
日志的設置
對于日志的設置,我們是使用logging.basicConfig(**kwargs)函數,它是對root logger進行設置的,一般使用較多的關鍵字參數如下:
level 決定root logger的日志級別。
format 它是日志格式字符串,指定日志輸出的字段信息,如日期,日志級別,文件名,當然還有我們的msg信息等等。
datefmt 決定日期字段的輸出格式。
filename 日志信息的輸出目的地文件,不指定時默認輸出到控制臺。
filemode 目的地文件的打開模式,不指定默認為"a"。
對于logging.basicConfig函數有一點需要注意:我們不能在該函數前使用任何模塊級日志輸出函數如logging.info、logging.error,因為它們會調用一個不帶參的basicConfig函數,使得logging.basicConfig函數失效。見源碼(由于代碼過多,建議參考注釋進行閱讀):
def basicConfig(**kwargs):
_acquireLock()
try:
#這里由于不帶參調用basicConifg,
#而root.handlers默認為空列表
#在Logger定義中可見self.handlers被設為[],
#而默認的root實例在創建時只指定了log級別
#所以if條件必然通過
if len(root.handlers) == 0:
#由于不帶參,所以handlers必為None
handlers = kwargs.pop("handlers", None)
if handlers is None:
#這里由于不帶參,所以即是handlers為None
#通過上面的if判斷,但kwargs同樣為None,
#所以該if不通過
if "stream" in kwargs and "filename" in kwargs:
raise ValueError("'stream' and 'filename' should not be "
"specified together")
else:
if "stream" in kwargs or "filename" in kwargs:
raise ValueError("'stream' or 'filename' should not be "
"specified together with 'handlers'")
#這里由于handlers為None通過if判斷繼續執行
if handlers is None:
filename = kwargs.pop("filename", None)
mode = kwargs.pop("filemode", 'a')
if filename:
h = FileHandler(filename, mode)
#不帶參,kwargs為None,所以filename
#在上面的執行語句的返回值為None,所以
#執行這個else分支
else:
stream = kwargs.pop("stream", None)
h = StreamHandler(stream)
#注意這里,十分重要,可見handlers終于不為None
#被賦予了一個列表,該列表有一個元素h
handlers = [h]
dfs = kwargs.pop("datefmt", None)
style = kwargs.pop("style", '%')
if style not in _STYLES:
raise ValueError('Style must be one of: %s' % ','.join(
_STYLES.keys()))
fs = kwargs.pop("format", _STYLES[style][1])
fmt = Formatter(fs, dfs, style)
#再看這里,十分重要
for h in handlers:
#這個無所謂,就是對format進行默認設置
if h.formatter is None:
h.setFormatter(fmt)
#這里最為關鍵,可見root.addHandler(h)函數
#會把h添加進root.handlers列表中,那么很顯然
#root.handlers不再是一個空列表
root.addHandler(h)
level = kwargs.pop("level", None)
if level is not None:
root.setLevel(level)
if kwargs:
keys = ', '.join(kwargs.keys())
raise ValueError('Unrecognised argument(s): %s' % keys)
finally:
_releaseLock()
所以即是不帶參調用basicConfig(),但是經過其函數體執行,root.handlers的列表長度會不為0,所以之后再調用logging.basicConifg函數時,對root.handlers判斷,就會因此而直接略過函數體中try部分(主要部分),直接執行finally,沒有進行任何設置。(并且很關鍵的basicConfig函數就是對root logger的handlers進行設置)
對象級使用
在logging模塊中logger對象從來都不是直接實例化,而是通過一個模塊級借口完成:logging.getLogger(name=None),注意我們創建的logger都是root logger的子類。而通過我們自己創建的logger對象,使用日志記錄也是和模塊級接口一樣的:
logger.debug(msg)
logger.info(msg)
logger.warning(msg)
logger.error(msg)
logger.critical(msg)
同樣對于日志格式的設置也是通過logging.basicConfig函數完成的,雖然該函數是對root logger實例對象的日志格式設置,但由于Logger類方法的特殊實現,其實是能夠沿用該設置的。
并且對于對象級接口,如logger.info函數:
def info(self, msg, *args, **kwargs):
if self.isEnabledFor(INFO):
self._log(INFO, msg, args, **kwargs)
可見其中沒有對basicConfig函數的調用,所以也就沒有修改root.handlers列表,即不會發生上文的logging.basciConfig函數失效的問題。
Logger類及basicConfig沿用問題(無興趣可略)
上文提到了由于Logger類中方法的特殊實現,使得之后實例化的logger對象在進行日志輸出記錄時也能沿用root logger的basicConfig設定。而這背后的機制到底是怎樣的呢?先看:
class RootLogger(Logger):
def __init__(self, level):
"""
Initialize the logger with the name "root".
"""
Logger.__init__(self, "root", level)
_loggerClass = Logger
.....
#由上面代碼可見RootLogger其實就是調用了Logger類
root = RootLogger(WARNING)
#這里是對Logger類綁定兩個類級屬性
#把root實例作為Logger類的一個root屬性
Logger.root = root
#把root實例作為參數傳入Manager類,
#并用返回的實例定義Logger類的manager屬性
#并且Manager類只會被實例化這一次
Logger.manager = Manager(Logger.root)
以上源碼中出現的Manager類很重要,在我們的logging.getLogger函數中:
def getLogger(name=None):
"""
Return a logger with the specified name,
creating it if necessary.
If no name is specified, return the root logger.
"""
if name:
#結合上文的代碼,我們使用root實例作為參數
#傳入Manager類定義了Logger.manager屬性,
#此處使用manager實例所有的getLogger方法
#來具體實現模塊級的getLogger方法
return Logger.manager.getLogger(name)
else:
return root
小結:
Logger.manager屬性是Manager(root)的實例,而logging.getLogger其實就是由Logger.manager對象調用它的getLogger方法。
再轉到Manager類的定義中,來分析Manager(Logger.root)創建manager實例對象和用logger.getLogger時具體發生了什么:
class Manager(object):
"""
holds the hierarchy of loggers.
"""
#Logger.manager = Manager(Logger.root)發生的事情
def __init__(self, rootnode):
"""
Initialize the manager with the root node of the logger hierarchy.
"""
#結合上文Logger.manager = Manager(Logger.root)
#可見我們使用root logger作為rootnode,并把它賦予
#Manager類的self.root屬性
self.root = rootnode
self.disable = 0
self.emittedNoHandlerWarning = False
#self.loggerDict維護logger hierarchy中的loggers
self.loggerDict = {}
self.loggerClass = None
self.logRecordFactory = None
#使用logging.getLogger時發生的事情,Logger.manager屬性
#也就是使用root logger作為參數的Manager實例調用該實例
#所屬Manager類的getLogger方法
def getLogger(self, name):
#這里創建一個值為None的rv
rv = None
#不用理會,只是判斷name值是否為str
if not isinstance(name, str):
raise TypeError('A logger name must be a string')
_acquireLock()
try:
#這里由于是使用Logger.manager實例來調用
#getLogger方法,而Logger.manager實例的
#self.loggerDict初始值為空,所以在第一
#次使用getLogger方法時,這個判斷不通過
#就算之后再次調用logging.getLogger(name)
#也要看name是否在loggerDict中
if name in self.loggerDict:
rv = self.loggerDict[name]
if isinstance(rv, PlaceHolder):
ph = rv
rv = (self.loggerClass or _loggerClass)(name)
rv.manager = self
self.loggerDict[name] = rv
self._fixupChildren(ph, rv)
self._fixupParents(rv)
#所以直接轉到此處
else:
#這里self.loggerClass初始為空,使用
#_loggerClass其實就是Logger類實例化
#一個logger對象附于rv
rv = (self.loggerClass or _loggerClass)(name)
#這里再把self也就是Logger.manager賦予
#rv.manager,并未創建新的Manager實例
rv.manager = self
#把rv加入loggerDict中,name:rv鍵值對
self.loggerDict[name] = rv
#設定當前logger實例的parent
self._fixupParents(rv)
finally:
_releaseLock()
#返回rv,也就是創建的logger實例對象
return rv
大概介紹了Manager的部分源碼,我們回到最初的問題,為何我們自己創建的logger實例能沿用root logger的basicConfig設定,再看logger.info方法的源碼:
#logger實例的info方法
def info(self, msg, *args, **kwargs):
#對比INFO日志級別和當前logger實例的日志級別來決定是否進行log
if self.isEnabledFor(INFO):
#所以這里其實是調用了logger實例的底層_log方法
self._log(INFO, msg, args, **kwargs)
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
"""
Low-level logging routine which
creates a LogRecord and then calls
all the handlers of this logger to
handle the record.
"""
#為避免繁瑣,把一些簡單使用時不會碰到的代碼略過
...
#生成log record
record = self.makeRecord(self.name, level, fn, lno, msg, args,
exc_info, func, extra, sinfo)
#調用當前logger實例的handle方法來處理log record
self.handle(record)
def handle(self, record):
if (not self.disabled) and self.filter(record):
#可見logger類的handle方法又調用了該類的
#callHandlers方法來處理record
self.callHandlers(record)
#注意看源碼中自帶注釋對于callHandlers方法解釋,其中
#很重要的一點就是,該函數會loop through該logger乃至
#該logger的parent,parent的parent一直到root logger
#中的handlers,注意我們在前文中說了,basicConfig其實
#就是對root logger的handlers進行設置
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.
"""
#把當前logger實例賦予c
c = self
found = 0
#這個while一定可以進去,因為c是一個logger對象,不為None
while c:
#對于我們創建的logger,它的handlers初始值是空列表
#所以這個for一開始進不去,對于我們的簡單使用場景
#使用了logging.basicConfig(...),然后創建自己的
#logger=logging.getLogger("mylogger"),當前
#logger的parent肯定是root logger,而前面的
#basicConfig對root logger的handlers設置了,
#所以root logger的handlers不為空,可以進入
for hdlr in c.handlers:
found = found + 1
if record.levelno >= hdlr.level:
#使用handlers中的handler對
#log record進行handle處理
hdlr.handle(record)
#這個也進不去,logger實例的propagate都是默認True
if not c.propagate:
c = None #break out
#只能進這里了
else:
#把當前logger實例的parent賦予c
#然后繼續循環,如果handlers仍然為空
#則繼續把parent賦予c直到root logger
c = c.parent
...
簡而言之,我們自己創建的logger能使用parent(Manager實例所維持的一個hierarchy)的handlers對log信息進行handle,而向上追溯的祖先就是root logger,我們已經通過basicConfig的話,實質上就是對它的handlers進行了設置,所以在這個hierarchy中的后代便能享用。