Source code for hbutils.logging.format
"""
Colored logging formatter utilities with ANSI styling and multi-line alignment.
This module provides utilities for enhancing Python logging output using ANSI
escape sequences. It includes a color palette class for terminal styling,
a formatter that applies color based on log severity, and a helper function
for aligning multi-line log messages with the log prefix.
The main public components are:
* :class:`ANSIColors` - ANSI escape sequences for colors and styles.
* :class:`ColoredFormatter` - Logging formatter with colorized output.
* :func:`format_multiline_message` - Indentation helper for multi-line messages.
.. note::
ANSI escape codes are intended for terminal output. Some environments
(such as Windows cmd without ANSI support or log files) may not render
colors correctly.
Example::
>>> import logging
>>> from hbutils.logging.format import ColoredFormatter
>>>
>>> logger = logging.getLogger("example")
>>> logger.setLevel(logging.DEBUG)
>>> handler = logging.StreamHandler()
>>> handler.setFormatter(ColoredFormatter())
>>> logger.addHandler(handler)
>>>
>>> logger.info("Single line message")
>>> logger.warning("Multi-line message:\\nLine 2\\nLine 3")
"""
import logging
import os
from typing import Dict, Tuple
__all__ = [
'ANSIColors',
'ColoredFormatter',
'format_multiline_message',
]
[docs]
class ANSIColors:
"""
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.
:cvar str RESET: Reset all text formatting to default.
:cvar str BOLD: Apply bold text style.
:cvar str UNDERLINE: Apply underline text style.
:cvar str BLACK: Apply black color to text.
:cvar str RED: Apply red color to text.
:cvar str GREEN: Apply green color to text.
:cvar str YELLOW: Apply yellow color to text.
:cvar str BLUE: Apply blue color to text.
:cvar str MAGENTA: Apply magenta color to text.
:cvar str CYAN: Apply cyan color to text.
:cvar str WHITE: Apply white color to text.
:cvar str BRIGHT_BLACK: Apply bright black (gray) color to text.
:cvar str BRIGHT_RED: Apply bright red color to text.
:cvar str BRIGHT_GREEN: Apply bright green color to text.
:cvar str BRIGHT_YELLOW: Apply bright yellow color to text.
:cvar str BRIGHT_BLUE: Apply bright blue color to text.
:cvar str BRIGHT_MAGENTA: Apply bright magenta color to text.
:cvar str BRIGHT_CYAN: Apply bright cyan color to text.
:cvar str BRIGHT_WHITE: 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 a compatible terminal
>>> print(f"{ANSIColors.BOLD}{ANSIColors.GREEN}Bold green text{ANSIColors.RESET}")
Bold green text # Displayed in bold green
"""
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 indentation for continuation lines.
The first line of the message is preserved, while each subsequent line
is prefixed with a number of spaces specified by ``indent_length``. This
function is commonly used to align multi-line log messages with a log
prefix.
:param message: The message to format, potentially containing multiple lines.
:type message: str
:param indent_length: Number of spaces to indent continuation lines.
:type indent_length: int
:return: The formatted message with indentation applied to continuation lines.
: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):
"""
Logging formatter with ANSI colors and multi-line alignment.
The formatter applies a color scheme based on log severity and ensures
multi-line messages are indented to align with the log message content.
Color scheme:
- DEBUG: Blue
- INFO: Green
- WARNING: Yellow
- ERROR: Red
- CRITICAL: Bold Red
:cvar dict COLORS: Mapping of log level names to ANSI color codes.
:ivar str datefmt: Date format string for timestamp rendering.
:ivar dict _indent_cache: Cached prefix lengths for indentation calculation.
Example::
>>> import logging
>>> logger = logging.getLogger("demo")
>>> handler = logging.StreamHandler()
>>> handler.setFormatter(ColoredFormatter())
>>> logger.addHandler(handler)
>>> logger.setLevel(logging.DEBUG)
>>> logger.warning("Warning message\\nwith multiple lines")
"""
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 formatter.
:param datefmt: Date format string for timestamps, defaults to
``'%Y-%m-%d %H:%M:%S'``.
:type datefmt: str
:param args: Additional positional arguments passed to
:class:`logging.Formatter`.
:param kwargs: Additional keyword arguments passed to
:class:`logging.Formatter`.
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: Dict[Tuple[str, str], int] = {}
def _calculate_indent_length(self, record: logging.LogRecord) -> int:
"""
Calculate prefix length for multi-line indentation.
The prefix includes the timestamp, level name, and logger name. This
length excludes ANSI escape codes and is cached based on the log
level and logger name for performance.
:param record: The log record containing log context.
:type record: logging.LogRecord
:return: Prefix length used to indent continuation lines.
:rtype: int
Example::
>>> # For level INFO and logger name 'myapp'
>>> # Prefix: "[2024-01-15 10:30:45] INFO myapp "
>>> # This method returns the length 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 with color and indentation.
Output structure:
``[timestamp] level_name logger_name message``
Multi-line messages are indented so that continuation lines align
with the first character of the message content.
:param record: The log record to format.
:type record: logging.LogRecord
:return: The formatted log message as a string.
: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)
>>> print(formatted)
[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 the message is already formatted
formatter = logging.Formatter(format_str, datefmt=self.datefmt)
return formatter.format(record_copy)