import logging from functools import wraps from threading import Lock class Logger: _instances = {} _lock = Lock() def __new__(cls, name="root"): with cls._lock: if name not in cls._instances: instance = super().__new__(cls) instance._init(name) cls._instances[name] = instance return cls._instances[name] def _init(self, name): self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) self.logger.propagate = False console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) file_handler = logging.FileHandler("app.log") file_handler.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s [%(threadName)s] %(levelname)-5s %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", # no milliseconds ) console_handler.setFormatter(formatter) file_handler.setFormatter(formatter) self.logger.addHandler(console_handler) self.logger.addHandler(file_handler) def _log(self, level, message): log_message = f"{message}".strip() self.logger.log(level, log_message) def debug(self, message, **kwargs): self._log(logging.DEBUG, message, **kwargs) def info(self, message, **kwargs): self._log(logging.INFO, message, **kwargs) def warning(self, message, **kwargs): self._log(logging.WARNING, message, **kwargs) def error(self, message, **kwargs): self._log(logging.ERROR, message, **kwargs) def critical(self, message, **kwargs): self._log(logging.CRITICAL, message, **kwargs) # decorator for logging function calls def log_fn(name=None): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): log = Logger(name or fn.__name__) log.info("begin") result = fn(*args, **kwargs) log.info("end") return result return wrapper return decorator