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')
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")
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.
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')
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")
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.