Skip to content

Instantly share code, notes, and snippets.

@santibreo
Created October 11, 2023 09:06
Show Gist options
  • Save santibreo/15560575114964c876cecb2523b19907 to your computer and use it in GitHub Desktop.
Save santibreo/15560575114964c876cecb2523b19907 to your computer and use it in GitHub Desktop.
Python colored logging
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