I have written a decorator which logs the arguments used to call a particular function or method. As shown below, it works well except that the line number reported in the logRecord
is the line number of the decorator rather than the line number of func
that is being wrapped:
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)
This sample results in the following output
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)
Note that the line numbers all point to the decorator.
With inspect.getsourcelines(func)
I can get the line number I'm interested in, but an attempt to over-write lineno
in logger.debug
results in an error. What is the best approach to getting the line number of the wrapped function to appear in the logging statement?
As Martijn points out, things sometimes change. However, since you're using Python 2 (the iteritems gave it away), the following code will work if you don't mind monkey-patching logging:
You can't easily change the line number, because the
Logger.findCaller()
method extracts this information via introspection.You could re-build the function and code objects for the wrapper function you generate, but that is very hairy indeed (see the hoops I and Veedrac jump through on this post) and will lead to problems when you have a bug, as your traceback will show the wrong source lines!
You'd be better off adding the line number, as well as your module name (since that can differ too) to your log output manually:
Since you always have a function here, I used some more direct introspection to get the first line number for the function, via the associated code object.
Another possibility is to subclass
Logger
to overrideLogger.makeRecord
. This is the method that raises aKeyError
if you try to change any of the standard attributes (likerv.lineno
) in theLogRecord
:By removing this precaution, we can override the lineno value by supplying an
extra
argument to thelogger.log
call:yields
The line
creates a new class which is a subclass of
type(logger)
which overridesmakeRecord
. Insidereturn_func
, thelogger
's class is changed toUpdateableLogger
so the call tologger.log
can modifylineno
and then the original logger class is restored.By doing it this way -- by avoiding monkey-patching
Logger.makeRecord
-- alllogger
s behave exactly as before outside the decorated functions.For comparison, the monkey-patching approach is shown here.