在 Python 记录器中覆盖 lineno 的最佳方法

Best way to override lineno in Python logger

我编写了一个装饰器,用于记录用于调用特定函数或方法的参数。如下所示,除了 logRecord 中报告的行号是装饰器的行号而不是被包装的 func 的行号外,它运行良好:

from functools import wraps
import inspect
import logging

arg_log_fmt = "{name}({arg_str})"


def log_args(logger, level=logging.DEBUG):
    """Decorator to log arguments passed to func."""
    def inner_func(func):
        line_no = inspect.getsourcelines(func)[-1]

        @wraps(func)
        def return_func(*args, **kwargs):
            arg_list = list("{!r}".format(arg) for arg in args)
            arg_list.extend("{}={!r}".format(key, val)
                            for key, val in kwargs.iteritems())
            msg = arg_log_fmt.format(name=func.__name__,
                                     arg_str=", ".join(arg_list))
            logger.log(level, msg)
            return func(*args, **kwargs)
        return return_func

    return inner_func

if __name__ == "__main__":
    logger = logging.getLogger(__name__)
    handler = logging.StreamHandler()
    fmt = "%(asctime)s %(levelname)-8.8s [%(name)s:%(lineno)4s] %(message)s"
    handler.setFormatter(logging.Formatter(fmt))
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)


    @log_args(logger)
    def foo(x, y, z):
        pass

    class Bar(object):
        @log_args(logger)
        def baz(self, a, b, c):
            pass

    foo(1, 2, z=3)
    foo(1, 2, 3)
    foo(x=1, y=2, z=3)

    bar = Bar()
    bar.baz(1, c=3, b=2)

此示例产生以下输出

2015-09-07 12:42:47,779 DEBUG    [__main__:  25] foo(1, 2, z=3)
2015-09-07 12:42:47,779 DEBUG    [__main__:  25] foo(1, 2, 3)
2015-09-07 12:42:47,779 DEBUG    [__main__:  25] foo(y=2, x=1, z=3)
2015-09-07 12:42:47,779 DEBUG    [__main__:  25] baz(<__main__.Bar object at 0x1029094d0>, 1, c=3, b=2)

注意行号都指向装饰器。

使用 inspect.getsourcelines(func) 我可以获得我感兴趣的行号,但是尝试在 logger.debug 中覆盖 lineno 会导致错误。让包装函数的行号出现在日志语句中的最佳方法是什么?

您不能轻易更改行号,因为 Logger.findCaller() method 通过内省提取此信息。

可以 为您生成的包装函数重新构建函数和代码对象,但这确实非常麻烦(请参阅我和 Veedrac 在 this post) 和 当你有错误时会导致问题,因为你的回溯将显示错误的源行!

您最好手动将行号以及您的模块名称(因为它们也可能不同)添加到您的日志输出中:

arg_log_fmt = "{name}({arg_str}) in {filename}:{lineno}"

# ...

codeobj = func.__code__
msg = arg_log_fmt.format(
    name=func.__name__, arg_str=", ".join(arg_list),
    filename=codeobj.co_filename, lineno=codeobj.co_firstlineno)

因为你总是在这里有一个函数,我使用了一些更直接的内省来通过关联的代码对象获取函数的第一行号。

正如 Martijn 指出的那样,事情有时会发生变化。但是,由于您使用的是 Python 2(iteritems 放弃了它),如果您不介意猴子修补日志记录,以下代码将起作用:

from functools import wraps
import logging

class ArgLogger(object):
    """
    Singleton class -- will only be instantiated once
    because of the monkey-patching of logger.
    """

    singleton = None

    def __new__(cls):
        self = cls.singleton
        if self is not None:
            return self
        self = cls.singleton = super(ArgLogger, cls).__new__(cls)
        self.code_location = None

        # Do the monkey patch exactly one time
        def findCaller(log_self):
            self.code_location, code_location = None, self.code_location
            if code_location is not None:
                return code_location
            return old_findCaller(log_self)
        old_findCaller = logging.Logger.findCaller
        logging.Logger.findCaller = findCaller

        return self

    def log_args(self, logger, level=logging.DEBUG):
        """Decorator to log arguments passed to func."""
        def inner_func(func):
            co = func.__code__
            code_loc = (co.co_filename, co.co_firstlineno, co.co_name)

            @wraps(func)
            def return_func(*args, **kwargs):
                arg_list = list("{!r}".format(arg) for arg in args)
                arg_list.extend("{}={!r}".format(key, val)
                                for key, val in kwargs.iteritems())
                msg = "{name}({arg_str})".format(name=func.__name__,
                                        arg_str=", ".join(arg_list))
                self.code_location = code_loc
                logger.log(level, msg)
                return func(*args, **kwargs)
            return return_func

        return inner_func


log_args = ArgLogger().log_args

