Source code for flox.common.logging_config

"""
This module contains logging configuration for flox.
"""
from __future__ import annotations

import logging
import logging.config
import logging.handlers
import os
import pathlib
import re
import sys

log = logging.getLogger(__name__)

DEFAULT_FORMAT = (
    "%(created)f %(asctime)s %(levelname)s %(processName)s-%(process)d "
    "%(threadName)s-%(thread)d %(name)s:%(lineno)d %(funcName)s "
    "%(message)s"
)

_ital = "\033[3m"
_redb = "\033[41m"
_teal = "\033[32m"
_yel = "\033[33m"
_byel = "\033[93m"
_yelb = "\033[43m"
_purp = "\033[35m"
_cyan = "\033[36m"
_gray = "\033[37m"
_grayonb = "\033[37;40m"
_r = "\033[m"
_C_BASE = (
    f"{_teal}%(created)f{_r} {_yel}%(asctime)s{_r} {_ital}%(levelname)s{_r}"
    " %(processName)s-%(process)d %(threadName)s-%(thread)d"
    f" %(name)s:{_cyan}%(lineno)d{_r} {_purp}%(funcName)s{_r}"
)
COLOR_ERROR = _redb
COLOR_WARNING = _yelb
COLOR_INFO = _gray
COLOR_DEBUG = _grayonb
C_ERROR_FMT = _C_BASE + f" {COLOR_ERROR}%(message)s{_r}"
C_WARNING_FMT = _C_BASE + f" {COLOR_WARNING}%(message)s{_r}"
C_INFO_FMT = _C_BASE + f" {COLOR_INFO}%(message)s{_r}"
C_DEBUG_FMT = _C_BASE + f" {COLOR_DEBUG}%(message)s{_r}"


[docs]class FloxConsoleFormatter(logging.Formatter): """ For internal use only. This formatter handles output to standard streams in the following way: if 'debug' is False (default): info messages and below are treated as "user output" and are minimally decorated warning messages and up are given a full format, so are debug messages if 'debug' is True: all messages are given the full format """ _u = "[0-9A-Fa-f]" # convenience _uuid_re = f"{_u}{{8}}-{_u}{{4}}-{_u}{{4}}-{_u}{{4}}-{_u}{{12}}" # match uuids for colorization that have not otherwise already been colorized uuid_re = re.compile(rf"(?<!\dm)({_uuid_re})") def __init__( self, debug: bool = False, no_color: bool = False, fmt: str = "", datefmt: str = "%Y-%m-%d %H:%M:%S", ) -> None: super().__init__() self.use_color = debug and not no_color and sys.stderr.isatty() if fmt: d_fmt, i_fmt, w_fmt, e_fmt = fmt, fmt, fmt, fmt else: d_fmt, i_fmt, w_fmt, e_fmt = ( C_DEBUG_FMT, C_INFO_FMT, C_WARNING_FMT, C_ERROR_FMT, ) if not self.use_color: ansi_re = re.compile("\033.*?m") d_fmt = ansi_re.sub("", d_fmt) i_fmt = ansi_re.sub("", i_fmt) w_fmt = ansi_re.sub("", w_fmt) e_fmt = ansi_re.sub("", e_fmt) if debug: self._error_formatter = logging.Formatter(fmt=e_fmt, datefmt=datefmt) self._warning_formatter = logging.Formatter(fmt=w_fmt, datefmt=datefmt) self._debug_formatter = logging.Formatter(fmt=d_fmt, datefmt=datefmt) self._info_formatter = logging.Formatter(fmt=i_fmt, datefmt=datefmt) else: self._info_formatter = logging.Formatter(fmt="> %(message)s") self._warning_formatter = logging.Formatter(fmt=w_fmt, datefmt=datefmt) self._error_formatter = self._warning_formatter self._debug_formatter = self._warning_formatter
[docs] def format(self, record: logging.LogRecord): if self.use_color: # Highlight all UUIDs if record.levelno > logging.WARNING: end_coloring = COLOR_ERROR elif record.levelno > logging.INFO: end_coloring = COLOR_WARNING elif record.levelno > logging.DEBUG: end_coloring = COLOR_INFO else: end_coloring = COLOR_DEBUG repl = f"{_byel}\\1{_r}{end_coloring}" try: record.msg = self.uuid_re.sub(repl, record.msg) if isinstance(record.args, dict): for k, v in record.args.items(): record.args[k] = self.uuid_re.sub(repl, str(v)) elif record.args: args = tuple(self.uuid_re.sub(repl, str(a)) for a in record.args) record.args = args except Exception as exc: # Basically, inform, but ignore print(f"Unable to colorize log message: {exc}") if record.levelno > logging.WARNING: return self._error_formatter.format(record) elif record.levelno > logging.INFO: return self._warning_formatter.format(record) elif record.levelno > logging.DEBUG: return self._info_formatter.format(record) return self._debug_formatter.format(record)
def _get_file_dict_config( logfile: str, console_enabled: bool, debug: bool, no_color: bool ) -> dict: # ensure that the logdir exists logdir = os.path.dirname(logfile) os.makedirs(logdir, exist_ok=True) log_handlers = ["logfile"] if console_enabled: log_handlers.append("console") return { "version": 1, "formatters": { "streamfmt": { "()": "flox.common.logging_config.FloxConsoleFormatter", "debug": debug, "no_color": no_color, }, "filefmt": { "format": DEFAULT_FORMAT, "datefmt": "%Y-%m-%d %H:%M:%S", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "streamfmt", }, "logfile": { "class": "logging.handlers.RotatingFileHandler", "level": "DEBUG", "filename": logfile, "formatter": "filefmt", "maxBytes": 100 * 1024 * 1024, "backupCount": 1, }, }, "loggers": { "": { "level": "DEBUG" if debug else "INFO", "handlers": log_handlers, }, }, } def _get_stream_dict_config(debug: bool, no_color: bool) -> dict: return { "version": 1, "formatters": { "streamfmt": { "()": "flox.common.logging_config.FloxConsoleFormatter", "debug": debug, "no_color": no_color, }, }, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG" if debug else "INFO", "formatter": "streamfmt", } }, "loggers": { "": { "level": "DEBUG", "handlers": ["console"], }, }, }
[docs]class FloxLogger(logging.Logger): TRACE = logging.DEBUG - 5 def trace(self, msg, *args, **kwargs): self.log(FloxLogger.TRACE, msg, args, **kwargs)
logging.setLoggerClass(FloxLogger) logger = logging.getLogger(__name__)
[docs]def setup_logging( *, logfile: pathlib.Path | str | None = None, console_enabled: bool = True, debug: bool = False, no_color: bool = False, ) -> None: if logfile is not None: config = _get_file_dict_config(str(logfile), console_enabled, debug, no_color) else: config = _get_stream_dict_config(debug, no_color) logging.config.dictConfig(config)