讀懂掌握 Python logging 模塊源碼 (附帶一些 example)


搜了一下自己的 Blog 一直缺乏一篇 Python logging 模塊的深度使用的文章。其實這個模塊非常常用,也有非常多的濫用。所以看看源碼來詳細記錄一篇屬於 logging 模塊的文章。

整個 logging 模塊的主要部分 1700 來行代碼,還是很簡單的。我們從實際行為來帶大家過下代碼

 

當我們在寫 logging.info 的時候我們在干啥?

def info(msg, *args, **kwargs):
    if len(root.handlers) == 0:
        basicConfig()
    root.info(msg, *args, **kwargs)

可以看到默認使用的 root.info 接口。

如果我們直接調用的話會默認調用 basicConfig 的初始配置:

_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()

第一行就是拿一把鎖,我們可以容易判斷, logging 模塊應該是考慮了線程安全的情況。

這里我們沒有 filename 所以直接會初始化一個流 handler StreamHandler 在這里會傳入一個 None 最后會得到一個標准輸出 sys.stderr。

之后就是得到一些默認配置,然后設置一下輸出的格式化方法最后 set 上 handler。

最后為 root 添加上這個流處理的 handler。

完成后釋放鎖。

這樣我們就完成了 baseConfig 的初始化。 當然你在平時看到的其他 blog 中肯定有介紹初始化 config 的各種方法,其實他們在做的就是覆蓋上面那些代碼的一些參數。修改成自定義的參數。

 

那么可能大家會問了, root.info 這個 root 是什么定義呢?

我們來看代碼:

root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)

 

由於上面代碼是會在 logging 被 import 的時候就執行,所以 root 會直接被賦值。讓我們來看看 RootLogger 初始化的時候發生了什么。

class RootLogger(Logger):
    def __init__(self, level):
        """
        Initialize the logger with the name "root".
        """
        Logger.__init__(self, "root", level)

沒什么看點 讓我們來看父類 logger.__init__ 實現了什么

class Logger(Filterer):
    def __init__(self, name, level=NOTSET):
        Filterer.__init__(self)
        self.name = name
        self.level = _checkLevel(level)
        self.parent = None
        self.propagate = 1
        self.handlers = []
        self.disabled = 0

這里默認 name 是 root。 然后會初始化這些參數最后返回這個 logger 對象。

logger 有非常多的方法其中就包括 .info .error .warn 之類的直接打印日志的方法。

def info(self, msg, *args, **kwargs):
    """
    Log 'msg % args' with severity 'INFO'.

    To pass exception information, use the keyword argument exc_info with
    a true value, e.g.

    logger.info("Houston, we have a %s", "interesting problem", exc_info=1)
    """
    if self.isEnabledFor(INFO):
        self._log(INFO, msg, args, **kwargs)

當我們再調用 logging.info 的時候就是在調用 logger('root').info 

 

當我們知道這點時候 就繼續來看一下 logger 是怎么打日志的吧。

調用 self._log 方法其實就是調用 logger._log 方法

def _log(self, level, msg, args, exc_info=None, extra=None):
    if _srcfile:
        try:
            fn, lno, func = self.findCaller()
        except ValueError:
            fn, lno, func = "(unknown file)", 0, "(unknown function)"
    else:
        fn, lno, func = "(unknown file)", 0, "(unknown function)"
    if exc_info:
        if not isinstance(exc_info, tuple):
            exc_info = sys.exc_info()
    record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra)
    self.handle(record)

如果沒有什么特別的參數配置

會直接初始化 record 調用 makeRecord 方法。最后調用自己的 handle 方法。我們可以先來看下 makeRecord 方法。

在看這個代碼之前可以先為大家明確一下 logRecord 在 logging 日志模塊里面意義。就是說我們每打印的一條日志 其實就是一個  logRecord 實例。好我們來看代碼

def __init__(self, name, level, pathname, lineno,
             msg, args, exc_info, func=None):
    """
    Initialize a logging record with interesting information.
    """
    ct = time.time()
    self.name = name
    self.msg = msg
    #
    # The following statement allows passing of a dictionary as a sole
    # argument, so that you can do something like
    #  logging.debug("a %(a)d b %(b)s", {'a':1, 'b':2})
    # Suggested by Stefan Behnel.
    # Note that without the test for args[0], we get a problem because
    # during formatting, we test to see if the arg is present using
    # 'if self.args:'. If the event being logged is e.g. 'Value is %d'
    # and if the passed arg fails 'if self.args:' then no formatting
    # is done. For example, logger.warn('Value is %d', 0) would log
    # 'Value is %d' instead of 'Value is 0'.
    # For the use case of passing a dictionary, this should not be a
    # problem.
    # Issue #21172: a request was made to relax the isinstance check
    # to hasattr(args[0], '__getitem__'). However, the docs on string
    # formatting still seem to suggest a mapping object is required.
    # Thus, while not removing the isinstance check, it does now look
    # for collections.Mapping rather than, as before, dict.
    if (args and len(args) == 1 and isinstance(args[0], collections.Mapping)
        and args[0]):
        args = args[0]
    self.args = args
    self.levelname = getLevelName(level)
    self.levelno = level
    self.pathname = pathname
    try:
        self.filename = os.path.basename(pathname)
        self.module = os.path.splitext(self.filename)[0]
    except (TypeError, ValueError, AttributeError):
        self.filename = pathname
        self.module = "Unknown module"
    self.exc_info = exc_info
    self.exc_text = None      # used to cache the traceback text
    self.lineno = lineno
    self.funcName = func
    self.created = ct
    self.msecs = (ct - long(ct)) * 1000
    self.relativeCreated = (self.created - _startTime) * 1000
    if logThreads and thread:
        self.thread = thread.get_ident()
        self.threadName = threading.current_thread().name
    else:
        self.thread = None
        self.threadName = None
    if not logMultiprocessing:
        self.processName = None
    else:
        self.processName = 'MainProcess'
        mp = sys.modules.get('multiprocessing')
        if mp is not None:
            # Errors may occur if multiprocessing has not finished loading
            # yet - e.g. if a custom import hook causes third-party code
            # to run when multiprocessing calls import. See issue 8200
            # for an example
            try:
                self.processName = mp.current_process().name
            except StandardError:
                pass
    if logProcesses and hasattr(os, 'getpid'):
        self.process = os.getpid()
    else:
        self.process = None

