Source code for apikeyper.log_engine

"""
log_engine
----------
Author: Taylor-Jayde
Date: Today's Date

This module provides logging utilities tailored for the APIKeyPER application. It establishes a flexible logging mechanism
with support for both console and file outputs, featuring rich, colored console logs using the Rich library. The module
also incorporates a Singleton design pattern for the Logger class, ensuring consistent logging behavior across the
application.

Furthermore, the module provides a meta-class 'Loggable' which can be inherited by other classes to instantly equip them
with logging capabilities.

Key components:
    - Logger: A Singleton class responsible for managing application logging.
    - Loggable: A meta-class providing logging capabilities to the classes that inherit from it.

Dependencies:
    - logging: Standard library module for event logging.
    - RichHandler from rich.logging: Provides rich, colored output for console logs.
    - inspect: Standard library module for introspecting live objects.
    - apikeyper.__about__: Module providing metadata about the APIKeyPER application.
    - apikeyper.log_engine.helpers: Helper functions for the logging engine.
"""

import inspect
import logging
from rich.logging import RichHandler
from apikeyper.__about__ import __PROG__ as PROG_NAME
from apikeyper.log_engine.helpers import (
    translate_to_logging_level,
    clean_module_name,
    CustomFormatter,
)


DEFAULT_LOGGING_LEVEL = logging.DEBUG


[docs]class Logger: """ A Singleton class responsible for managing the logging mechanisms of the application. It ensures consistent logging behavior across different parts of the application by maintaining single instances of loggers with specific names. The class supports logging to both the console (with colored output) and a file. Attributes: - instances (dict): Keeps track of logger instances to implement the Singleton pattern. """ instances = {}
[docs] def __new__(cls, name, *args, **kwargs): """ Creates a new instance or returns an existing instance of the Logger class for the provided name. This method ensures that only one instance of Logger with a given name exists, implementing the Singleton pattern. Parameters: name (str): The name of the logger. Returns: Logger: An instance of the Logger class. """ if name not in cls.instances: instance = super(Logger, cls).__new__(cls) cls.instances[name] = instance return instance return cls.instances[name]
def __init__( self, name, console_level=DEFAULT_LOGGING_LEVEL, file_level=logging.DEBUG, filename="app.log", ): """ Initializes a logger instance. Sets up console and file handlers and establish logging levels. Parameters: name (str): The name of the logger. console_level (logging level object, optional): The logging level for the console. Defaults to DEBUG. Must be a member of logging module's set of level constants (like logging.INFO or logging.DEBUG). file_level (logging level object, optional): The logging level for the file. Defaults to DEBUG. Must be a member of logging module's set of level constants (like logging.INFO or logging.DEBUG). filename (str, optional): The name of the log file. Defaults to 'app.log'. """ if not hasattr(self, "logger"): self.__name = name self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) self.__console_level = console_level self.filename = filename self.__file_level = file_level or DEFAULT_LOGGING_LEVEL # Remove existing handlers for handler in self.logger.handlers[:]: self.logger.removeHandler(handler) # Prevent log records from being passed to the handlers of ancestor loggers. self.logger.propagate = False self.set_up_console() self.set_up_file() self.children = []
[docs] def set_up_console(self): """ Configures and attaches a console handler to the logger. This method uses the RichHandler to produce colored and formatted console outputs. """ console_handler = RichHandler( show_level=True, markup=True, rich_tracebacks=True, tracebacks_show_locals=True, ) formatter = CustomFormatter( "[green][bold][%(name)s][bold][/green] - %(message)s" ) console_handler.setFormatter(formatter) console_handler.setLevel(self.__console_level) self.logger.addHandler(console_handler)
[docs] def set_up_file(self): """ Configures and attaches a file handler to the logger. This ensures that log messages are also written to a specified file. """ file_handler = logging.FileHandler(self.filename) file_handler.setLevel(self.__file_level) formatter = CustomFormatter( "%(asctime)s - [%(name)s] - %(levelname)s - %(message)s" ) file_handler.setFormatter(formatter) self.logger.addHandler(file_handler)
[docs] def set_level(self, console_level=None, file_level=None): """ Updates the logging levels for both the console and file handlers. If provided, also updates child loggers. Parameters: console_level (logging level object, optional): The console logging level. Defaults to None. file_level (logging level object, optional): The file logging level. Defaults to None. """ if console_level is not None: self.logger.handlers[0].setLevel(console_level) for child in self.children: child.set_level(console_level=console_level) if file_level is not None: self.logger.handlers[1].setLevel(file_level) for child in self.children: child.set_level(file_level=file_level)
[docs] def get_child(self, name=None, console_level=None, file_level=None): console_level = console_level or DEFAULT_LOGGING_LEVEL # Get the stack frame of the caller caller_frame = inspect.stack()[1] # If no name is provided, use the name of the calling function/method/class if name is None: name = caller_frame.function # Check if the caller is a class or a function/method caller_self = caller_frame.frame.f_locals.get("self", None) # Determine the separator separator = ":" if caller_self and hasattr(caller_self, name) else "." child_logger_name = f"{self.logger.name}{separator}{name}" # Check if child logger already exists for child in self.children: if child.logger.name == child_logger_name: return child # If child logger doesn't exist, create a new one child_logger = Logger(child_logger_name, console_level, file_level) self.children.append(child_logger) return child_logger
[docs] def get_child_names(self) -> list[str]: """ Fetches the names of all child loggers associated with this logger instance. Returns: list of str: A list containing the names of all child loggers. """ return [child.logger.name for child in self.children]
[docs] def find_child_by_name( self, name: str, ): names = self.get_child_names()
LOG_DEVICE = Logger(PROG_NAME, DEFAULT_LOGGING_LEVEL) MOD_LOG_DEVICE = LOG_DEVICE.get_child("log_engine") MOD_LOGGER = MOD_LOG_DEVICE.logger MOD_LOGGER.debug(f"Started logger for {__name__}.") add_child = LOG_DEVICE.get_child import inspect def _get_parent_logging_device(): """ Determines the parent logging device by inspecting the caller's log_device or parent_log_device attribute. Returns: Logger: The parent logging device. """ caller_frame = inspect.currentframe().f_back caller_locals = caller_frame.f_locals print(caller_locals) if "logger" in caller_locals: return caller_locals["logger"] elif "parent_log_device" in caller_locals: return caller_locals["parent_log_device"] else: raise ValueError("Unable to determine the parent logging device.")
[docs]class Loggable: """ A metaclass to enhance classes with logging capabilities. Classes that inherit from 'Loggable' can instantly access a logger without manually setting it up. This logger is derived from a parent logger, ensuring consistent logging behavior and hierarchy. Attributes: - log_device: The logger device associated with the instance of the class. """ def __init__(self, parent_log_device=None, **kwargs): self.parent_log_device = parent_log_device self.__log_name = self.__class__.__name__ if self.parent_log_device is not None: self.__log_device = self.parent_log_device.get_child( self.__class__.__name__ ) else: self.__log_device = _get_parent_logging_device().get_child( self.__class__.__name__ ) @property def log_device(self): return self.__log_device @log_device.setter def log_device(self, new): if not isinstance(new, Logger): raise TypeError('log_device must be of type "Logger"') self.__log_device = new
[docs] def create_child_logger(self, name=None, override=False): """ Creates and returns a child logger of this object's logger. Parameters: name (str, optional): The name of the child logger. If not provided, the name of the calling function is used. override (bool, optional): A flag to override the membership check. Defaults to False. Returns: Logger: An instance of the Logger class that represents the child logger. """ if not override: self.__is_member__() if name is None: name = inspect.stack()[1][ 3 ] # Get the name of the calling function if no name is provided return self.log_device.get_child(name)
[docs] def __is_member__(self): """ Checks whether the caller of this method is a member of the same class. Raises: PermissionError: If the caller of this method is not a member of the same class. """ log_device = self.log_device.get_child("__is_member__") log = log_device.logger current_frame = inspect.currentframe() log.debug(f"Current frame: {current_frame}") caller_frame = current_frame.f_back log.debug(f"Caller frame: {caller_frame}") caller_self = caller_frame.f_locals.get("self", None) log.debug(f"Caller self: {caller_self}") log.debug("Checking if caller is a member of this class...") if not isinstance(caller_self, self.__class__): raise PermissionError( "Access denied.\n" f"Method can only be accessed by members of the same class. {caller_self.__class__.__name__} is not such a member" ) log.debug(f"Access granted to {caller_self.__class__.__name__}")