Source code for src.logger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Provides a centralised logging manager for the application.

:module: logger
:author: Le Bars, Yoann
"""

# Defines the public names of this module.
__all__ = ['Logger']

import sys
from typing import TextIO
from threading import Lock
from syslog import syslog, openlog, LOG_PID, LOG_USER
from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext
from src.args import CmdLineArgs, LogLevel


[docs] class Logger: """ Manages application logging based on command-line arguments. This class sets up a custom Qt message handler to filter logs by level and route them to the console, a file, or syslog as specified by the user. :ivar CmdLineArgs _args: The parsed command-line arguments. :ivar set[int] _active_levels: The set of QtMsgType levels to log. :ivar dict[int, str] _level_prefixes: Prefixes for each log level. :ivar TextIO | None _log_file: An open file handle for the log file, or None. :ivar Lock _lock: A lock to ensure thread-safe access to shared resources. """ _active_levels: set[int] _args: CmdLineArgs _level_prefixes: dict[int, str] _log_file: TextIO | None = None _lock: Lock def __init__(self, args: CmdLineArgs): """ Initialises the logger and installs the custom Qt message handler. :param CmdLineArgs args: Parsed command-line arguments. :rtype: None """ self._args = args self._lock = Lock() # This mapping defines the filtering levels. # A higher level includes all levels below it. log_levels = { LogLevel.DEBUG: {QtMsgType.QtDebugMsg, QtMsgType.QtInfoMsg, QtMsgType.QtWarningMsg, QtMsgType.QtCriticalMsg, QtMsgType.QtFatalMsg}, LogLevel.INFO: {QtMsgType.QtInfoMsg, QtMsgType.QtWarningMsg, QtMsgType.QtCriticalMsg, QtMsgType.QtFatalMsg}, LogLevel.WARNING: {QtMsgType.QtWarningMsg, QtMsgType.QtCriticalMsg, QtMsgType.QtFatalMsg}, LogLevel.CRITICAL: {QtMsgType.QtCriticalMsg, QtMsgType.QtFatalMsg}, LogLevel.FATAL: {QtMsgType.QtFatalMsg} } self._active_levels = log_levels[args.log_level] self._level_prefixes = { QtMsgType.QtDebugMsg: '[debug]', QtMsgType.QtInfoMsg: '[info]', QtMsgType.QtWarningMsg: '[warning]', QtMsgType.QtCriticalMsg: '[critical]', QtMsgType.QtFatalMsg: '[fatal]' } if self._args.debug_syslog: openlog(self._args.debug_syslog, LOG_PID, LOG_USER) if self._args.debug_file: # The file handle must remain open for the lifetime of the logger, so a 'with' # statement is not appropriate here. The file is closed in the cleanup() method. self._log_file = open(self._args.debug_file, 'w', # pylint: disable=consider-using-with encoding='utf-8') qInstallMessageHandler(self.message_handler)
[docs] def message_handler(self, mode: QtMsgType, _context: QMessageLogContext, msg: str): """ A custom Qt message handler that filters, formats, and routes log messages. :param QtMsgType mode: The type/level of the message. :param QMessageLogContext _context: The context information (unused). :param str msg: The log message. """ # Acquire the lock to ensure that the entire message handling process is atomic. # This prevents race conditions if multiple threads log messages simultaneously. with self._lock: # Check if the message's type is in the set of active levels. if mode not in self._active_levels: return prefix = self._level_prefixes.get(mode, '[unknown]') formatted_msg = f"{prefix} {msg}" if self._args.debug_console: print(formatted_msg, file=sys.stdout) if self._args.debug_syslog: syslog(formatted_msg) if self._log_file: self._log_file.write(formatted_msg + '\n') self._log_file.flush()
[docs] def cleanup(self) -> None: """ Cleans up logging resources and restores the default message handler. """ if self._log_file: self._log_file.close() self._log_file = None # Restore the default message handler to avoid issues during shutdown. qInstallMessageHandler(None)