可以看到各種參數,包括打印所需要的當前時間戳,名字要打印的內容。包括一些帶進來的參數,需要打印時候的 level 等級。都會被賦到這個 logRecord 實例上面。

拿到這個 record 之后就是執行 logger.handle 方法了。並且傳入 record 實例對象。

def handle(self, record):
    """
    Call the handlers for the specified record.

    This method is used for unpickled records received from a socket, as
    well as those created locally. Logger-level filtering is applied.
    """
    if (not self.disabled) and self.filter(record):
        self.callHandlers(record)

如果說沒有被 self.filter 過濾的話。注意這里的 handle 方法是是 logger 的。filter 方法卻是基類 filter 的。 filter 這里會去取之前 add 進 handler 的 filter 方法是一個數組, Logger 和 Handler 類的 父類都是 Filterer類 他會維護一個 self.filters 的數組。

然后我們調用 callHandlers 方法

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:
            found = found + 1
            if record.levelno >= hdlr.level:
                hdlr.handle(record)
        if not c.propagate:
            c = None    #break out
        else:
            c = c.parent
    if (found == 0) and raiseExceptions and not self.manager.emittedNoHandlerWarning:
        sys.stderr.write("No handlers could be found for logger"
                         " \"%s\"\n" % self.name)
        self.manager.emittedNoHandlerWarning = 1

通過 logger 上掛的 handler 數組,依次拿出來調用 handle 方法處理 record 對象。

def handle(self, record):
    """
    Conditionally emit the specified logging record.

    Emission depends on filters which may have been added to the handler.
    Wrap the actual emission of the record with acquisition/release of
    the I/O thread lock. Returns whether the filter passed the record for
    emission.
    """
    rv = self.filter(record)
    if rv:
        self.acquire()
        try:
            self.emit(record)
        finally:
            self.release()
    return rv

這個方法是 Handler 類的方法。還記得嗎 剛才我們初始化的是 StreamHandler 方法, Handler 類是它的父類。

然后這里會再次調用 handler 的 自身的 filter 方法。如果過濾之后依然有結果就加鎖 調起 emit 方法開始打日志。

這里會使用 StreamHandler 方法的 emit 方法。

def emit(self, record):
    """
    Emit a record.

    If a formatter is specified, it is used to format the record.
    The record is then written to the stream with a trailing newline.  If
    exception information is present, it is formatted using
    traceback.print_exception and appended to the stream.  If the stream
    has an 'encoding' attribute, it is used to determine how to do the
    output to the stream.
    """
    try:
        msg = self.format(record)
        stream = self.stream
        fs = "%s\n"
        if not _unicode: #if no unicode support...
            stream.write(fs % msg)
        else:
            try:
                if (isinstance(msg, unicode) and
                    getattr(stream, 'encoding', None)):
                    ufs = u'%s\n'
                    try:
                        stream.write(ufs % msg)
                    except UnicodeEncodeError:
                        #Printing to terminals sometimes fails. For example,
                        #with an encoding of 'cp1251', the above write will
                        #work if written to a stream opened or wrapped by
                        #the codecs module, but fail when writing to a
                        #terminal even when the codepage is set to cp1251.
                        #An extra encoding step seems to be needed.
                        stream.write((ufs % msg).encode(stream.encoding))
                else:
                    stream.write(fs % msg)
            except UnicodeError:
                stream.write(fs % msg.encode("UTF-8"))
        self.flush()
    except (KeyboardInterrupt, SystemExit):
        raise
    except:
        self.handleError(record)

然后就愉快的寫到 流里面去了,就結束了整個流程。

 

其實只要理解了這個流程,會發現打日志還是很簡單的。我們平常的使用無法就是在這個流程上修改一些配置,或者初始化的時候不使用 root 而使用自定義的 名字然后增加一些自定義的配置 XD.

 

 

Reference:

https://juejin.im/post/5b13fdd0f265da6e0b6ff3dd  Pycon2018 利用logging模塊輕松地進行Python日志記錄

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM