在 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.makeRecord
。 This 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
——所有 logger
s 在装饰函数之外的行为与以前完全一样。
相比之下,猴子修补方法是 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 的回答中所述,在使用范围之外撤消此猴子修补可能是个好主意,该文件中的其他一些代码就是这样做的。
我编写了一个装饰器,用于记录用于调用特定函数或方法的参数。如下所示,除了 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.makeRecord
。 This 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
——所有 logger
s 在装饰函数之外的行为与以前完全一样。
相比之下,猴子修补方法是 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 的回答中所述,在使用范围之外撤消此猴子修补可能是个好主意,该文件中的其他一些代码就是这样做的。