From 117de29d96e6cfa98ed39ca42dbe1eb3c909e71c Mon Sep 17 00:00:00 2001 From: Art Lukyanchyk <artiom.lukyanchyk@hs-hannover.de> Date: Thu, 14 Sep 2017 13:12:10 +0200 Subject: [PATCH] Make the colored logger best friends with celery :] --- colored_logger.py | 66 ++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/colored_logger.py b/colored_logger.py index 300fab3..32854e1 100644 --- a/colored_logger.py +++ b/colored_logger.py @@ -20,7 +20,8 @@ intro_words = ["magic", "MaGiC", "voodoo", "sorcery", "wizardry", "witchery", "f intro_adjectives = ["colourful", "evil", "fairy", "random"] intro_colors = [34, 36, 32, 33, 35] -decorate_info = { + +logger_methods_colors = { # method names and their desired highlight colors "debug": "1;90", "info": "1;32", @@ -32,7 +33,10 @@ decorate_info = { # Adjusting handlers: Python logging.StreamHandler targets stderr by default. # Django default logging configuration doesn't change it, too. -# Existing loggers first. +# New loggers (they will 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 if stream != sys.stderr else sys.stdout) +# Existing loggers patched_objects = set() for logger in logging.Logger.manager.loggerDict.values(): for handler in getattr(logger, "handlers", list()): @@ -40,45 +44,51 @@ for logger in logging.Logger.manager.loggerDict.values(): if handler.stream == sys.stderr: handler.stream = sys.stdout patched_objects.add(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_objects.add(sys.stdout.isatty) - sys.stdout.isatty = lambda: True # stdout is now a TTY no matter what -def _apply_color_to_msg(msg, color, extras): - return "".join([color_set.format(color), extras, str(msg), color_reset]) + +# the isatty() stuff below solves the problem with IDE streams not being recognized as TTY +# normally sys.stdout is sufficient, but some stubborn stuff like Celery explicitly checks sys.stderr even if it doesn't write there +for stream in [sys.stdout, sys.stderr]: + try: + isatty = stream.isatty() + except Exception: + isatty = False + if not isatty: + patched_objects.add(stream) + stream.isatty = lambda: True # stdout is now a TTY no matter what + + +def colorize_string(msg, color="1;32", extras=""): + return str().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 """ + """ + decorate a method, replacing its first argument (must be str!) with the same, but colorized + (it also knows how to decorate bound methods) + """ 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 + msg_arg_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) + args[msg_arg_index] = colorize_string(args[msg_arg_index], color, extras) return method(*args, **kwargs) return wrapper + +# decorate classes 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) - patched_objects.add(entity) - else: - pass # that should be a placeholder object (non-public API), ignoring it + for method_name, color_str in logger_methods_colors.items(): + if isinstance(entity, logging.PlaceHolder): + continue # ignore the PlaceHolder object, it's not functional and not a public API + new_method = decorate_for_tty(getattr(entity, method_name), color_str) + setattr(entity, method_name, new_method) + patched_objects.add(entity) -# useless foo +# fancy useless stuff intro_seq = zip(itertools.cycle(intro_colors), random.choice(intro_words)) intro_msg = "{adj} {word}{reset}".format( adj=random.choice(intro_adjectives).capitalize(), @@ -94,6 +104,8 @@ sys.stdout.write("{intro_msg}{c}: patched {n} object{s}{cr}\n".format( s="" if len(patched_objects) is 1 else "s", cr=color_reset, )) -logging.getLogger("pydevutils").debug("Logging patches: {}.".format(", ".join(str(e) for e in patched_objects) or "none")) + +# debug with the following line: +# sys.stdout.write("Patched objects: {}\n".format(", ".join(str(e) for e in patched_objects) or "none")) -- GitLab