if __name__ == "__main__":
    logger = logging.getLogger(__name__)
    handler = logging.StreamHandler()
    fmt = "%(asctime)s %(levelname)-8.8s [%(name)s:%(lineno)4s] %(message)s"
    handler.setFormatter(logging.Formatter(fmt))
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)


    @log_args(logger)
    def foo(x, y, z):
        pass

    class Bar(object):
        @log_args(logger)
        def baz(self, a, b, c):
            pass

    def test_regular_log():
        logger.debug("Logging without ArgLog still works fine")

    foo(1, 2, z=3)
    foo(1, 2, 3)
    foo(x=1, y=2, z=3)

    bar = Bar()
    bar.baz(1, c=3, b=2)
    test_regular_log()

另一种可能性是subclass Logger覆盖Logger.makeRecordThis is the method 如果您尝试更改 LogRecord:

中的任何标准属性(如 rv.lineno),则会引发 KeyError
for key in extra:
    if (key in ["message", "asctime"]) or (key in rv.__dict__):
        raise KeyError("Attempt to overwrite %r in LogRecord" % key)
    rv.__dict__[key] = extra[key]

通过删除此预防措施,我们可以通过提供一个 extra logger.log 调用的参数:

logger.log(level, msg, extra=dict(lineno=line_no))

from functools import wraps
import inspect
import logging

arg_log_fmt = "{name}({arg_str})"


def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
    """
    A factory method which can be overridden in subclasses to create
    specialized LogRecords.
    """
    rv = logging.LogRecord(name, level, fn, lno, msg, args, exc_info, func)
    if extra is not None:
        rv.__dict__.update(extra)
    return rv

def log_args(logger, level=logging.DEBUG, cache=dict()):
    """Decorator to log arguments passed to func."""
    logger_class = logger.__class__
    if logger_class in cache:
        UpdateableLogger = cache[logger_class]
    else:
        cache[logger_class] = UpdateableLogger = type(
            'UpdateableLogger', (logger_class,), dict(makeRecord=makeRecord))

    def inner_func(func):
        line_no = inspect.getsourcelines(func)[-1]
        @wraps(func)
        def return_func(*args, **kwargs):
            arg_list = list("{!r}".format(arg) for arg in args)
            arg_list.extend("{}={!r}".format(key, val)
                            for key, val in kwargs.iteritems())
            msg = arg_log_fmt.format(name=func.__name__,
                                     arg_str=", ".join(arg_list))
            logger.__class__ = UpdateableLogger
            try:
                logger.log(level, msg, extra=dict(lineno=line_no))
            finally:
                logger.__class__ = logger_class
            return func(*args, **kwargs)
        return return_func

    return inner_func

if __name__ == "__main__":
    logger = logging.getLogger(__name__)
    handler = logging.StreamHandler()
    fmt = "%(asctime)s %(levelname)-8.8s [%(name)s:%(lineno)4s] %(message)s"
    handler.setFormatter(logging.Formatter(fmt))
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)

    @log_args(logger)
    def foo(x, y, z):
        pass

    class Bar(object):

        @log_args(logger)
        def baz(self, a, b, c):
            pass

    foo(1, 2, z=3)
    foo(1, 2, 3)
    foo(x=1, y=2, z=3)

    bar = Bar()
    bar.baz(1, c=3, b=2)

产量

2015-09-07 16:01:22,332 DEBUG    [__main__:  48] foo(1, 2, z=3)
2015-09-07 16:01:22,332 DEBUG    [__main__:  48] foo(1, 2, 3)
2015-09-07 16:01:22,332 DEBUG    [__main__:  48] foo(y=2, x=1, z=3)
2015-09-07 16:01:22,332 DEBUG    [__main__:  53] baz(<__main__.Bar object at 0x7f17f75b0490>, 1, c=3, b=2)

    UpdateableLogger = type('UpdateableLogger', (type(logger),), 
                            dict(makeRecord=makeRecord))

创建一个新的 class,它是 type(logger) 的子 class,它覆盖了 makeRecord。 在 return_func 中,logger 的 class 更改为 UpdateableLogger,因此对 logger.log 的调用可以修改 lineno,然后是原始记录器 class恢复了。

通过这种方式——通过避免猴子修补 Logger.makeRecord——所有 loggers 在装饰函数之外的行为与以前完全一样。


相比之下,猴子修补方法是 shown here

这是一个旧的 post,但这个答案可能对其他人仍然有用。

现有解决方案的一个问题是 multiple parameters providing logging context,如果您想支持任意日志格式器,所有这些都需要修补。

原来这是raised as an issue with the Python logging library about a year ago, and as a result, the stacklevel keyword argument was added in Python 3.8。使用该功能,您可以修改日志记录调用以将堆栈级别设置为 2(在您的示例中调用 logger.log 的位置之上的级别):

logger.log(level, msg, stacklevel=2)

由于 Python 3.8 尚未发布(在本次回复时),您可以使用 findCaller and _log methods updated in Python 3.8.

猴子修补您的记录器

我有一个名为 logquacious, where I do the same sort of monkey-patching. You can reuse the patch_logger class that I've defined in logquacious 的日志记录实用程序库,并将上面的日志记录示例更新为:

from logquacious.backport_configurable_stacklevel import patch_logger

logger = logging.getLogger(__name__)
logger.__class__ = patch_logger(logger.__class__)

如 unutbu 的回答中所述,在使用范围之外撤消此猴子修补可能是个好主意,该文件中的其他一些代码就是这样做的。