Source code for hbutils.logging.format
"""
Overview:
This module provides functionality to enhance logging output with colors for different log levels.
It includes an `ANSIColors` class defining ANSI escape sequences for various colors and styles,
and a `ColoredFormatter` class to format log messages with these colors based on their severity level.
The module supports multi-line log messages with proper indentation alignment, making logs more
readable and visually organized.
Example::
>>> import logging
>>> from hbutils.logging import ColoredFormatter
>>>
>>> logger = logging.getLogger()
>>> logger.setLevel(logging.DEBUG)
>>> console_handler = logging.StreamHandler()
>>> console_handler.setFormatter(ColoredFormatter())
>>> logger.addHandler(console_handler)
>>>
>>> # Test single line message
>>> logger.info("This is a single line message")
>>> # Test multi-line message
>>> logger.warning("This is a multi-line message:\\nLine 2 content\\nLine 3 content\\nLine 4 content")
>>> logger.error(
... "Error details:\\n - Error code: 500\\n - Error message: Internal server error\\n - Stack trace follows...")
"""
import logging
import os
__all__ = [
'ANSIColors',
'ColoredFormatter',
'format_multiline_message',
]
[docs]
class ANSIColors:
"""
A collection of ANSI escape sequences for terminal text coloring and styling.
These constants can be used to format strings with various colors and text styles
in terminal environments that support ANSI escape codes.
Attributes:
* RESET (str): Reset all text formatting to default.
* BOLD (str): Apply bold text style.
* UNDERLINE (str): Apply underline text style.
* BLACK (str): Apply black color to text.
* RED (str): Apply red color to text.
* GREEN (str): Apply green color to text.
* YELLOW (str): Apply yellow color to text.
* BLUE (str): Apply blue color to text.
* MAGENTA (str): Apply magenta color to text.
* CYAN (str): Apply cyan color to text.
* WHITE (str): Apply white color to text.
* BRIGHT_BLACK (str): Apply bright black (gray) color to text.
* BRIGHT_RED (str): Apply bright red color to text.
* BRIGHT_GREEN (str): Apply bright green color to text.
* BRIGHT_YELLOW (str): Apply bright yellow color to text.
* BRIGHT_BLUE (str): Apply bright blue color to text.
* BRIGHT_MAGENTA (str): Apply bright magenta color to text.
* BRIGHT_CYAN (str): Apply bright cyan color to text.
* BRIGHT_WHITE (str): Apply bright white color to text.
Example::
>>> print(f"{ANSIColors.RED}This is red text{ANSIColors.RESET}")
This is red text # (displayed in red in terminal)
>>> print(f"{ANSIColors.BOLD}{ANSIColors.GREEN}Bold green text{ANSIColors.RESET}")
Bold green text # (displayed in bold green in terminal)
"""
RESET = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BRIGHT_BLACK = "\033[90m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
[docs]
def format_multiline_message(message: str, indent_length: int) -> str:
"""
Format a multi-line message with proper indentation for continuation lines.
The first line of the message remains unchanged, while all subsequent lines
are indented by the specified number of spaces to align with the message content.
:param message: The message to format, potentially containing multiple lines.
:type message: str
:param indent_length: The number of spaces to indent continuation lines.
:type indent_length: int
:return: The formatted message with proper indentation for multi-line content.
:rtype: str
Example::
>>> msg = "First line\\nSecond line\\nThird line"
>>> result = format_multiline_message(msg, 4)
>>> print(result)
First line
Second line
Third line
"""
lines = message.splitlines(keepends=False)
if len(lines) == 0:
return ''
# First line remains unchanged
formatted_lines = [lines[0]]
# Subsequent lines get indented
indent = ' ' * indent_length
for line in lines[1:]:
formatted_lines.append(indent + line)
return os.linesep.join(formatted_lines)
[docs]
class ColoredFormatter(logging.Formatter):
"""
A logging formatter that applies colors to log messages based on their severity level.
This formatter enhances log readability by applying different colors to different
log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) and formatting components
(timestamp, level name, logger name, message). It also supports multi-line messages
with proper indentation alignment.
The color scheme is:
- DEBUG: Blue
- INFO: Green
- WARNING: Yellow
- ERROR: Red
- CRITICAL: Bold Red
Attributes:
COLORS (dict): Mapping of log level names to their corresponding ANSI color codes.
datefmt (str): The date format string for timestamps.
Example::
>>> import logging
>>> logger = logging.getLogger(__name__)
>>> handler = logging.StreamHandler()
>>> handler.setFormatter(ColoredFormatter())
>>> logger.addHandler(handler)
>>> logger.setLevel(logging.DEBUG)
>>>
>>> logger.debug("Debug message")
>>> logger.info("Info message")
>>> logger.warning("Warning message\\nwith multiple lines")
>>> logger.error("Error message")
>>> logger.critical("Critical message")
"""
COLORS = {
'DEBUG': ANSIColors.BLUE,
'INFO': ANSIColors.GREEN,
'WARNING': ANSIColors.YELLOW,
'ERROR': ANSIColors.RED,
'CRITICAL': ANSIColors.BOLD + ANSIColors.RED,
}
[docs]
def __init__(self, datefmt: str = '%Y-%m-%d %H:%M:%S', *args, **kwargs):
"""
Initialize the ColoredFormatter with custom date format and optional parameters.
:param datefmt: The date format string for formatting timestamps in log messages.
Defaults to '%Y-%m-%d %H:%M:%S'.
:type datefmt: str
:param args: Additional positional arguments passed to the parent Formatter class.
:param kwargs: Additional keyword arguments passed to the parent Formatter class.
Example::
>>> formatter = ColoredFormatter(datefmt='%H:%M:%S')
>>> formatter = ColoredFormatter(datefmt='%Y/%m/%d %H:%M:%S')
"""
super().__init__(*args, **kwargs)
self.datefmt = datefmt
self._indent_cache = {}
def _calculate_indent_length(self, record: logging.LogRecord) -> int:
"""
Calculate the length of the log prefix for proper multi-line message indentation.
The prefix includes the timestamp, level name, and logger name. This method
calculates the total character length (excluding ANSI color codes) to determine
how much to indent continuation lines in multi-line messages.
Results are cached based on (level name, logger name) to improve performance.
:param record: The log record containing information about the log event.
:type record: logging.LogRecord
:return: The length of the prefix without ANSI color codes, used for indentation.
:rtype: int
Example::
>>> # For a log record with level INFO and logger name 'myapp'
>>> # Prefix might be: "[2024-01-15 10:30:45] INFO myapp "
>>> # This method would return the character count of that prefix
"""
# Create a cache key based on level name and logger name
cache_key = (record.levelname, record.name)
if cache_key not in self._indent_cache:
# Format timestamp part
timestamp = self.formatTime(record, datefmt=self.datefmt)
timestamp_part = f"[{timestamp}]"
# Format level part (8 characters wide)
level_part = f"{record.levelname:<8}"
# Format logger name part
name_part = f"{record.name}"
# Calculate total length: [timestamp] + space + level + space + name + space
indent_length = len(timestamp_part) + 1 + len(level_part) + 1 + len(name_part) + 1
self._indent_cache[cache_key] = indent_length
return self._indent_cache[cache_key]
[docs]
def format(self, record: logging.LogRecord) -> str:
"""
Format the specified log record as colored text with proper multi-line indentation.
This method applies appropriate colors based on the log level and formats the
message with the following structure:
[timestamp] level_name logger_name message
Multi-line messages are automatically indented so that continuation lines align
with the start of the message content.
:param record: The log record to be formatted.
:type record: logging.LogRecord
:return: The formatted log message with appropriate colors and proper multi-line indentation.
:rtype: str
Example::
>>> import logging
>>> record = logging.LogRecord(
... name='test', level=logging.INFO, pathname='', lineno=0,
... msg='Multi-line\\nmessage', args=(), exc_info=None
... )
>>> formatter = ColoredFormatter()
>>> formatted = formatter.format(record)
>>> # Output will be colored and properly indented:
>>> # [2024-01-15 10:30:45] INFO test Multi-line
>>> # message
"""
log_color = self.COLORS.get(record.levelname, ANSIColors.RESET)
# Build the format string
format_str = f"{ANSIColors.BRIGHT_BLACK}[%(asctime)s]{ANSIColors.RESET} "
format_str += f"{log_color}%(levelname)-8s{ANSIColors.RESET} "
format_str += f"{ANSIColors.CYAN}%(name)s{ANSIColors.RESET} "
format_str += f"%(message)s"
# Calculate indent length for multi-line messages
indent_length = self._calculate_indent_length(record)
# Format multi-line message with proper indentation
original_message = record.getMessage()
formatted_message = format_multiline_message(original_message, indent_length)
# Create a new record with the formatted message
record_copy = logging.makeLogRecord(record.__dict__)
record_copy.msg = formatted_message
record_copy.args = None # Clear args since we've already formatted the message
formatter = logging.Formatter(format_str, datefmt=self.datefmt)
return formatter.format(record_copy)