API Reference

CustomLogger

Bases: Logger

Custom logger that supports a TRACE level and sets up logging with both console and file handlers.

Source code in src/decima/logger.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class CustomLogger(logging.Logger):
    """Custom logger that supports a TRACE level and sets up logging with both console and file handlers."""

    TRACE: ClassVar[int] = 5
    logging.addLevelName(TRACE, "TRACE")

    def trace(self, msg: str, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
        """Log a message with severity 'TRACE'."""
        if self.isEnabledFor(self.TRACE):
            self._log(self.TRACE, msg, args, **kwargs)

    @staticmethod
    def setup_logging(folder: str, filename: str, level: str, class_length: int) -> None:
        """Set up logging with a handler that uses the custom LogFormatter and a file handler that writes logs in JSON format using the JsonFormatter.

        Args:
            folder (str): The directory where log files will be stored. If the directory does not exist, it will be created.
            filename (str): The base name for the log files. The actual log file will include a timestamp to ensure uniqueness.
            level (str): The logging level (e.g., "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") that determines the minimum severity of messages to be logged.
            class_length (int): The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

        Description:
            - This method configures the logging system to use a custom logger class (CustomLogger) and sets up two handlers: a console handler that formats logs with colors based
            on log levels, and
        """
        logging.setLoggerClass(CustomLogger)

        root: logging.Logger = logging.getLogger()
        for handler in root.handlers[:]:
            root.removeHandler(handler)

        log_dir = Path(folder)
        log_dir.mkdir(parents=True, exist_ok=True)
        date_time: str = datetime.now(tz=datetime.now().astimezone().tzinfo).strftime("%Y%m%d-%H%M%S")
        file_handler = logging.FileHandler(log_dir / f"{filename}-{date_time}.log", mode="w")
        file_handler.setFormatter(logging.Formatter(LogFormatter.fmt_str))

        console_handler: logging.StreamHandler[TextIO] = logging.StreamHandler()
        console_handler.setFormatter(LogFormatter(class_length))

        log_path: Path = Path(folder) / f"{filename}.jsonl"
        file_h = logging.FileHandler(log_path, mode="a")
        file_h.setFormatter(JsonFormatter())

        logging.basicConfig(level=level, handlers=[console_handler, file_handler, file_h])

setup_logging(folder, filename, level, class_length) staticmethod

Set up logging with a handler that uses the custom LogFormatter and a file handler that writes logs in JSON format using the JsonFormatter.

Parameters:
  • folder (str) –

    The directory where log files will be stored. If the directory does not exist, it will be created.

  • filename (str) –

    The base name for the log files. The actual log file will include a timestamp to ensure uniqueness.

  • level (str) –

    The logging level (e.g., "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") that determines the minimum severity of messages to be logged.

  • class_length (int) –

    The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

Description
  • This method configures the logging system to use a custom logger class (CustomLogger) and sets up two handlers: a console handler that formats logs with colors based on log levels, and
Source code in src/decima/logger.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@staticmethod
def setup_logging(folder: str, filename: str, level: str, class_length: int) -> None:
    """Set up logging with a handler that uses the custom LogFormatter and a file handler that writes logs in JSON format using the JsonFormatter.

    Args:
        folder (str): The directory where log files will be stored. If the directory does not exist, it will be created.
        filename (str): The base name for the log files. The actual log file will include a timestamp to ensure uniqueness.
        level (str): The logging level (e.g., "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") that determines the minimum severity of messages to be logged.
        class_length (int): The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

    Description:
        - This method configures the logging system to use a custom logger class (CustomLogger) and sets up two handlers: a console handler that formats logs with colors based
        on log levels, and
    """
    logging.setLoggerClass(CustomLogger)

    root: logging.Logger = logging.getLogger()
    for handler in root.handlers[:]:
        root.removeHandler(handler)

    log_dir = Path(folder)
    log_dir.mkdir(parents=True, exist_ok=True)
    date_time: str = datetime.now(tz=datetime.now().astimezone().tzinfo).strftime("%Y%m%d-%H%M%S")
    file_handler = logging.FileHandler(log_dir / f"{filename}-{date_time}.log", mode="w")
    file_handler.setFormatter(logging.Formatter(LogFormatter.fmt_str))

    console_handler: logging.StreamHandler[TextIO] = logging.StreamHandler()
    console_handler.setFormatter(LogFormatter(class_length))

    log_path: Path = Path(folder) / f"{filename}.jsonl"
    file_h = logging.FileHandler(log_path, mode="a")
    file_h.setFormatter(JsonFormatter())

    logging.basicConfig(level=level, handlers=[console_handler, file_handler, file_h])

trace(msg, *args, **kwargs)

Log a message with severity 'TRACE'.

Source code in src/decima/logger.py
82
83
84
85
def trace(self, msg: str, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
    """Log a message with severity 'TRACE'."""
    if self.isEnabledFor(self.TRACE):
        self._log(self.TRACE, msg, args, **kwargs)

JsonFormatter

Bases: Formatter

Log formatter that outputs logs in JSON format, including timestamp, level, name, message, and any extra data.

Source code in src/decima/logger.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class JsonFormatter(logging.Formatter):
    """Log formatter that outputs logs in JSON format, including timestamp, level, name, message, and any extra data."""

    def format(self, record: logging.LogRecord) -> str:
        """Format the log record as a JSON string, including timestamp, level, name, message, and any extra data if provided."""
        log_record: dict[str, Any] = {
            "timestamp": datetime.fromtimestamp(record.created, tz=datetime.now().astimezone().tzinfo).isoformat(),
            "level": record.levelname,
            "name": record.name,
            "message": record.getMessage(),
        }
        # If the user passed extra={} data, include it
        if hasattr(record, "extra_data"):
            log_record["extra"] = getattr(record, "extra_data", None)

        return json.dumps(log_record)

format(record)

Format the log record as a JSON string, including timestamp, level, name, message, and any extra data if provided.

Source code in src/decima/logger.py
61
62
63
64
65
66
67
68
69
70
71
72
73
def format(self, record: logging.LogRecord) -> str:
    """Format the log record as a JSON string, including timestamp, level, name, message, and any extra data if provided."""
    log_record: dict[str, Any] = {
        "timestamp": datetime.fromtimestamp(record.created, tz=datetime.now().astimezone().tzinfo).isoformat(),
        "level": record.levelname,
        "name": record.name,
        "message": record.getMessage(),
    }
    # If the user passed extra={} data, include it
    if hasattr(record, "extra_data"):
        log_record["extra"] = getattr(record, "extra_data", None)

    return json.dumps(log_record)

LogFormatter

Bases: Formatter

Log formatter that adds colors based on log levels.

Source code in src/decima/logger.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class LogFormatter(logging.Formatter):
    """Log formatter that adds colors based on log levels."""

    def __init__(self, class_length: int) -> None:
        """Initialize the LogFormatter with a specified class name length for formatting.

        Args:
            class_length (int): The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

        Notes:
            - The formatter will apply different colors to the log output based on the log level (e.g., TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL).
            - The log format includes the timestamp, log level, logger name (formatted with bold), and the log message.

        """
        super().__init__()
        self.class_length: int = class_length

    cyan, blue, gray, yellow, red, bold_red = (
        "\x1b[36m",
        "\x1b[34m",
        "\x1b[37m",
        "\x1b[33m",
        "\x1b[31m",
        "\x1b[31;1m",
    )
    reset = "\x1b[0m"

    bold = "\033[1m"
    reset_bold = "\033[0m"

    fmt_str = f"%(asctime)s - [%(levelname)s] - {bold}%(name)s{reset_bold} - %(message)s"

    def format(self, record: logging.LogRecord) -> str:
        """Override the format method to apply color formatting based on log levels and truncate logger names if they exceed the specified class length."""
        if len(record.name) > self.class_length:
            record.name = record.name[-self.class_length :]

        formats = {
            5: f"{self.cyan}{self.fmt_str}{self.reset}",
            logging.DEBUG: f"{self.blue}{self.fmt_str}{self.reset}",
            logging.INFO: f"{self.gray}{self.fmt_str}{self.reset}",
            logging.WARNING: f"{self.yellow}{self.fmt_str}{self.reset}",
            logging.ERROR: f"{self.red}{self.fmt_str}{self.reset}",
            logging.CRITICAL: f"{self.bold_red}{self.fmt_str}{self.reset}",
        }
        log_fmt = formats.get(record.levelno, self.fmt_str)

        return logging.Formatter(log_fmt).format(record)

__init__(class_length)

Initialize the LogFormatter with a specified class name length for formatting.

Parameters:
  • class_length (int) –

    The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

Notes
  • The formatter will apply different colors to the log output based on the log level (e.g., TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL).
  • The log format includes the timestamp, log level, logger name (formatted with bold), and the log message.
Source code in src/decima/logger.py
11
12
13
14
15
16
17
18
19
20
21
22
23
def __init__(self, class_length: int) -> None:
    """Initialize the LogFormatter with a specified class name length for formatting.

    Args:
        class_length (int): The maximum length of the logger name to display in the log output. If the logger name exceeds this length, it will be truncated from the left.

    Notes:
        - The formatter will apply different colors to the log output based on the log level (e.g., TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL).
        - The log format includes the timestamp, log level, logger name (formatted with bold), and the log message.

    """
    super().__init__()
    self.class_length: int = class_length

format(record)

Override the format method to apply color formatting based on log levels and truncate logger names if they exceed the specified class length.

Source code in src/decima/logger.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def format(self, record: logging.LogRecord) -> str:
    """Override the format method to apply color formatting based on log levels and truncate logger names if they exceed the specified class length."""
    if len(record.name) > self.class_length:
        record.name = record.name[-self.class_length :]

    formats = {
        5: f"{self.cyan}{self.fmt_str}{self.reset}",
        logging.DEBUG: f"{self.blue}{self.fmt_str}{self.reset}",
        logging.INFO: f"{self.gray}{self.fmt_str}{self.reset}",
        logging.WARNING: f"{self.yellow}{self.fmt_str}{self.reset}",
        logging.ERROR: f"{self.red}{self.fmt_str}{self.reset}",
        logging.CRITICAL: f"{self.bold_red}{self.fmt_str}{self.reset}",
    }
    log_fmt = formats.get(record.levelno, self.fmt_str)

    return logging.Formatter(log_fmt).format(record)