Skip to content

Instantly share code, notes, and snippets.

@jlumbroso
Created November 16, 2020 04:43
Show Gist options
  • Save jlumbroso/1b3736b13cbf55a6e7aa52eb741eced5 to your computer and use it in GitHub Desktop.
Save jlumbroso/1b3736b13cbf55a6e7aa52eb741eced5 to your computer and use it in GitHub Desktop.
Decorator to log function calls with loguru in Python (written for the https://github.com/jlumbroso/restsdk project)
# Snippet licensed under The Unlicense (https://unlicense.org/)
# Originally part of https://github.com/jlumbroso/restsdk (which is LGPLv3 licensed)
import functools
import inspect
import os
import sys
import time
import typing
import loguru
__author__ = "Jérémie Lumbroso <lumbroso@cs.princeton.edu>"
__all__ = [
"logger",
"log_call",
]
ReturnType = typing.TypeVar("ReturnType") # return type
# Alias for loguru logger; turning colors on
logger = loguru.logger
logger.opt = functools.partial(logger.opt, colors=True)
# noinspection PyShadowingBuiltins
def log_call(
func: typing.Optional[typing.Callable[..., ReturnType]] = None,
# disallow positional arguments
*,
# keyword arguments
entry: bool = True,
exit: bool = True,
message: str = None,
include_result: bool = True,
include_timing: bool = True,
include_metadata: typing.Optional[dict] = None,
exclude_kwargs: typing.Collection = (),
level: str = "DEBUG",
) -> typing.Callable[[typing.Callable[..., ReturnType]], typing.Callable[..., ReturnType]]:
# SO: https://stackoverflow.com/a/47060298/408734
# GH: https://github.com/itamarst/eliot/issues/338#issuecomment-445124934
# RTFD: https://loguru.rtfd.io/en/stable/resources/recipes.html#logging-entry-and-exit-of-functions-with-a-decorator
if include_metadata is None:
include_metadata = dict()
# noinspection PyShadowingNames
def wrapper(func: typing.Callable[..., ReturnType]) -> typing.Callable[..., ReturnType]:
module_name = getattr(func, "__module__", None)
package_name = module_name.split(".", maxsplit=1)[0] if module_name is not None else None
function_data = dict(
name=func.__name__,
module=module_name,
package=package_name,
file=inspect.getfile(func),
argv=sys.argv,
pid=os.getpid(),
)
@functools.wraps(func)
def _log_call(*func_args, **func_kwargs) -> ReturnType:
call_data = dict(
args=func_args,
kwargs={k: v for k, v in func_kwargs.items() if k not in exclude_kwargs},
**function_data,
)
call_data.update(include_metadata)
logger_ = logger.opt(depth=1)
logger_ = logger_.bind(**include_metadata)
if include_timing:
call_data["start_time"] = time.time()
if entry:
msg = "Entering `{name}`"
msg_params = []
if len(call_data["args"]) > 0:
msg_params.append("args={args}")
if len(call_data["kwargs"]) > 0:
msg_params.append("kwargs={kwargs}")
if len(msg_params) > 0:
msg += " ({})".format(", ".join(msg_params))
if message is not None:
msg += " - "
msg += message
logger_.log(
level,
msg,
**call_data,
)
result = func(*func_args, **func_kwargs)
call_data["result"] = result
if include_timing:
call_data["stop_time"] = time.time()
call_data["timing"] = call_data["stop_time"] - call_data["start_time"]
if exit:
msg = "Exiting `{name}`"
msg_params = []
if include_result:
msg_params.append("result={result}")
if include_timing:
msg_params.append("timing={timing:.2f}")
if len(msg_params) > 0:
msg += " ({})".format(", ".join(msg_params))
if message is not None:
msg += " - "
msg += message
logger_.log(
level,
msg,
**call_data,
)
return result
return _log_call
if func is None:
return wrapper
else:
return wrapper(func=func)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment