Created
October 11, 2023 09:06
-
-
Save santibreo/15560575114964c876cecb2523b19907 to your computer and use it in GitHub Desktop.
Python colored logging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
import logging | |
import sys | |
from typing import Callable | |
class TextEffect: | |
"""Represents a command that can be incorporated to log messages to apply | |
a text effect (bold, color, italics, ...) to following text""" | |
name_pattern = r'[A-Za-z0-9_/-]+' | |
def __init__(self, name: str, value: str) -> None: | |
self.name = self.name_format(name) | |
self.value = value | |
def __add__(self, other): | |
if isinstance(other, type(self)): | |
return self.value + other.value | |
if isinstance(other, str): | |
return self.value + other | |
raise TypeError(f"Cannot sum {type(other)} with {type(self)}") | |
def as_tuple(self) -> tuple[str, 'TextEffect']: | |
return self.name_unformat(self.name), self | |
@staticmethod | |
def name_format(name: str) -> str: | |
"""Validate names and returns command string representation""" | |
if not re.fullmatch(TextEffect.name_pattern, name): | |
raise ValueError(f'Name {name!r} cannot be used as command') | |
return f"<{name}>".casefold() | |
@staticmethod | |
def name_unformat(name: str) -> str: | |
"""Returns original given name, without command specification""" | |
match = re.fullmatch(r'<(' + TextEffect.name_pattern + ')>', name) | |
if not match: | |
raise ValueError(f'Name {name!r} is not a command') | |
return match.group(1) | |
class CliLogFormatter(logging.Formatter): | |
def __init__(self, use_text_effects: bool = True): | |
log_format = "%(asctime)s | %(name)s | %(levelname)s > %(message)s" | |
super().__init__(log_format, datefmt="%Y-%m-%d %H:%M:%S") | |
get_value: Callable[[str], str] = lambda val: val if use_text_effects else "" | |
self._text_effects: dict[str, TextEffect] = dict([ | |
TextEffect('black', get_value("\033[0;30m")).as_tuple(), | |
TextEffect('red', get_value("\033[0;31m")).as_tuple(), | |
TextEffect('green', get_value("\033[0;32m")).as_tuple(), | |
TextEffect('yellow', get_value("\033[0;33m")).as_tuple(), | |
TextEffect('blue', get_value("\033[0;34m")).as_tuple(), | |
TextEffect('magenta', get_value("\033[0;35m")).as_tuple(), | |
TextEffect('cyan', get_value("\033[0;36m")).as_tuple(), | |
TextEffect('white', get_value("\033[0;37m")).as_tuple(), | |
TextEffect('bold', get_value("\033[1m" )).as_tuple(), | |
TextEffect('/', get_value("\033[0m" )).as_tuple(), | |
]) | |
"""Mapping between text effects ids and their values""" | |
self.level_effect_mapping: dict[str, str] = { | |
'DEBUG': self['blue'].value, | |
'INFO': self['white'].value, | |
'WARNING': self['yellow'].value, | |
'ERROR': self['red'].value, | |
'CRITICAL': self['red'] + self['bold'], | |
} | |
"""Mapping between logging level name and text color""" | |
def __getitem__(self, key: str) -> TextEffect: | |
return self._text_effects[key] | |
@property | |
def text_effects(self) -> list[str]: | |
"""List of text effects that can be used in log messages""" | |
return [text_effect.name for text_effect in self._text_effects.values()] | |
def format_message(self, message: str) -> str: | |
message_fmt: str = message | |
for text_effect_name in self.text_effects: | |
text_effect: TextEffect = self[TextEffect.name_unformat(text_effect_name)] | |
message_fmt = message_fmt.replace(text_effect.name, text_effect.value) | |
return message_fmt | |
def format(self, record: logging.LogRecord) -> str: | |
level_color = self.level_effect_mapping.get(record.levelname) | |
if level_color: | |
record.levelname = level_color + record.levelname + self['/'].value | |
record.msg = self.format_message(record.getMessage()) | |
return super().format(record) | |
# Configure logger | |
logger = logging.getLogger("your-cli") | |
stdout_handler: logging.StreamHandler = logging.StreamHandler(sys.stdout) | |
stdout_handler.setFormatter(EwxCliLogFormatter()) | |
logger.addHandler(stdout_handler) | |
logger.setLevel(logging.INFO) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment