# An opinionated approach to logging in Python import logging import sys def setup_logger(name: str | None = None, level: str | int = 'WARN', log_format: str | None = None) -> logging.Logger: """Sets up an opinionated logger bifurcating on log level to send error and warning logs to stderr, info and debug to stdout Args: name: name of the logger to get; default `None` returns root logger level: level attribute in logging as a string log_format: optional log format; should be a valid format string, see std lib logging docs for more info Returns: configured `logging.Logger` """ logger = logging.getLogger(name) if logger.handlers: return logger if isinstance(level, int): logger.setLevel(level) else: logger.setLevel(getattr(logging, level)) if log_format is None: log_format = "%(name)-10s %(levelname)-7s %(message)s" formatter = logging.Formatter(log_format) h1 = logging.StreamHandler(sys.stdout) h1.setLevel(logging.DEBUG) h1.setFormatter(formatter) h1.addFilter(lambda record: record.levelno <= logging.INFO) h2 = logging.StreamHandler() h2.setLevel(logging.WARNING) h2.setFormatter(formatter) logger.addHandler(h1) logger.addHandler(h2) return logger class LoggingMixin: """ Mixin class which adds logging functionality to a class. Not intended to be used as a subclass. To use, call `self._setup_logger; this creates an instance-internal logger at `self._logger`. When setting up a logger, this expects this app's primary logger, the `cli` logger, to be already set up, and uses its level as the internal logger's level. Then, call `self.log` as a convenience method or, alternatively, the internal logger directly. """ _logger: logging.Logger def _setup_logger(self, name: str, msg_prefix: str | None = None) -> None: """Sets up an internal logger object using a prescriptive convention Note that this tries to match the log level of the hydrate logger, because that's what is set up using CLI args. Changes to the hydrate logger will need to be set here """ self._prefix = msg_prefix self._logger = setup_logger(name, level=logging.getLogger('cli').level) def log(self, msg: str, lvl: str = 'error', **kwargs) -> None: """Convenience method for logging to the internal logger Args: msg: log message as a string lvl: log level as a string name, should be a method name off a `logging.Logger` object; i.e. `debug`, `info`, `warn` Returns: Result of logger method call; expected to be None """ meth = getattr(self._logger, lvl) meth(f"{self._prefix + ": " if self._prefix else ""}{msg}", **kwargs)