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 pathlib
import sys

from typing import Any, Self

from ..config.models import LoggingConfig
from ..helpers import script_info
from .exit_handler import ExitHandler
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) -> Self: if not cls._instance: cls._instance = super().__new__(cls, *args, **kwargs) cls._instance.initialized = False return cls._instance
[docs] def __init__(self) -> None: pass
[docs] def initialize(self, config: LoggingConfig | dict[str, Any]) -> None: if not isinstance(config, LoggingConfig): config = LoggingConfig.model_validate(config) if self.initialized: msg = f"Must not initialise {type(self).__name__} twice" raise RuntimeError(msg) self.initialized = True self.config = config self.log_file_path = 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() self._configure_exception_handler()
def _configure_root_logger(self) -> None: """Configure root logger.""" logging.captureWarnings(capture=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 = pathlib.Path(self.log_file_path).parent if not pathlib.Path(log_dir_path).exists(): pathlib.Path(log_dir_path).mkdir(parents=True) 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) if not script_info.is_unit_test(): 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_exception_handler(self) -> None: if self.config.rich: from rich.traceback import install from app.util import callguard install( extra_lines=1, suppress=(callguard,), code_width=160, width=200, show_locals=True, locals_hide_dunder=True, word_wrap=False, ) 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, } )