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