Source code for utils.logger

"""
Logging configuration for RegressionLab.

This module provides centralized logging configuration and utilities.
It supports both file and console logging with customizable log levels.
"""

# Standard library
import logging
from pathlib import Path
from typing import Optional

# Third-party packages
try:
    from colorama import Fore, Style, init as colorama_init

    # Initialize colorama for Windows
    colorama_init(autoreset=True)
    _COLORAMA_AVAILABLE = True
except ImportError:
    _COLORAMA_AVAILABLE = False

# Local imports
from config import DEFAULT_LOG_FILE, DEFAULT_LOG_LEVEL, get_env
from i18n import t

# Format defaults
_DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
_DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"

# Color configuration for different log levels
_LOG_COLORS = {
    "DEBUG": Fore.CYAN if _COLORAMA_AVAILABLE else "",
    "INFO": Fore.GREEN if _COLORAMA_AVAILABLE else "",
    "WARNING": Fore.YELLOW if _COLORAMA_AVAILABLE else "",
    "ERROR": Fore.RED if _COLORAMA_AVAILABLE else "",
    "CRITICAL": Fore.RED + Style.BRIGHT if _COLORAMA_AVAILABLE else "",
}

# Map env log level names to logging constants (avoid rebuilding on every call)
_LEVEL_MAP = {
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL,
}


class _ColoredFormatter(logging.Formatter):
    """
    Custom formatter that adds color to console log output.

    Colors are applied based on log level:

        - DEBUG: Cyan
        - INFO: Green
        - WARNING: Yellow
        - ERROR: Red
        - CRITICAL: Bright Red
    """

    def format(self, record: logging.LogRecord) -> str:
        """
        Format the log record with colors.

        Args:
            record: The log record to format

        Returns:
            Formatted and colored log string
        """
        if not _COLORAMA_AVAILABLE:
            return super().format(record)

        color = _LOG_COLORS.get(record.levelname, "")
        reset = Style.RESET_ALL
        # Save the original levelname
        original_levelname = record.levelname

        # Add color to levelname
        record.levelname = f"{color}{record.levelname}{reset}"

        # Format the message
        formatted = super().format(record)

        # Restore original levelname
        record.levelname = original_levelname

        return formatted


def _get_log_level_from_env() -> int:
    """
    Get log level from environment variable.

    Returns:
        Logging level constant (e.g., logging.INFO)
    """
    # Use central configuration helper so values and defaults always
    # match those defined in ``.env`` / ``config.env.ENV_SCHEMA``.
    level_name = str(get_env("LOG_LEVEL", DEFAULT_LOG_LEVEL)).upper()
    return _LEVEL_MAP.get(level_name, logging.INFO)


def _get_log_file_from_env() -> str:
    """
    Get log file path from environment variable.

    Returns:
        Path to log file
    """
    return str(get_env("LOG_FILE", DEFAULT_LOG_FILE))


def _should_log_to_console() -> bool:
    """
    Check if console logging is enabled via environment variable.

    Returns:
        True if console logging should be enabled
    """
    # Default and casting are controlled by ``config.env`` so that GUI
    # and Streamlit apps share the exact same behaviour.
    return bool(get_env("LOG_CONSOLE", False, bool))


[docs] def setup_logging( log_file: Optional[str] = None, level: Optional[int] = None, console: Optional[bool] = None, log_format: str = _DEFAULT_LOG_FORMAT, date_format: str = _DEFAULT_DATE_FORMAT, ) -> None: """ Configure application-wide logging. This function sets up logging to both file and console (optional). If no parameters are provided, it uses environment variables or defaults. Args: log_file: Path to log file. If None, uses LOG_FILE env var or default level: Logging level. If None, uses LOG_LEVEL env var or INFO console: Enable console logging. If None, uses LOG_CONSOLE env var or True log_format: Format string for log messages date_format: Format string for timestamps Examples: >>> setup_logging() # Use defaults and env vars >>> setup_logging(log_file='my_app.log', level=logging.DEBUG) """ # Get configuration from environment or use provided/default values if log_file is None: log_file = _get_log_file_from_env() if level is None: level = _get_log_level_from_env() if console is None: console = _should_log_to_console() # Create log directory if it doesn't exist (when path has a directory component) log_path = Path(log_file) if len(log_path.parts) > 1: log_path.parent.mkdir(parents=True, exist_ok=True) # Create formatters # Use standard formatter for file (no colors) file_formatter = logging.Formatter(log_format, datefmt=date_format) # Use colored formatter for console console_formatter = _ColoredFormatter(log_format, datefmt=date_format) # Get root logger root_logger = logging.getLogger() root_logger.setLevel(level) # Remove existing handlers to avoid duplicates root_logger.handlers.clear() # Add file handler try: file_handler = logging.FileHandler(log_file, encoding="utf-8") file_handler.setLevel(level) file_handler.setFormatter(file_formatter) root_logger.addHandler(file_handler) except Exception as e: print(t("warning.log_file_warning", log_file=log_file, error=str(e))) # Add console handler if enabled if console: console_handler = logging.StreamHandler() console_handler.setLevel(level) console_handler.setFormatter(console_formatter) root_logger.addHandler(console_handler) # Log initial message root_logger.info(t("log.logging_initialized")) root_logger.debug(t("log.log_file", file=log_file)) root_logger.debug(t("log.log_level", level=logging.getLevelName(level))) root_logger.debug(t("log.console_logging", enabled=console))
[docs] def get_logger(name: str) -> logging.Logger: """ Get a logger instance for a specific module. Args: name: Name of the logger (typically __name__ of the module) Returns: Logger instance Examples: >>> logger = get_logger(__name__) >>> logger.info("Processing data...") >>> logger.error("Failed to load file", exc_info=True) """ return logging.getLogger(name)
[docs] def log_exception( logger: logging.Logger, exception: Exception, context: Optional[str] = None ) -> None: """ Log an exception with context. Args: logger: Logger instance to use. exception: Exception that occurred. context: Optional context description. Examples: >>> try: ... risky_operation() ... except Exception as e: ... log_exception(logger, e, "Failed to load data") """ msg = ( f"{context}: {type(exception).__name__}: {exception}" if context else f"{type(exception).__name__}: {exception}" ) logger.error(msg, exc_info=True)