Skip to content
Snippets Groups Projects
colored_logger.py 3.81 KiB
Newer Older
  • Learn to ignore specific revisions
  • Art's avatar
    Art committed
    
    """
    
    This module decorates methods of Logger class.
    It's quite harmless, unless you use some weird non-tty stdout/stderr while DEBUG-ing.
    
    This hack should be auto-loaded by it's app.
    If you don't use the app, just import this file to apply the hack.
    
    """
    
    import sys
    import logging
    import itertools
    import random
    
    
    # Control sequences (just google VT52 to see the compatibility burden straight from 1975)
    color_set = "\x1b[{}m{}"
    color_reset = "\x1b[0m"
    
    # useless stuff displayed before applying patches... yes, I felt very bored
    intro_words = ["magic", "MaGiC", "voodoo", "sorcery", "wizardry", "witchery", "foobar", "rainbow"]
    intro_adjectives = ["colourful", "evil", "fairy", "random"]
    intro_colors = [34, 36, 32, 33, 35]
    
    decorate_info = {
        # method names and their desired highlight colors
        "debug": "1;90",
        "info": "1;32",
        "warning": "1;33",
        "error": "1;91",
        "critical": "1;95",
        "exception": "1;95",
    }
    
    if settings.DEBUG:  # probably PyCharm or some other IDE with own stream handling
        # useless foo
        intro_seq = zip(itertools.cycle(intro_colors), random.choice(intro_words))
        print(
            "{adj} {word}{reset}:".format(
                adj=random.choice(intro_adjectives).capitalize(),
                word=str().join(color_set.format(*symbol) for symbol in intro_seq),
                reset=color_reset,
            ),
            end=" ")
        # Patching handlers: Python logging.StreamHandler targets stderr by default,
        # therefore Django example config does, therefore our logging config also does,
        # so PyCharm believes every line of log is error. Well... Fixing it now.
        # Existing loggers first.
        patched = list()
        for logger in logging.Logger.manager.loggerDict.values():
            for handler in getattr(logger, "handlers", list()):
                if isinstance(handler, logging.StreamHandler):
                    if handler.stream == sys.stderr:
                        handler.stream = sys.stdout
                        patched.append(handler)
        # Future loggers (they should be initially created with the proper stream).
        original_constructor = logging.StreamHandler.__init__  # must grab a reference outside of the lambda
        logging.StreamHandler.__init__ = lambda zelf, stream=None: original_constructor(zelf, stream or sys.stdout)
        # the isatty() stuff below solved some problems in the past, I'm not sure anymore which exactly
        try:
            stdout_tty = sys.stdout.isatty()
        except Exception:
            stdout_tty = False
        if not stdout_tty:
            patched.append(sys.stdout.isatty)
            sys.stdout.isatty = lambda: True  # stdout is now a TTY no matter what
        # reporting
        print("applied {n} logging patches.".format(n=len(patched)))
        logging.getLogger().debug("Logging patches: {}.".format(", ".join(str(e) for e in patched) or "none"))
    
    
    def _apply_color_to_msg(msg, color, extras):
        return "".join([color_set.format(color, extras), str(msg), color_reset])
    
    
    def decorate_for_tty(method, color="1;39", extras=""):
        """ it also knows how to decorate bound methods, but it is not used anymore """
        method_is_bound = bool(getattr(method, "__self__", None))  # existing loggers cause bound methods
        msg_index = 0 if method_is_bound else 1  # message argument is located there
    
        def wrapper(*args, **kwargs):
            args = list(args)  # it's an immutable tuple when received
            args[msg_index] = _apply_color_to_msg(args[msg_index], color, extras)
            return method(*args, **kwargs)
    
        return wrapper
    
    for entity in [logging.Logger]:
        for method_name, color_str in decorate_info.items():
            if isinstance(entity, logging.Logger) or entity is logging.Logger:
                new_method = decorate_for_tty(getattr(entity, method_name), color_str)
                setattr(entity, method_name, new_method)
            else:
                pass  # that should be a placeholder object (non-public API), ignoring it