自定义记录器类和日志中的正确行号/功能名称

时间:2012-10-19 18:44:09

标签: python logging

我想将Python记录器包装在一个自定义类中,以嵌入一些特定于应用程序的功能,并隐藏开发人员的设置细节(设置文件输出,日志记录级别等)。为此,我使用以下API创建了一个类:

__init__(log_level, filename)
debug(msg)
info(msg)
warning(msg)
error(msg)

Logger.debug / info / warning / etc调用通常会在日志中写入进行日志调用的函数和行号。但是,使用我的自定义类,写入日志文件的函数和行号始终相同(对应于自定义类中的debug()/ info()/ warning()/ error()函数)。我希望它保存记录msg的应用程序代码行。这可能吗?

提前致谢。

6 个答案:

答案 0 :(得分:4)

如果您愿意重新实现一些标准日志记录模块,则可以生成日志包装器。诀窍是编写自己的findCaller()方法,该方法在解释反向跟踪时知道如何忽略日志包装器源文件。

logwrapper.py中的

import logging
import os
import sys

from logging import *


# This code is mainly copied from the python logging module, with minor modifications

# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame.
#
if hasattr(sys, 'frozen'): #support for py2exe
    _srcfile = "logging%s__init__%s" % (os.sep, __file__[-4:])
elif __file__[-4:].lower() in ['.pyc', '.pyo']:
    _srcfile = __file__[:-4] + '.py'
else:
    _srcfile = __file__
_srcfile = os.path.normcase(_srcfile)


class LogWrapper(object):
    def __init__(self, logger):
        self.logger = logger

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

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

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

    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.logger.isEnabledFor(INFO):
            self._log(INFO, msg, args, **kwargs)


    # Add other convenience methods

    def log(self, level, msg, *args, **kwargs):
        """
        Log 'msg % args' with the integer severity 'level'.

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

        logger.log(level, "We have a %s", "mysterious problem", exc_info=1)
        """
        if not isinstance(level, int):
            if logging.raiseExceptions:
                raise TypeError("level must be an integer")
            else:
                return
        if self.logger.isEnabledFor(level):
            self._log(level, msg, args, **kwargs)


    def _log(self, level, msg, args, exc_info=None, extra=None):
        """
        Low-level logging routine which creates a LogRecord and then calls
        all the handlers of this logger to handle the record.
        """
        # Add wrapping functionality here.
        if _srcfile:
            #IronPython doesn't track Python frames, so findCaller throws an
            #exception on some versions of IronPython. We trap it here so that
            #IronPython can use logging.
            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.logger.makeRecord(
            self.logger.name, level, fn, lno, msg, args, exc_info, func, extra)
        self.logger.handle(record)


    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.
        """
        f = logging.currentframe()
        #On some versions of IronPython, currentframe() returns None if
        #IronPython isn't run with -X:Frames.
        if f is not None:
            f = f.f_back
        rv = "(unknown file)", 0, "(unknown function)"
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == _srcfile:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

使用它的一个例子:

import logging
from logwrapper import LogWrapper

logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(filename)s(%(lineno)d): "
                    "%(message)s")
logger = logging.getLogger()
lw = LogWrapper(logger)

lw.info('Wrapped well, this is interesting')

答案 1 :(得分:1)

是:sys._getframe(NUM)其中NUM表示您正在寻找的当前项目之外的功能数量。返回的框架对象具有f_linenof_code.co_filename等属性。

http://docs.python.org/library/sys.html#sys._getframe

答案 2 :(得分:1)

基于@Will Ware的答案。另一个选择是覆盖findCaller方法并将自定义类用作默认记录器:

class MyLogger(logging.Logger):
    """
        Needs to produce correct line numbers
    """
    def findCaller(self):
        n_frames_upper = 2
        f = logging.currentframe()
        for _ in range(2 + n_frames_upper):  # <-- correct frame
            if f is not None:
                f = f.f_back
        rv = "(unknown file)", 0, "(unknown function)"
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == logging._srcfile:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

logging.setLoggerClass(MyLogger)
logger = logging.getLogger('MyLogger')  # init MyLogger
logging.setLoggerClass(logging.Logger) # reset logger class to default

答案 3 :(得分:0)

这是重写findCaller的又一次尝试。这使您可以基于每个功能自定义额外的堆栈帧深度。

import os
import logging
from contextlib import contextmanager

logging.basicConfig(
    format='%(asctime)-15s  %(levelname)s  %(filename)s:%(lineno)d  %(message)s',
    level=logging.INFO
)


@contextmanager
def logdelta(n, level=logging.DEBUG):
    _frame_stuff = [0, logging.Logger.findCaller]

    def findCaller(_):
        f = logging.currentframe()
        for _ in range(2 + _frame_stuff[0]):
            if f is not None:
                f = f.f_back
        rv = "(unknown file)", 0, "(unknown function)"
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == logging._srcfile:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

    rootLogger = logging.getLogger()
    isEnabled = rootLogger.isEnabledFor(level)
    d = _frame_stuff[0]
    try:
        logging.Logger.findCaller = findCaller
        _frame_stuff[0] = d + n
        yield isEnabled
    except:
        raise
    finally:
        logging.Logger.findCaller = _frame_stuff[1]
        _frame_stuff[0] = d


def A(x):
    with logdelta(1):
        logging.info('A: ' + x)    # Don't log with this line number

def B(x):
    with logdelta(2):
        logging.info('A: ' + x)    # or with this line number

def C(x):
    B(x)                       # or this line number

A('hello')                     # Instead, log with THIS line number
C('hello')                     # or THIS line number```

答案 4 :(得分:0)

更改行号和文件名的最佳位置(如果确实要更改)在记录器附带的过滤器中。这是概念证明。最后一行将记录一条自定义消息,其中包含文件名和最后一行:

import logging
import inspect
from pathlib import Path

class up_stacked_logger:
    def __init__(self, logger, n):
        self.logger = logger

        calling_frame = inspect.stack()[n+1].frame
        trace = inspect.getframeinfo(calling_frame)  

        class UpStackFilter(logging.Filter):
            def filter(self, record):       
                record.lineno = trace.lineno
                record.pathname = trace.filename
                record.filename = Path(trace.filename).name
                return True

        self.f = UpStackFilter()

    def __enter__(self):
        self.logger.addFilter(self.f)
        return self.logger

    def __exit__(self, *args, **kwds):
        self.logger.removeFilter(self.f)


def my_cool_customized_log_function(logger, msg):  

    with up_stacked_logger(logger, n=1) as logger:
        logger.info(f'--> customize {msg}')


logging.basicConfig(level=logging.DEBUG, format="[%(name)s][%(levelname)s]  %(message)s (%(filename)s:%(lineno)d)")
logger = logging.getLogger('some.module.logger')  

my_cool_customized_log_function(logger, 'a message')

答案 5 :(得分:0)

尝试stacklevel,它计算从原始日志记录呼叫到记录器的debug()info()等呼叫的呼叫数。它是logging 3.8中的新功能:

  

第三个可选关键字参数是堆栈级别,默认为   1.如果大于1,则在计算设置中的行号和函数名称时,将跳过相应的堆栈帧数   为记录事件创建的LogRecord。可用于记录   助手,以便记录函数名称,文件名和行号   不是有关辅助功能/方法的信息,而是   呼叫者。此参数的名称反映了   警告模块。