Last active
August 18, 2022 20:28
-
-
Save skewty/c90c705eb30018d5c27047951d4c1d77 to your computer and use it in GitHub Desktop.
Python decorator to log synchronous function / method usage and exceptions
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
import logging | |
import sys | |
def _get_logger_in_scope(scope: dict) -> logging.Logger | None: | |
for prop in ("log", "logger", "LOG", "LOGGER"): | |
if prop in scope and isinstance(scope[prop], logging.Logger): | |
return scope[prop] | |
for value in scope.values(): | |
if isinstance(value, logging.Logger): | |
return value | |
return None | |
def logit(param: logging.Logger = None, level: str | int = logging.DEBUG): | |
param_is_callable = hasattr(param, "__call__") | |
logger = None if param_is_callable else param | |
if isinstance(level, str): | |
if not isinstance(level := logging.getLevelName(level), int): | |
raise ValueError(f"Invalid logging level {level!r}") | |
if logger is None: | |
context = sys._getframe().f_back # noqa | |
if (logger := _get_logger_in_scope(context.f_globals)) is None: | |
if (logger := _get_logger_in_scope(context.f_locals)) is None: | |
raise ValueError("Logger object could not be found") | |
def decorator(function: callable): | |
def wrapper(*args, **kwargs): | |
logger.log(level, f"{function.__qualname__}() enter") | |
try: | |
result = function(*args, **kwargs) | |
except Exception: # noqa | |
logger.exception(f"{function.__qualname__}() exception\n{'=' * 60}\n") | |
raise | |
else: | |
logger.log(level, f"{function.__qualname__}() exit") | |
return result | |
return wrapper | |
return decorator(param) if param_is_callable else decorator | |
# =============================================================================================== | |
logger2 = logging.getLogger("test2") # this logger would be auto-chosen based on its type | |
logger = logging.getLogger("test") # this logger is auto-chosen based on its variable name | |
class LogitTesting: | |
@logit | |
def test1(self): | |
pass | |
@logit(level="CRITICAL") | |
def test2(self): | |
pass | |
@logit(level=30) | |
def test3(self): | |
pass | |
@logit(logger2) | |
def test4(self): | |
pass | |
@logit(logger2, level="INFO") | |
def test5(self): | |
pass | |
@logit | |
def test5(self): | |
raise Exception("It logs exceptions too!") | |
logging.basicConfig(level="DEBUG") | |
[getattr(LogitTesting(), f"test{n}")() for n in range(1, 6)] | |
""" | |
Output produced is: | |
DEBUG:test:LogitTesting.test1() enter | |
DEBUG:test:LogitTesting.test1() exit | |
CRITICAL:test:LogitTesting.test2() enter | |
CRITICAL:test:LogitTesting.test2() exit | |
WARNING:test:LogitTesting.test3() enter | |
WARNING:test:LogitTesting.test3() exit | |
DEBUG:test2:LogitTesting.test4() enter | |
DEBUG:test2:LogitTesting.test4() exit | |
DEBUG:test:LogitTesting.test5() enter | |
ERROR:test:LogitTesting.test5() exception | |
============================================================ | |
Traceback (most recent call last): | |
...[call stack removed]... | |
Exception: It logs exceptions too! | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment