Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using functools.wraps with a logging decorator

Tags:

python

logging

I'm trying to write a simple decorator that logs a given statement before calling the decorated function. The logged statements should both appear to come from the same function, which I thought was the purpose of functools.wraps().

Why does the following code:

import logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(funcName)20s - %(message)s')

from functools import wraps

def log_and_call(statement):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            logging.info(statement)            
            return func(*args, **kwargs)
        return wrapper
    return decorator


@log_and_call("This should be logged by 'decorated_function'")
def decorated_function():
    logging.info('I ran')

decorated_function()

result in log statements like:

             wrapper - This should be logged by 'decorated_function'
  decorated_function - I ran

I thought the call to wraps would rename wrapper with decorated_function's name.

I'm using python 2.7.1.

like image 983
AndrewF Avatar asked Aug 09 '11 22:08

AndrewF


People also ask

What is the use of Functools wraps?

wraps() function. functools is a standard Python module for higher-order functions (functions that act on or return other functions). wraps() is a decorator that is applied to the wrapper function of a decorator.

Why do we need wrapper function in decorators?

The purpose of having a wrapper function is that a function decorator receives a function object to decorate, and it must return the decorated function.

What is logging decorator?

The logging decorator, logs the file and function name where it's defined not where it's used. For example, if we are using a decorator to log the file main.py and defined the log decorator in log_decorator.py.

What does Functools do in Python?

The functools module, part of Python's standard Library, provides useful features that make it easier to work with high order functions (a function that returns a function or takes another function as an argument ).


3 Answers

Unfortunately logging uses the function code object to infer the name. You could work around this by using the extra keyword argument to specify some additional attributes for the record, which you could then use during formatting. You could do something like:

logging.basicConfig(
    level=logging.DEBUG,
    format='%(real_func_name)20s - %(message)s',
)

...

logging.info(statement, extra={'real_func_name': func.__name__})

The only downside to this approach is that you have to pass in the extra dictionary every time. To avoid that you could use a custom formatter and have it override funcName:

import logging
from functools import wraps

class CustomFormatter(logging.Formatter):
    """Custom formatter, overrides funcName with value of name_override if it exists"""
    def format(self, record):
        if hasattr(record, 'name_override'):
            record.funcName = record.name_override
        return super(CustomFormatter, self).format(record)

# setup logger and handler
logger = logging.getLogger(__file__)
handler = logging.StreamHandler()
logger.setLevel(logging.DEBUG)
handler.setLevel(logging.DEBUG)
handler.setFormatter(CustomFormatter('%(funcName)20s - %(message)s'))
logger.addHandler(handler)

def log_and_call(statement):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # set name_override to func.__name__
            logger.info(statement, extra={'name_override': func.__name__})
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log_and_call("This should be logged by 'decorated_function'")
def decorated_function():
    logger.info('I ran')

decorated_function()

Which does what you want:

% python logging_test.py
  decorated_function - This should be logged by 'decorated_function'
  decorated_function - I ran
like image 56
zeekay Avatar answered Oct 16 '22 14:10

zeekay


I have found in docs how it can be done, just add this code to your decorator:

def log_and_call(statement):        
    def decorator(func):
        old_factory = logging.getLogRecordFactory()

        def record_factory(*args, **kwargs):
            record = old_factory(*args, **kwargs)
            record.funcName = func.__name__
            return record

        def wrapper(*args, **kwargs):
            logging.setLogRecordFactory(record_factory)
            logging.info(statement)
            logging.setLogRecordFactory(old_factory)
            return func(*args, **kwargs)
        return wrapper
    return decorator

or instead of functools.wrap use this decorator:

def log_wrapper(func_overrider):
    old_factory = logging.getLogRecordFactory()

    def new_factory(*args, **kwargs):
        record = old_factory(*args, **kwargs)
        record.funcName = func_overrider.__name__
        return record

    def decorator(func):
        def wrapper(*args, **kwargs):
            logging.setLogRecordFactory(new_factory)
            result = func(*args, **kwargs)
            logging.setLogRecordFactory(old_factory)
            return result

        return wrapper

    return decorator
like image 45
Ivan Bryzzhin Avatar answered Oct 16 '22 15:10

Ivan Bryzzhin


Unlike you may suspect, the logging.functions do no use the __name__ attribute. This implies using @wraps (or setting the __name__ of the wrapper manually) does not work!

Instead, the show that name, the call-frame is examined. It contains a list of code-items (basically the stack). There the function name read, as well as the filename and line-number. When using a logging-decorator, the wrapper-name is always printed, as it is the one that calls log.

BTW. The logging.level() functions all call logging._log(*level*, ...), which calls other (log) functions as well. Which all end-up on the stack. To prevent those log-functions are shown, the list of frames is searched for the first (lowest) function which filename is not part of 'logging'. That should be the real function to log: the one calling logger.func().

Regrettably, it is wrapper.

It would, however, be possible to use a log-decorator: when it is part of the logging source file. But there is none (yet)

like image 1
Albert Avatar answered Oct 16 '22 13:10

Albert