Created
November 16, 2020 04:43
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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