diff --git a/src/aare/common/logger_config.py b/src/aare/common/logger_config.py index e0dbca01..eced4277 100644 --- a/src/aare/common/logger_config.py +++ b/src/aare/common/logger_config.py @@ -1,32 +1,14 @@ import logging import logging.config +import os from pathlib import Path import yaml -import os -from PySide6.QtCore import QObject, Signal - - -class QtLogEmitter(QObject): - message = Signal(str) - - -class QtLogHandler(logging.Handler): - def __init__(self, emitter: QtLogEmitter): - super().__init__() - self.emitter = emitter - - def emit(self, record: logging.LogRecord): - try: - msg = self.format(record) - self.emitter.message.emit(msg) - except Exception: - self.handleError(record) class IgnoreSuccessfulStatusAccessFilter(logging.Filter): - #Should be disabled if debugging status calls. - #In particular if this is causing server/GUI lag + # Should be disabled if debugging status calls. + # In particular if this is causing server/GUI lag _SUPPRESSED_SUCCESS_MESSAGES = ( '"GET /status HTTP/1.1" 200', '"GET /sample/reference_tools HTTP/1.1" 200', @@ -94,6 +76,7 @@ def get_uvicorn_logging_config() -> dict: }, } + def get_config_path(env: str = "dev") -> Path: module_dir = Path(__file__).parent config_filename = f"logging_{env}.yaml" @@ -104,8 +87,13 @@ def get_config_path(env: str = "dev") -> Path: return config_path -#TODO fix logging. -def setup_logger(name="aareDAQ", base_dir: str | None = f'~/tmp/mxlogs/', config_path: str | None = None): + +# TODO fix logging. +def setup_logger( + name="aareDAQ", + base_dir: str | None = f"~/tmp/mxlogs/", + config_path: str | None = None, +): # switch to production mode using: $ APP_ENV=prod python main.py env = os.getenv("APP_ENV", "dev") # default: dev @@ -118,14 +106,15 @@ def setup_logger(name="aareDAQ", base_dir: str | None = f'~/tmp/mxlogs/', config config = yaml.safe_load(f.read()) # Force base directory to be under the user's home dir - effective_base = base_dir + f'{name}' or f'/tmp/logs/mxlogs/{name}' - effective_base = os.path.abspath(os.path.expanduser(os.path.expandvars(effective_base))) + effective_base = base_dir + f"{name}" or f"/tmp/logs/mxlogs/{name}" + effective_base = os.path.abspath( + os.path.expanduser(os.path.expandvars(effective_base)) + ) # Ensure directories for file handlers exist handlers = config.get("handlers", {}) for h in handlers.values(): - filename = h.get("filename") if not filename: continue @@ -134,7 +123,7 @@ def setup_logger(name="aareDAQ", base_dir: str | None = f'~/tmp/mxlogs/', config h["filename"] = abs_filename log_dir = os.path.dirname(abs_filename) if log_dir and not os.path.exists(log_dir): - os.makedirs(log_dir, exist_ok=True) + os.makedirs(log_dir, exist_ok=True) logging.config.dictConfig(config) logging.getLogger("redis_lock").setLevel(logging.WARNING) @@ -144,7 +133,11 @@ def setup_logger(name="aareDAQ", base_dir: str | None = f'~/tmp/mxlogs/', config logging.getLogger("aaregui").setLevel(logging.INFO) return logging.getLogger(name) -def find_existing_formatter(default_fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", default_datefmt=None): + +def find_existing_formatter( + default_fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + default_datefmt=None, +): root = logging.getLogger() for h in root.handlers: if h.formatter: @@ -158,15 +151,18 @@ def find_existing_formatter(default_fmt="%(asctime)s [%(levelname)s] %(name)s: % return logging.Formatter(default_fmt, datefmt=default_datefmt) + def attach_to_logger(logger_name: str = "", handler: logging.Handler = None): logging.getLogger(logger_name).addHandler(handler) + if __name__ == "__main__": - os.environ["APP_ENV"] = "dev" # or "prod" - #os.environ["LOG_DIR"] = "/tmp/mxlogs" # optional; matches your setup code + # os.environ["LOG_DIR"] = "/tmp/mxlogs" # optional; matches your setup code - log = setup_logger("aareDAQ", base_dir="/tmp/test_logs") # use the configured logger name + log = setup_logger( + "aareDAQ", base_dir="/tmp/test_logs" + ) # use the configured logger name log.debug("debug smoke test") log.error("error smoke test") @@ -182,4 +178,3 @@ if __name__ == "__main__": log.debug("debug test") log.error("error test") print(type(h), getattr(h, "baseFilename", None), h.level) - diff --git a/src/aare/gui/log.py b/src/aare/gui/log.py new file mode 100644 index 00000000..7bb6bb7d --- /dev/null +++ b/src/aare/gui/log.py @@ -0,0 +1,20 @@ +import logging + +from PySide6.QtCore import QObject, Signal + + +class QtLogEmitter(QObject): + message = Signal(str) + + +class QtLogHandler(logging.Handler): + def __init__(self, emitter: QtLogEmitter): + super().__init__() + self.emitter = emitter + + def emit(self, record: logging.LogRecord): + try: + msg = self.format(record) + self.emitter.message.emit(msg) + except Exception: + self.handleError(record) diff --git a/src/aare/gui/panels/LogPanel.py b/src/aare/gui/panels/LogPanel.py index 8af2f928..ef4986a8 100644 --- a/src/aare/gui/panels/LogPanel.py +++ b/src/aare/gui/panels/LogPanel.py @@ -1,17 +1,18 @@ from PySide6.QtCore import Qt, QTimer, Signal, Slot from PySide6.QtWidgets import ( QDockWidget, - QPlainTextEdit, - QWidget, - QVBoxLayout, + QFrame, QHBoxLayout, QLabel, + QPlainTextEdit, QPushButton, QToolButton, - QFrame, + QVBoxLayout, + QWidget, ) -from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger +from aare.common.logger_config import attach_to_logger, find_existing_formatter +from aare.gui.log import QtLogEmitter, QtLogHandler class RuntimeNotificationWidget(QFrame): @@ -148,7 +149,9 @@ class RuntimeNotificationWidget(QFrame): timeout = 5000 if auto_clear_ms is None else int(auto_clear_ms) self._auto_clear_timer.start(timeout) - def show_waiting(self, *, title: str, message: str, auto_clear_ms: int | None = None) -> None: + def show_waiting( + self, *, title: str, message: str, auto_clear_ms: int | None = None + ) -> None: self.show_notification( title=title, message=message, @@ -171,7 +174,9 @@ class RuntimeNotificationWidget(QFrame): class LogDock(QDockWidget): def __init__(self, title="Log", parent=None): super().__init__(title, parent) - self.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea) + self.setAllowedAreas( + Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea + ) self.container = QWidget(self) @@ -236,4 +241,4 @@ class LogDock(QDockWidget): self.notification.clear_notification() def clear(self): - self.view.clear() \ No newline at end of file + self.view.clear() diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py index 9a367107..1f0c833c 100644 --- a/src/aare/gui/panels/developer_help_dialog.py +++ b/src/aare/gui/panels/developer_help_dialog.py @@ -5,35 +5,35 @@ import logging import time from typing import Dict -from PySide6.QtCore import Qt, Slot, QUrl -from PySide6.QtGui import QGuiApplication, QDesktopServices +from PySide6.QtCore import Qt, QUrl, Slot +from PySide6.QtGui import QDesktopServices, QGuiApplication from PySide6.QtWidgets import ( - QDialog, - QVBoxLayout, - QHBoxLayout, - QLineEdit, - QPushButton, - QTabWidget, - QTableWidget, - QTableWidgetItem, - QTextEdit, - QLabel, - QWidget, QCheckBox, + QDialog, QDialogButtonBox, QFormLayout, QFrame, - QSpinBox, + QHBoxLayout, + QLabel, + QLineEdit, QMessageBox, + QPushButton, + QSpinBox, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QTextEdit, + QVBoxLayout, + QWidget, ) from aare.common.error_codes import error_code_help -from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger +from aare.common.logger_config import attach_to_logger, find_existing_formatter from aare.common.models import OpenGuiSessionInfo +from aare.gui.log import QtLogEmitter, QtLogHandler from aare.gui.threads.daq_worker import DAQWorker - class DeveloperHelpDialog(QDialog): def __init__(self, *, daq: DAQWorker, is_staff: bool, parent=None): super().__init__(parent) @@ -57,7 +57,9 @@ class DeveloperHelpDialog(QDialog): self._banner = QLabel(self) self._banner.setVisible(self._is_staff) self._banner.setWordWrap(True) - self._banner.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._banner.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse + ) self._banner.setStyleSheet( "QLabel {" " background: #f6f6f6;" @@ -76,7 +78,9 @@ class DeveloperHelpDialog(QDialog): top.addWidget(QLabel("Filter:", self)) self._filter = QLineEdit(self) - self._filter.setPlaceholderText("Type to filter (matches name, value, or help)…") + self._filter.setPlaceholderText( + "Type to filter (matches name, value, or help)…" + ) self._filter.setClearButtonEnabled(True) self._filter.setMinimumHeight(28) self._filter.setStyleSheet( @@ -120,12 +124,16 @@ class DeveloperHelpDialog(QDialog): self._open_gui_log_btn = QPushButton("Open GUI log", self) self._open_gui_log_btn.setVisible(self._is_staff) - self._open_gui_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareGUI")) + self._open_gui_log_btn.clicked.connect( + lambda: self._open_log_file_for_logger("aareGUI") + ) top.addWidget(self._open_gui_log_btn) self._open_daq_log_btn = QPushButton("Open DAQ log", self) self._open_daq_log_btn.setVisible(self._is_staff) - self._open_daq_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareDAQ")) + self._open_daq_log_btn.clicked.connect( + lambda: self._open_log_file_for_logger("aareDAQ") + ) top.addWidget(self._open_daq_log_btn) self._tabs = QTabWidget(self) @@ -136,7 +144,9 @@ class DeveloperHelpDialog(QDialog): self._codes_table.setHorizontalHeaderLabels(["Name"]) self._codes_table.setSortingEnabled(True) self._codes_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) - self._codes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self._codes_table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) self._codes_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) self._codes_table.itemSelectionChanged.connect(self._update_code_details) self._codes_table.horizontalHeader().setStretchLastSection(True) @@ -156,17 +166,23 @@ class DeveloperHelpDialog(QDialog): details_layout.setSpacing(8) form = QFormLayout() - form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + form.setLabelAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + ) details_layout.addLayout(form) self._detail_name = QLabel("-", self) - self._detail_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._detail_name.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse + ) form.addRow("Name:", self._detail_name) value_row = QHBoxLayout() value_row.setSpacing(8) self._detail_value = QLabel("-", self) - self._detail_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._detail_value.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse + ) self._copy_value_btn = QPushButton("Copy value", self) self._copy_value_btn.clicked.connect(self._copy_selected_value) value_row.addWidget(self._detail_value, 1) @@ -177,7 +193,9 @@ class DeveloperHelpDialog(QDialog): self._detail_help = QLabel("-", self) self._detail_help.setWordWrap(True) - self._detail_help.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._detail_help.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse + ) self._detail_help.setStyleSheet( "QLabel {" " background: white;" @@ -201,7 +219,9 @@ class DeveloperHelpDialog(QDialog): self._payload_summary = QLabel(self) self._payload_summary.setVisible(self._is_staff) self._payload_summary.setWordWrap(True) - self._payload_summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._payload_summary.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse + ) self._payload_text = QTextEdit(self) self._payload_text.setReadOnly(True) @@ -253,11 +273,26 @@ class DeveloperHelpDialog(QDialog): self._gui_sessions_table = QTableWidget(self) self._gui_sessions_table.setColumnCount(8) self._gui_sessions_table.setHorizontalHeaderLabels( - ["Session", "User", "Staff", "Baton", "Last seen", "Last interaction", "Close requested", "Requested by"] + [ + "Session", + "User", + "Staff", + "Baton", + "Last seen", + "Last interaction", + "Close requested", + "Requested by", + ] + ) + self._gui_sessions_table.setEditTriggers( + QTableWidget.EditTrigger.NoEditTriggers + ) + self._gui_sessions_table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) + self._gui_sessions_table.setSelectionMode( + QTableWidget.SelectionMode.SingleSelection ) - self._gui_sessions_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) - self._gui_sessions_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) - self._gui_sessions_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) self._gui_sessions_table.setSortingEnabled(True) self._gui_sessions_table.horizontalHeader().setStretchLastSection(True) @@ -270,7 +305,9 @@ class DeveloperHelpDialog(QDialog): self._gui_close_grace_spin.setSuffix(" s") self._gui_request_close_btn = QPushButton("Request Close", self) - self._gui_request_close_btn.clicked.connect(self._request_close_selected_gui) + self._gui_request_close_btn.clicked.connect( + self._request_close_selected_gui + ) self._gui_force_remove_btn = QPushButton("Force Remove", self) self._gui_force_remove_btn.clicked.connect(self._force_remove_selected_gui) @@ -387,7 +424,9 @@ class DeveloperHelpDialog(QDialog): except Exception: pretty = str(p) blocks.append(f"#{i}\n{pretty}") - self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)") + self._payloads_text.setPlainText( + "\n\n".join(blocks) if blocks else "(none captured yet)" + ) @Slot(str) def _append_error_log_line(self, line: str) -> None: @@ -406,7 +445,10 @@ class DeveloperHelpDialog(QDialog): if isinstance(body, dict): code = body.get("code") msg = body.get("message") - return (str(code) if code is not None else None, str(msg) if msg is not None else None) + return ( + str(code) if code is not None else None, + str(msg) if msg is not None else None, + ) return (None, None) def _update_banner(self) -> None: @@ -457,9 +499,11 @@ class DeveloperHelpDialog(QDialog): items = sorted(self._codes.items(), key=lambda kv: kv[0]) if term: + def _match(name: str, value: str) -> bool: h = error_code_help(value) or "" return term in f"{name}\n{value}\n{h}".lower() + items = [(k, v) for (k, v) in items if _match(k, v)] self._codes_table.setRowCount(len(items)) @@ -476,7 +520,11 @@ class DeveloperHelpDialog(QDialog): self._update_code_details() # If enabled, re-try selection after the table content changes - if self._is_staff and self._always_highlight_last_error and not self._freeze_payload: + if ( + self._is_staff + and self._always_highlight_last_error + and not self._freeze_payload + ): self._select_code_from_last_error() def _selected_code(self) -> tuple[str | None, str | None]: @@ -502,7 +550,9 @@ class DeveloperHelpDialog(QDialog): self._detail_name.setText(name) self._detail_value.setText(value) - self._detail_help.setText(error_code_help(value) or "(no help text defined yet)") + self._detail_help.setText( + error_code_help(value) or "(no help text defined yet)" + ) self._copy_value_btn.setEnabled(True) @Slot() @@ -548,7 +598,12 @@ class DeveloperHelpDialog(QDialog): if not self._is_staff: return - self._gui_sessions = [s if isinstance(s, OpenGuiSessionInfo) else OpenGuiSessionInfo.model_validate(s) for s in sessions] + self._gui_sessions = [ + s + if isinstance(s, OpenGuiSessionInfo) + else OpenGuiSessionInfo.model_validate(s) + for s in sessions + ] self._gui_sessions_table.setSortingEnabled(False) self._gui_sessions_table.setRowCount(len(self._gui_sessions)) @@ -592,7 +647,9 @@ class DeveloperHelpDialog(QDialog): def _request_close_selected_gui(self) -> None: session_id = self._selected_gui_session_id() if session_id is None: - QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.") + QMessageBox.information( + self, "Active GUIs", "Please select a GUI session first." + ) return grace_seconds = int(self._gui_close_grace_spin.value()) @@ -611,7 +668,9 @@ class DeveloperHelpDialog(QDialog): def _force_remove_selected_gui(self) -> None: session_id = self._selected_gui_session_id() if session_id is None: - QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.") + QMessageBox.information( + self, "Active GUIs", "Please select a GUI session first." + ) return answer = QMessageBox.warning( @@ -629,4 +688,4 @@ class DeveloperHelpDialog(QDialog): return self._daq.force_remove_gui_session(session_id) - self._daq.load_gui_sessions() \ No newline at end of file + self._daq.load_gui_sessions()