Create a generic logger for function start and finish time in Python

Question:

I want to create a logger decorator that will print before and after a function, in addition I want to add some information that if avaiable will appear in each line, for example most of our current logs look like:

START: reading_data product_id=123, message_id=abc, timesteamp=123456789

assuming that I want the logger to be generic, I can’t assume that I will have all parameters in the function I’m decorating also I don’t know if I will get them as args or kwargs.
I know that I can use if 'product_id' in locals() but that only works inside reading_data function and not in the decorator. I also thought about adding args and kwargs to the log, but they could have lots of info (big dictionaries or lists)

I want it to de something like:

def log(func):
    def decorate(*args, **kwargs):
        text = ''
        if product_id:
            text += f'product_id={product_id}'
        if message_id:
            text += f'message_id={message_id}'
        if timesteamp:
            text += f'timesteamp={timesteamp}'

        print(f'START: {func.__name__} {text}')
        func(*args, **kwargs)
        print(f'FINISH {func.__name__} {text}')
    return decorate

@log
def reading_data(product_id, message_id, timesteamp=now()):
    print('Do work')
Asked By: shlomiLan

||

Answers:

You can use zip() and inspect.signature() to get the positional arguments with their names. Then you can use a list comprehension to convert those positional arguments and the keyword arguments to a string:

import inspect

def log(func):
    def decorate(*args, **kwargs):
        arg_params = zip(inspect.signature(func).parameters, args)
        args_text = ' '.join([f'{param}={arg}' for param, arg in arg_params])
        kwargs_text = ' '.join([f'{kwarg}={val}' for kwarg, val in kwargs.items()])
        
        print('START', func.__name__, args_text, kwargs_text)
        func(*args, **kwargs)
        print('FINISH', func.__name__, args_text, kwargs_text)
    return decorate

@log
def reading_data(product_id, message_id, timestamp):
    print('Do work')


reading_data(1, message_id=2, timestamp="4-17-23")
Answered By: Michael M.

inspect can be utilised for ‘binding’ arguments.

import inspect

def log(func):
    def wrapper(*args, **kwargs):
        ba = inspect.signature(func).bind(*args, **kwargs)
        ba.apply_defaults()
        call_sig = f"{func.__name__}({str(ba)[17:-2]})"

        print(call_sig)
        result = func(*args, **kwargs)
        print(call_sig, '->', result)
        return result
    return wrapper

The printed message is formatted so that it would run if copy and pasted. This is recommended for __repr__. One could argue the same reasoning applies here.

Returning the result of the func call is good practice. It’ll maintain the expected behaviour of the decorated function, i.e. that its returned value is passed to the caller. The decorator is written to be generally applicable and would easily work elsewhere in your program. It’s also possible that reading_data is enhanced to return a pass/fail result. The less you need to understand/remember about your code to use it safely, the better.

Answered By: Guy Gangemi