Source code for app.util.logging.manager

# SPDX-License-Identifier: GPLv3-or-later
# Copyright © 2025 pygaindalf Rui Pinheiro

"""
Logging configuration and utilities for pygaindalf.
Configures file and TTY logging, log levels, and custom handlers.
"""

import logging
import sys
import os

from typing import Any

from ..helpers import script_info

from .exit_handler import ExitHandler
from .logger import Logger
from ..config.models import LoggingConfig
from .filters import HandlerFilter
from .formatters import ConditionalFormatter

######
# MARK: Constants

# Log file name
LOG_FILE_NAME : str = f"{script_info.get_script_name()}.log"


######
# MARK: Logging Manager
[docs] class LoggingManager: _instance = None
[docs] def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(LoggingManager, cls).__new__(cls, *args, **kwargs) cls._instance.initialized = False return cls._instance
[docs] def __init__(self): pass
[docs] def initialize(self, config : LoggingConfig | dict[str, Any]): if not isinstance(config, LoggingConfig): config = LoggingConfig.model_validate(config) if self.initialized: raise RuntimeError(f"Must not initialise {self.__class__.__name__} twice") self.initialized = True self.config = config self.log_file_path = os.path.join(config.dir, LOG_FILE_NAME) self._configure_root_logger () self._configure_file_handler() self._configure_tty_handler () self._configure_exit_handler() self._configure_custom_levels()
def _configure_root_logger(self) -> None: """ Configure root logger """ logging.captureWarnings(True) logging.root.setLevel(self.config.levels.root.value) def _configure_file_handler(self) -> None: self.fh = None if self.config.levels.file.value < 0: return log_dir_path = os.path.dirname(self.log_file_path) if not os.path.exists(log_dir_path): os.makedirs(log_dir_path) self.fh = logging.FileHandler(self.log_file_path, mode='w') self.fh.setLevel(self.config.levels.file.value) self.fh_formatter = ConditionalFormatter('%(asctime)s [%(levelname)s:%(name)s] %(message)s') self.fh.setFormatter(self.fh_formatter) logging.root.addHandler(self.fh) self.fh_filter = HandlerFilter('file') self.fh.addFilter(self.fh_filter) def _configure_tty_handler(self) -> None: self.ch = None if self.config.levels.tty.value < 0: return # Create console handler if self.config.rich: from .rich_handler import CustomRichHandler self.ch = CustomRichHandler() else: self.ch = logging.StreamHandler(sys.stderr) self.ch_formatter = ConditionalFormatter('[%(levelname).1s:%(name)s] %(message)s') self.ch.setFormatter(self.ch_formatter) self.ch.setLevel(self.config.levels.tty.value) logging.root.addHandler(self.ch) self.ch_filter = HandlerFilter('tty') self.ch.addFilter(self.ch_filter) def _configure_exit_handler(self) -> None: # Exit handler is not needed for unit tests if script_info.is_unit_test(): return self.eh = ExitHandler(self) logging.root.addHandler(self.eh) def _configure_custom_levels(self) -> None: """ Configure custom loggers with the specified levels. """ for name, level in self.config.levels.custom.items(): logging.getLogger(name).setLevel(level.value)
# Handle unit tests - we just initialize the logging manager with minimal configuration if script_info.is_unit_test(): LoggingManager().initialize({ 'levels': { 'file': 'OFF', 'tty': 'NOTSET', }, 'rich': False, })