Skip to content

Instantly share code, notes, and snippets.

@skewty
Last active August 18, 2022 20:28
Show Gist options
  • Save skewty/c90c705eb30018d5c27047951d4c1d77 to your computer and use it in GitHub Desktop.
Save skewty/c90c705eb30018d5c27047951d4c1d77 to your computer and use it in GitHub Desktop.
Python decorator to log synchronous function / method usage and exceptions
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