diff --git a/lodge.py b/lodge.py index 0fb1b34..61982ed 100644 --- a/lodge.py +++ b/lodge.py @@ -15,7 +15,7 @@ def _get_log_level_from_env_var(logger_name: str) -> str: return os.environ.get(env_var_key, DEFAULT_LOG_LEVEL) -def _get_format(): +def _get_default_format(): DEFAULT_LOG_ENV = os.environ.get("LOG_ENV", "PROD") LOG_BASE_FIELDS = eval( os.environ.get( @@ -38,12 +38,24 @@ def _add_base_configs(logger: logging.Logger): logging.addLevelName(logging.WARNING, "WARN") -def _add_handlers(logger: logging.Logger): +def _add_default_handler(logger: logging.Logger): stderr = logging.StreamHandler(DEFAULT_LOG_STREAM) - stderr.setFormatter(_get_format()) + stderr.setFormatter(_get_default_format()) logger.addHandler(stderr) +# https://github.com/python/cpython/blob/e8e341993e3f80a3c456fb8e0219530c93c13151/Lib/logging/__init__.py#L162 +if hasattr(sys, '_getframe'): + currentframe = lambda level: sys._getframe(level) # noqa +else: # pragma: no cover + def currentframe(level: int): + """Return the frame object for the caller's stack frame.""" + try: + raise Exception + except Exception: + return sys.exc_info()[2].tb_frame.f_back # type: ignore + + def get_logger(name: str) -> logging.Logger: """ Return a logger with configs based on the environment. @@ -54,7 +66,60 @@ def get_logger(name: str) -> logging.Logger: logger = logging.getLogger(name) _add_base_configs(logger) - _add_handlers(logger) + _add_default_handler(logger) logger.setLevel(_get_log_level_from_env_var(name)) return logger + + +class _ProxyLogger: + """ + Internal proxy Logger Class. It should not be used directly. Use `log` + or `logger`! + + From the proxy methods it will select the proper log to call. If it + not exists it will create a new one based on the callers `__name__`. + The created logger will have the config loaded from the env vars. + """ + def __init__(self): + self.manager = logging.root.manager + + def _get_or_create_logger(self, name: str) -> logging.Logger: + logger_already_initialized = name in self.manager.loggerDict.keys() + if logger_already_initialized: + return logging.getLogger(name) + else: + return get_logger(name) + + def _get_caller_module(self) -> str: + # Calling currentframe from here we need to go 4 levels deep to reach the module + # By changing currentframe call location this number will probably change. + STACK_LEVEL = 4 + frame = currentframe(STACK_LEVEL) + return frame.f_globals["__name__"] + + def _get_logger(self) -> logging.Logger: + caller_name = self._get_caller_module() + logger = self._get_or_create_logger(caller_name) + return logger + + # Proxy methods + def debug(self, msg, *args, **kwargs): + self._get_logger().debug(msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + self._get_logger().info(msg, *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + self._get_logger().warn(msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + self._get_logger().error(msg, *args, **kwargs) + + def fatal(self, msg, *args, **kwargs): + self._get_logger().fatal(msg, *args, **kwargs) + + +logger = _ProxyLogger() +# If you prefer `log` instead of `logger` +log = logger diff --git a/test_lodge.py b/test_lodge.py index fc34bd6..27d7038 100644 --- a/test_lodge.py +++ b/test_lodge.py @@ -124,3 +124,32 @@ def test_set_base_fields_on_logging(stream, monkeypatch): assert log_structured["message"] == "Logging other base fields" assert log_structured["anotherField"] == "yes" + + +def test_import_proxy_log(stream, monkeypatch): + monkeypatch.setenv("LOG_ENV", "DEV") + + class StubFrame: + f_globals = {"__name__": "package.module.submodule"} + + with import_lodge() as lodge: + monkeypatch.setattr(lodge, "currentframe", lambda x: StubFrame()) + lodge.log.info("logging with lodge.log") + + log_entry = stream.read() + log_entry_splitted = log_entry.split('|') + + assert log_entry_splitted[-1] == " logging with lodge.log\n" + assert log_entry_splitted[1] == " INFO " + assert log_entry_splitted[2] == " package.module.submodule " + + +def test_proxy_log_warn_with_level_error_should_not_log(stream, monkeypatch): + monkeypatch.setenv("LOG_LEVEL", "ERROR") + + from lodge import log + log.warn("warn") + + log_entry = stream.read() + + assert log_entry == ""