A log module for KB4IT

Excerpt

A very useful log module to allow redirecting logs to another file in runtime.

KB4IT needs this feature to redirect logs to the right file when a new instance is executed.

Features

  • Log redirection: when KB4IT is executed, it doesn’t know which application is going to be executed, so the first logs are sent to a temporary log directory. Once the backend is aware about which app (or project) is started, temporary logs are copied to the right destination and the log module redirects the logging there. In this way, the user have the complete trace.

  • Distinct levels for console and file handlers: Console handler (the output displayed in the workflow) is set by default to level INFO (minimum). For troubleshooting purposes, the whole trace with level DEBUG is sent to the log file.

    Run kb4it -L INFO build config/blog.json -w 8
    
          INFO |   99 | Workflow             | 11/01/2026 12:38:45.751 | Building a website for repository 't00mterías'
          INFO |  100 | Workflow             | 11/01/2026 12:38:45.751 | Using theme 'blog'
          INFO |  101 | Workflow             | 11/01/2026 12:38:45.751 | Check environment
          INFO |  103 | Workflow             | 11/01/2026 12:38:45.760 | Allow theme to generate sources
          INFO |  106 | Workflow             | 11/01/2026 12:38:45.760 | Get source documents
       WARNING |  352 | Backend              | 11/01/2026 12:38:45.760 |   - Added missing 'About App' to your sources
          INFO |  108 | Workflow             | 11/01/2026 12:38:45.761 | Preprocessing
          INFO |  110 | Workflow             | 11/01/2026 12:38:45.844 | Backend processing
          INFO |  112 | Workflow             | 11/01/2026 12:38:45.877 | Theme processing
          INFO |  114 | Workflow             | 11/01/2026 12:38:45.927 | Compilation
          INFO |  116 | Workflow             | 11/01/2026 12:38:52.129 | Clean up target
          INFO |  118 | Workflow             | 11/01/2026 12:38:52.161 | Refresh target
          INFO |  120 | Workflow             | 11/01/2026 12:38:52.306 | Theme post activities
          INFO |  123 | Workflow             | 11/01/2026 12:38:52.467 | Repository website built
          INFO |  124 | Workflow             | 11/01/2026 12:38:52.467 | URL: /home/runner/work/blog/blog/docs/index.html
          INFO |  125 | Workflow             | 11/01/2026 12:38:52.467 | Full log: /home/runner/work/blog/blog/var/log/homerunnerworkblogblogsource.log
          INFO |  126 | Workflow             | 11/01/2026 12:38:52.467 | The End

Code

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Log module.
File: log.py
Author: Tomás Vírseda
License: GPL v3
"""

import logging

_PATTERN = (
    "%(levelname)10s | %(lineno)4d | %(name)-20s | "
    "%(asctime)s.%(msecs)03d | %(message)s"
)

_DATEFMT = "%d/%m/%Y %H:%M:%S"


def setup_logging(
    level: str = "INFO",
    logfile: str | None = None,
):
    """
    Configure root logger once.
    """

    if level is not None:
        level_dict = {
            'DEBUG': logging.DEBUG,
            'INFO': logging.INFO,
            'WARNING': logging.WARNING,
            'ERROR': logging.ERROR,
            'CRITICAL': logging.CRITICAL
        }
        severity = level_dict.get(level, logging.DEBUG)
    else:
        severity = logging.INFO

    root = logging.getLogger()
    root.setLevel(logging.DEBUG)

    if root.handlers:
        return  # Already configured

    formatter = logging.Formatter(_PATTERN, datefmt=_DATEFMT)

    # Console handler
    console = logging.StreamHandler()
    console.setFormatter(formatter)
    console.setLevel(severity)
    root.addHandler(console)

    # File handler
    file_handler = logging.FileHandler(logfile, mode="w")
    file_handler.setFormatter(formatter)
    root.addHandler(file_handler)


def get_logger(name: str) -> logging.Logger:
    """
    Return a named logger.
    """
    return logging.getLogger(name)


def redirect_logs(logfile: str):
    """
    Redirect logging to a new file at runtime.
    """
    root = logging.getLogger()

    formatter = logging.Formatter(_PATTERN, datefmt=_DATEFMT)

    # Remove only existing FileHandlers
    for handler in root.handlers[:]:
        if isinstance(handler, logging.FileHandler):
            handler.flush()
            handler.close()
            root.removeHandler(handler)

    # Add new file handler
    file_handler = logging.FileHandler(logfile, mode="a")
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.DEBUG)
    root.addHandler(file_handler)