#!/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)