Source code for law.contrib.rich.logger

# coding: utf-8

"""
Logging optimization using rich.
"""

from __future__ import annotations

__all__ = ["replace_console_handlers"]

import logging

from law.config import Config
from law.logger import is_tty_handler, Logger
from law.util import make_list, multi_match
from law._types import Sequence, Callable


[docs] def replace_console_handlers( loggers: Sequence[str] = ("luigi", "luigi.*", "luigi-*", "law", "law.*"), level: str | int | None = None, force_add: bool = False, check_fn: Callable[[logging.Logger, logging.Handler], bool] | None = None, **kwargs, ) -> list[tuple[logging.Logger, list[logging.Handler]]]: """ Removes all tty stream handlers (i.e. those logging to *stdout* or *stderr*) from certain *loggers* and adds a new ``rich.logging.RichHandler`` instance with a specified *level* and all *kwargs* passed as additional options to its constructor. *loggers* can either be logger instances or names. In the latter case, the names are used as patterns to identify matching loggers. Unless *force_add* is *True*, no new handler is added when no tty stream handler was previously registered. *check_fn* can be a function with two arguments, a logger instance and a handler instance, that should return *True* if that handler should be removed. When *None*, all handlers inheriting from the basic ``logging.StreamHandler`` are removed if their *stream* attibute referes to a tty stream. When *level* is *None*, it defaults to the log level of the first removed handler. In case no default level can be determined, *INFO* is used. The removed handlers are returned in a list of 2-tuples (*logger*, *removed_handlers*). """ from rich import logging as rich_logging # type: ignore[import-untyped, import-not-found] # prepare the return value ret = [] # default check_fn if check_fn is None: check_fn = lambda logger, handler: is_tty_handler(handler) loggers = make_list(loggers) for name, logger in logging.root.manager.loggerDict.items(): if not isinstance(logger, logging.Logger): continue # check if the logger is selected for l in loggers: if logger == l or (isinstance(l, str) and multi_match(name, l)): break else: # when this point is reached, the logger was not selected continue removed_handlers = [] handlers = getattr(logger, "handlers", []) for handler in handlers: if check_fn(logger, handler): # get the level if level is None: level = getattr(handler, "level", None) # remove it logger.removeHandler(handler) removed_handlers.append(handler) # when at least one handler was found and removed, or force_add is True, add a rich handler if removed_handlers or force_add: # make sure the level is set if level is None: level = logging.INFO # add the rich handler logger.addHandler(rich_logging.RichHandler(level, **kwargs)) # emit warning for colored_* configs cfg = Config.instance() opts = [(s, o) for s in ["task", "target"] for o in ["colored_str", "colored_repr"]] if isinstance(logger, Logger) and any(cfg.get_expanded_bool(*opt) for opt in opts): logger.warning_once( "interfering_colors_in_rich_handler", "law is currently configured to colorize string representations of tasks and " "targets which might lead to malformed logs of the RichHandler; to avoid this, " f"consider updating your law configuration file ({cfg.config_file}) to:\n" "[task]\ncolored_repr: False\ncolored_str: False\n\n" "[target]\ncolored_repr: False\ncolored_str: False", ) # add the removed handlers to the returned list if removed_handlers: ret.append((logger, removed_handlers)) return ret