From 3fcda2a686987a4fe3e325e30e6f7126a5476911 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Mon, 19 Aug 2024 00:09:52 +0330 Subject: [PATCH 1/2] :sparkles::zap::bulb::art: feat: add colorized logs at console Added ANSI Color Constants: Created AnsiColors class in constants/ansi_colors to define a comprehensive set of ANSI color codes for log formatting. Mapped log levels to appropriate colors using the LOG_LEVEL_COLORS dictionary. Colorization Utility: Implemented colorize_log_format in utils/colorizer, which applies color codes to log format placeholders based on log levels. Integrated colorization logic for various log format specifiers, including timestamps, log levels, filenames, and more. Colorized Formatter: Added ColorizedFormatter class in formatters/colorized_formatter, extending logging.Formatter. Applied colorization to log records dynamically while preserving the original log format. Ensured that ANSI escape sequences are removed from formats if colorization is not required. Configuration Updates: Updated the logging configuration in the LogConfig class to apply the ColorizedFormatter to the console handler when colorize_console is enabled. Implemented logic to remove ANSI escape sequences from log file formats, ensuring clean logs without color codes in files. Extended the resolve_format method to optionally enable or disable colorization based on configuration. Closes #14 --- django_logging/apps.py | 2 + django_logging/constants/ansi_colors.py | 38 +++++++++++++++++++ django_logging/formatters/__init__.py | 1 + .../formatters/colorized_formatter.py | 20 ++++++++++ django_logging/settings/conf.py | 31 +++++++++++++-- django_logging/utils/colorizer.py | 27 +++++++++++++ django_logging/utils/setup_conf.py | 2 + 7 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 django_logging/constants/ansi_colors.py create mode 100644 django_logging/formatters/__init__.py create mode 100644 django_logging/formatters/colorized_formatter.py create mode 100644 django_logging/utils/colorizer.py diff --git a/django_logging/apps.py b/django_logging/apps.py index f3a3b14..2085c00 100644 --- a/django_logging/apps.py +++ b/django_logging/apps.py @@ -21,6 +21,7 @@ def ready(self): log_file_formats = log_settings.get("LOG_FILE_FORMATS", {}) console_level = log_settings.get("LOG_CONSOLE_LEVEL", "DEBUG") console_format = log_settings.get("LOG_CONSOLE_FORMAT") + colorize_console = log_settings.get("LOG_CONSOLE_COLORIZE", True) log_date_format = log_settings.get("LOG_DATE_FORMAT", "%Y-%m-%d %H:%M:%S") log_email_notifier = log_settings.get("LOG_EMAIL_NOTIFIER", {}) log_email_notifier_enable = log_email_notifier.get("ENABLE", False) @@ -37,6 +38,7 @@ def ready(self): log_file_formats, console_level, console_format, + colorize_console, log_date_format, log_email_notifier_enable, log_email_notifier_log_levels, diff --git a/django_logging/constants/ansi_colors.py b/django_logging/constants/ansi_colors.py new file mode 100644 index 0000000..8ec502d --- /dev/null +++ b/django_logging/constants/ansi_colors.py @@ -0,0 +1,38 @@ +class AnsiColors: + BLACK = "\033[0;30m" + RED = "\033[0;31m" + RED_BACKGROUND = "\033[1;41m" + GREEN = "\033[0;32m" + YELLOW = "\033[0;33m" + BLUE = "\033[0;34m" + MAGENTA = "\033[0;35m" + CYAN = "\033[0;36m" + GRAY = "\033[0;37m" + WHITE = "\033[0;38m" + RESET = "\033[0m" + BRIGHT_BLACK = "\033[0;90m" + BRIGHT_RED = "\033[0;91m" + BRIGHT_GREEN = "\033[0;92m" + BRIGHT_YELLOW = "\033[0;93m" + BRIGHT_BLUE = "\033[0;94m" + BRIGHT_MAGENTA = "\033[0;95m" + BRIGHT_CYAN = "\033[0;96m" + BRIGHT_WHITE = "\033[0;97m" + BLACK_BACKGROUND = "\033[40m" + RED_BACKGROUND = "\033[41m" + GREEN_BACKGROUND = "\033[42m" + YELLOW_BACKGROUND = "\033[43m" + BLUE_BACKGROUND = "\033[44m" + MAGENTA_BACKGROUND = "\033[45m" + CYAN_BACKGROUND = "\033[46m" + WHITE_BACKGROUND = "\033[47m" + + +# Mapping log levels to ANSI colors +LOG_LEVEL_COLORS = { + "DEBUG": AnsiColors.BLUE, + "INFO": AnsiColors.GREEN, + "WARNING": AnsiColors.YELLOW, + "ERROR": AnsiColors.RED, + "CRITICAL": AnsiColors.RED_BACKGROUND, +} diff --git a/django_logging/formatters/__init__.py b/django_logging/formatters/__init__.py new file mode 100644 index 0000000..04cdd33 --- /dev/null +++ b/django_logging/formatters/__init__.py @@ -0,0 +1 @@ +from .colorized_formatter import ColorizedFormatter diff --git a/django_logging/formatters/colorized_formatter.py b/django_logging/formatters/colorized_formatter.py new file mode 100644 index 0000000..9ab7390 --- /dev/null +++ b/django_logging/formatters/colorized_formatter.py @@ -0,0 +1,20 @@ +import logging +from django_logging.settings.conf import LogConfig +from django_logging.utils.colorizer import colorize_log_format + + +class ColorizedFormatter(logging.Formatter): + def format(self, record): + original_format = self._style._fmt + + # checks that the format does not have any color it's self + if LogConfig.remove_ansi_escape_sequences(original_format) == original_format: + colorized_format = colorize_log_format(original_format, record.levelname) + self._style._fmt = colorized_format + + formatted_output = super().format(record) + + # Reset to the original format string + self._style._fmt = original_format + + return formatted_output diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 85a1d9c..811f7b0 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -23,6 +23,7 @@ def __init__( log_file_formats: Dict[str, Union[int, str]], console_level: str, console_format: Optional[Union[int, str]], + colorize_console: bool, log_date_format: str, log_email_notifier_enable: bool, log_email_notifier_log_levels: List[str], @@ -34,14 +35,17 @@ def __init__( self.log_file_formats = self._resolve_file_formats(log_file_formats) self.log_date_format = log_date_format self.console_level = console_level - self.console_format = self.resolve_format(console_format) + self.colorize_console = colorize_console + self.console_format = self.resolve_format( + console_format, use_colors=self.colorize_console + ) self.email_notifier_enable = log_email_notifier_enable self.email_notifier_log_levels = log_email_notifier_log_levels self.email_notifier_log_format = self.resolve_format( log_email_notifier_log_format ) - def _resolve_file_formats(self, log_file_formats: Dict[str, Union[int, str]]) -> Dict: + def _resolve_file_formats(self, log_file_formats): resolved_formats = {} for level in self.log_levels: format_option = log_file_formats.get(level, None) @@ -55,10 +59,23 @@ def _resolve_file_formats(self, log_file_formats: Dict[str, Union[int, str]]) -> else: resolved_formats[level] = FORMAT_OPTIONS[1] + colored_format = resolved_formats[level] + resolved_formats[level] = self.remove_ansi_escape_sequences(colored_format) + return resolved_formats @staticmethod - def resolve_format(_format: Union[int, str]) -> str: + def remove_ansi_escape_sequences(log_message: str) -> str: + """ + Remove ANSI escape sequences from log messages. + """ + import re + + ansi_escape = re.compile(r"(?:\x1B[@-_][0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", log_message) + + @staticmethod + def resolve_format(_format: Union[int, str], use_colors: bool = False): if _format: if isinstance(_format, int): resolved_format = FORMAT_OPTIONS.get(_format, FORMAT_OPTIONS[1]) @@ -67,6 +84,10 @@ def resolve_format(_format: Union[int, str]) -> str: else: resolved_format = FORMAT_OPTIONS[1] + # If colors are not enabled, strip out color codes, if provided in formats + if not use_colors: + resolved_format = LogConfig.remove_ansi_escape_sequences(resolved_format) + return resolved_format @@ -156,6 +177,10 @@ def set_conf(self) -> None: "format": self.log_config.console_format, "datefmt": self.log_config.log_date_format, } + if self.log_config.colorize_console: + formatters["console"].update( + {"()": "django_logging.formatters.ColorizedFormatter"} + ) formatters["email"] = { "format": self.log_config.email_notifier_log_format, diff --git a/django_logging/utils/colorizer.py b/django_logging/utils/colorizer.py new file mode 100644 index 0000000..2acc27f --- /dev/null +++ b/django_logging/utils/colorizer.py @@ -0,0 +1,27 @@ +from django_logging.constants.ansi_colors import LOG_LEVEL_COLORS, AnsiColors + + +def colorize_log_format(log_format, levelname): + color_mapping = { + "%(asctime)s": f"{AnsiColors.CYAN}%(asctime)s{AnsiColors.RESET}", + "%(created)f": f"{AnsiColors.BRIGHT_BLUE}%(created)f{AnsiColors.RESET}", + "%(relativeCreated)d": f"{AnsiColors.MAGENTA}%(relativeCreated)d{AnsiColors.RESET}", + "%(msecs)d": f"{AnsiColors.YELLOW}%(msecs)d{AnsiColors.RESET}", + "%(levelname)s": f"{LOG_LEVEL_COLORS.get(levelname, '')}%(levelname)s{AnsiColors.RESET}", + "%(levelno)d": f"{AnsiColors.RED}%(levelno)d{AnsiColors.RESET}", + "%(name)s": f"{AnsiColors.BRIGHT_MAGENTA}%(name)s{AnsiColors.RESET}", + "%(module)s": f"{AnsiColors.BRIGHT_GREEN}%(module)s{AnsiColors.RESET}", + "%(filename)s": f"{AnsiColors.YELLOW}%(filename)s{AnsiColors.RESET}", + "%(pathname)s": f"{AnsiColors.CYAN}%(pathname)s{AnsiColors.RESET}", + "%(lineno)d": f"{AnsiColors.RED}%(lineno)d{AnsiColors.RESET}", + "%(funcName)s": f"{AnsiColors.BRIGHT_BLUE}%(funcName)s{AnsiColors.RESET}", + "%(process)d": f"{AnsiColors.MAGENTA}%(process)d{AnsiColors.RESET}", + "%(thread)d": f"{AnsiColors.CYAN}%(thread)d{AnsiColors.RESET}", + "%(threadName)s": f"{AnsiColors.BRIGHT_MAGENTA}%(threadName)s{AnsiColors.RESET}", + "%(message)s": f"{AnsiColors.GRAY}%(message)s{AnsiColors.RESET}", + } + + for placeholder, colorized in color_mapping.items(): + log_format = log_format.replace(placeholder, colorized) + + return log_format diff --git a/django_logging/utils/setup_conf.py b/django_logging/utils/setup_conf.py index d81a0be..d336f1c 100644 --- a/django_logging/utils/setup_conf.py +++ b/django_logging/utils/setup_conf.py @@ -9,6 +9,7 @@ def set_logging( log_file_formats: Dict[str, Union[int, str]], console_level: str, console_format: Optional[Union[int, str]], + colorize_console: bool, log_date_format: str, log_email_notifier_enable: bool, log_email_notifier_log_levels: List[str], @@ -27,6 +28,7 @@ def set_logging( log_file_formats, console_level, console_format, + colorize_console, log_date_format, log_email_notifier_enable, log_email_notifier_log_levels, From a48af824656bcc778737fe6c4dce6452d5edf33c Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Mon, 19 Aug 2024 00:18:18 +0330 Subject: [PATCH 2/2] :zap: Update(settings) resolve_format method type annotations --- django_logging/settings/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 811f7b0..e741c05 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -45,7 +45,7 @@ def __init__( log_email_notifier_log_format ) - def _resolve_file_formats(self, log_file_formats): + def _resolve_file_formats(self, log_file_formats: Dict[str, Union[int, str]]) -> Dict: resolved_formats = {} for level in self.log_levels: format_option = log_file_formats.get(level, None) @@ -75,7 +75,7 @@ def remove_ansi_escape_sequences(log_message: str) -> str: return ansi_escape.sub("", log_message) @staticmethod - def resolve_format(_format: Union[int, str], use_colors: bool = False): + def resolve_format(_format: Union[int, str], use_colors: bool = False) -> str: if _format: if isinstance(_format, int): resolved_format = FORMAT_OPTIONS.get(_format, FORMAT_OPTIONS[1])