## ZooUI - Zooming User Interface
## Copyright (C) 2009 David Roberts <d@vidr.cc>
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <https://www.gnu.org/licenses/>.
"""Centralized logging configuration for ZooUI."""
import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import cast
from zooui.utils._xdg import get_state_dir
[docs]
class LoggerConfig:
"""
Constructor :
LoggerConfig()
Parameters :
None
LoggerConfig() --> None
Centralized logger configuration for ZooUI.
Provides consistent logging across all modules with support for:
- Multiple log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- Console and file output
- Rotating log files
- Color-coded console output (optional)
- Per-module log level control
"""
_initialized = False
_log_dir = None
_log_file = None
_console_level = logging.INFO
_file_level = logging.DEBUG
_loggers: dict[str, logging.Logger] = {}
# ANSI color codes for console output
COLORS = {
"DEBUG": "\033[36m", # Cyan
"INFO": "\033[32m", # Green
"WARNING": "\033[33m", # Yellow
"ERROR": "\033[31m", # Red
"CRITICAL": "\033[35m", # Magenta
"RESET": "\033[0m", # Reset
}
[docs]
@classmethod
def initialize(
cls,
debug: bool = False,
log_to_file: bool = True,
log_to_console: bool = True,
log_dir: str | None = None,
colored_output: bool = True,
verbose: bool = False,
) -> None:
"""
Method :
LoggerConfig.initialize(debug, log_to_file, log_to_console, log_dir, colored_output, verbose)
Parameters :
debug : bool
log_to_file : bool
log_to_console : bool
log_dir : str
colored_output : bool
verbose : bool
LoggerConfig.initialize(debug, log_to_file, log_to_console, log_dir, colored_output, verbose) --> None
Initialize the logging system.
Args:
debug (bool): Enable debug mode (sets console level to DEBUG)
log_to_file (bool): Enable logging to file
log_to_console (bool): Enable logging to console
log_dir (str): Directory for log files (default: ./logs)
colored_output (bool): Enable colored console output
verbose (bool): Enable verbose mode (shows more detailed info)
"""
# Determine log levels
if debug:
cls._console_level = logging.DEBUG
cls._file_level = logging.DEBUG
elif verbose:
cls._console_level = logging.INFO
cls._file_level = logging.DEBUG
else:
cls._console_level = logging.WARNING
cls._file_level = logging.INFO
# Setup log directory with tilde expansion for command-line arguments
if log_dir:
cls._log_dir = Path(log_dir).expanduser()
else:
cls._log_dir = get_state_dir() / "logs"
if log_to_file:
cls._log_dir.mkdir(parents=True, exist_ok=True)
cls._log_file = cls._log_dir / "zooui.log"
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # Capture everything, handlers will filter
# Remove existing handlers
for handler in root_logger.handlers[:]:
handler.close()
root_logger.removeHandler(handler)
# Create formatters
if colored_output and log_to_console:
console_formatter = ColoredFormatter("%(color)s[%(levelname)-8s]%(reset)s %(name)-25s | %(message)s")
else:
console_formatter = logging.Formatter("[%(levelname)-8s] %(name)-25s | %(message)s") # type: ignore[assignment]
file_formatter = logging.Formatter(
"%(asctime)s | [%(levelname)-8s] | %(name)-25s | %(funcName)-20s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
# Add console handler
if log_to_console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(cls._console_level)
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
# Add file handler with rotation
if log_to_file:
# _log_file is guaranteed to be set when log_to_file is True
file_handler = RotatingFileHandler(
cast(Path, cls._log_file),
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
)
file_handler.setLevel(cls._file_level)
file_handler.setFormatter(file_formatter)
root_logger.addHandler(file_handler)
cls._initialized = True
# Log initialization
init_logger = cls.get_logger("LoggerConfig")
init_logger.info("=" * 60)
init_logger.info("ZooUI Logging System Initialized")
init_logger.info(f"Debug Mode: {debug}")
init_logger.info(f"Console Level: {logging.getLevelName(cls._console_level)}")
init_logger.info(f"File Level: {logging.getLevelName(cls._file_level)}")
if log_to_file:
init_logger.info(f"Log File: {cls._log_file}")
init_logger.info("=" * 60)
[docs]
@classmethod
def get_logger(cls, name: str) -> logging.Logger:
"""
Method :
LoggerConfig.get_logger(name)
Parameters :
name : str
LoggerConfig.get_logger(name) --> logging.Logger
Get a logger instance for the specified module.
Args:
name (str): Name of the module/class requesting the logger
Returns:
logging.Logger: Configured logger instance
"""
if not cls._initialized:
# Initialize with default settings if not already done
cls.initialize()
if name not in cls._loggers:
logger = logging.getLogger(f"zooui.{name}")
cls._loggers[name] = logger
return cls._loggers[name]
[docs]
@classmethod
def set_level(cls, level: int, module: str | None = None) -> None:
"""
Method :
LoggerConfig.set_level(level, module)
Parameters :
level : int
module : str
LoggerConfig.set_level(level, module) --> None
Change the logging level at runtime.
Args:
level (int): Logging level (e.g., logging.DEBUG, logging.INFO)
module (str): Specific module name, or None for all modules
"""
if module:
logger = cls.get_logger(module)
logger.setLevel(level)
else:
# Update all loggers
cls._console_level = level
root_logger = logging.getLogger()
for handler in root_logger.handlers:
# Update both console and file handlers
handler.setLevel(level)
[docs]
@classmethod
def enable_debug(cls) -> None:
"""
Method :
LoggerConfig.enable_debug()
Parameters :
None
LoggerConfig.enable_debug() --> None
Enable debug mode at runtime.
"""
cls.set_level(logging.DEBUG)
logger = cls.get_logger("LoggerConfig")
logger.info("Debug mode enabled")
[docs]
@classmethod
def disable_debug(cls) -> None:
"""
Method :
LoggerConfig.disable_debug()
Parameters :
None
LoggerConfig.disable_debug() --> None
Disable debug mode at runtime.
"""
cls.set_level(logging.INFO)
logger = cls.get_logger("LoggerConfig")
logger.info("Debug mode disabled")
[docs]
@classmethod
def get_log_file_path(cls) -> Path | None:
"""
Method :
LoggerConfig.get_log_file_path()
Parameters :
None
LoggerConfig.get_log_file_path() --> Path
Get the path to the current log file.
Returns:
Path: Path to log file, or None if file logging is disabled
"""
return cls._log_file
[docs]
def get_logger(name: str) -> logging.Logger:
"""
Function :
get_logger(name)
Parameters :
name : str
get_logger(name) --> logging.Logger
Convenience function to get a logger.
Args:
name (str): Name of the module/class requesting the logger
Returns:
logging.Logger: Configured logger instance
"""
return LoggerConfig.get_logger(name)