在Python logger中重写lineno的最佳方法

在Python logger中重写lineno的最佳方法,python,logging,decorator,Python,Logging,Decorator,我编写了一个decorator,它记录用于调用特定函数或方法的参数。如下图所示,除了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 l

我编写了一个decorator,它记录用于调用特定函数或方法的参数。如下图所示,除了
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)
2015-09-07 12:42:47779调试
2015-09-07 12:42:47779调试
调试(y=2,x=1,z=3)
2015-09-07 12:42:47779调试
请注意,行号都指向装饰器


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

您无法轻松更改行号,因为通过内省提取此信息

您可以为生成的包装器函数重新构建函数和代码对象,但这确实非常棘手(请参见我和Veedrac的介绍),并且在出现错误时会导致问题,因为回溯将显示错误的源代码行

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

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提供了它),如果您不介意对日志进行monkey修补,那么以下代码可以工作:

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

另一种可能是子类化
Logger
以覆盖
Logger.makeRecord
。如果您试图更改
日志记录中的任何标准属性(如
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]
通过删除此预防措施,我们可以通过提供
logger.log调用的
extra
参数:

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

屈服

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)
创建一个新类,该类是
type(logger)
的子类,它覆盖
makeRecord
。 在
return\u func
中,
logger
的类更改为
UpdateableLogger
,因此对
logger.log
的调用可以修改
lineno
,然后还原原始的记录器类

通过这样做——通过避免猴子补丁
Logger.makeRecord
——所有
Logger
在修饰函数之外的行为与以前完全相同



相比之下,猴子补丁的方法是。

这是一篇老文章,但这个答案可能对其他人仍然有用

现有解决方案的一个问题是存在,如果您想支持任意日志格式化程序,所有这些都需要修补

事实证明,这是,因此。使用该功能,您可以修改日志调用,将stacklevel设置为2(在您的示例中调用
logger.log
的位置上方的一个级别):

logger.log(级别、消息、堆栈级别=2)
由于Python3.8尚未发布(在做出此响应时),因此您可以使用

我有一个名为的日志实用程序库,在那里我做同样的猴子补丁。您可以重用并使用以下内容更新上面的日志记录示例:

来自logquacious.backport\u可配置的\u堆栈级导入修补程序\u记录器
logger=logging.getLogger(_名称__)
记录器.\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu类记录器

正如unutbu在回答中提到的,在使用范围之外撤销这个猴子补丁可能是一个好主意,这是该文件中其他一些代码所做的。

感谢您的输入。我是否必须将
我的日志\u信息
传递到logger.log?另外,我在其他地方也经常使用记录器,所以我不想永久性地改变它。从这个角度来看,我对猴子补丁感到不舒服。不,这是用2.7编写的(试试!)。这个补丁也不太可怕——从其他地方登录将一如既往地工作,因为如果我们不是从这里登录,就会调用原始代码。谢谢,看起来确实如此。我需要做更多的测试,然后让您的解决方案通过代码审查;-)然后我接受这个答案。清理了它并添加了一个测试,显示常规日志记录不受影响。该方法可能会起作用,但不幸的是,当前的实现改变了其他日志记录程序的行为(他们目前会引发一个异常,因为他们试图将None传递给dict.update()。@PatrickMaupin:感谢您的更正(关于例外情况)。现在已修复
如果extra不是None
。您仍然放弃了该函数的一些原始行为——但如果您执行创建特殊dict子类之类的操作,应该不会太难恢复它。我已修改了解决方案,以避免对
logging.Logger.makeRecord
进行猴子修补。现在,所有记录器的行为都将精确在修饰函数外,y和以前一样。只有在修饰函数内,lineno才能更改。你知道,我想如果我需要这个,我会替换这个函数,除非是永久性的——在我看来,用这种方式来阻止程序员的意图是难以置信的不和谐的。
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))