# FlightMonitor/utils/logger.py import logging import tkinter as tk from tkinter.scrolledtext import ScrolledText from queue import Queue, Empty as QueueEmpty from typing import Optional, Dict, Any class TkinterTextHandler(logging.Handler): """ A logging handler that directs log messages to a Tkinter Text widget in a thread-safe manner using an internal queue and root.after(). """ def __init__( self, text_widget: tk.Text, root_tk_instance: tk.Tk, level_colors: Dict[int, str], queue_poll_interval_ms: int, ): super().__init__() self.text_widget = text_widget self.root_tk_instance = root_tk_instance self.log_queue = Queue() self.level_colors = level_colors self.queue_poll_interval_ms = queue_poll_interval_ms self._after_id_log_processor: Optional[str] = None self._is_active = True if not ( self.text_widget and hasattr(self.text_widget, "winfo_exists") and self.text_widget.winfo_exists() ): print( "Warning: TkinterTextHandler initialized with an invalid or non-existent text_widget.", flush=True, ) self._is_active = False return if not ( self.root_tk_instance and hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): print( "Warning: TkinterTextHandler initialized with an invalid or non-existent root_tk_instance.", flush=True, ) self._is_active = False return if self._is_active: for level, color_value in self.level_colors.items(): level_name = logging.getLevelName(level) if color_value: try: self.text_widget.tag_config(level_name, foreground=color_value) except tk.TclError: print( f"Warning: Could not configure tag for {level_name} during TkinterTextHandler init.", flush=True, ) pass if self._is_active: self._process_log_queue() def emit(self, record: logging.LogRecord): if not self._is_active: return if not ( hasattr(self.text_widget, "winfo_exists") and self.text_widget.winfo_exists() and hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): print( "Warning: TkinterTextHandler.emit: Widget or root instance does not exist. Deactivating handler.", flush=True, ) self._is_active = False return try: msg = self.format(record) level_name = record.levelname self.log_queue.put_nowait((level_name, msg)) except Exception as e: print(f"Error in TkinterTextHandler.emit before queueing: {e}", flush=True) def _process_log_queue(self): if not self._is_active: if ( self._after_id_log_processor and hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass except Exception as e: print( f"Error cancelling after_id in _process_log_queue (handler inactive): {e}", flush=True, ) self._after_id_log_processor = None return if not ( hasattr(self.text_widget, "winfo_exists") and self.text_widget.winfo_exists() and hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): print( "Debug: TkinterTextHandler._process_log_queue: Widget or root destroyed. Stopping.", flush=True, ) if self._after_id_log_processor: try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass except Exception as e: print( f"Error cancelling after_id in _process_log_queue (widgets gone): {e}", flush=True, ) self._after_id_log_processor = None self._is_active = False return try: while self._is_active: try: level_name, msg = self.log_queue.get(block=False, timeout=0.01) except QueueEmpty: break except Exception as e_get: print( f"Error getting from log queue: {e_get}. Stopping processing for this cycle.", flush=True, ) break if not self._is_active or not ( self.text_widget.winfo_exists() and self.root_tk_instance.winfo_exists() ): print( "Debug: TkinterTextHandler._process_log_queue: Widgets gone during queue processing loop. Stopping.", flush=True, ) self._is_active = False break try: self.text_widget.configure(state=tk.NORMAL) if self.text_widget.winfo_exists(): self.text_widget.insert(tk.END, msg + "\n", (level_name,)) self.text_widget.see(tk.END) if self.text_widget.winfo_exists(): self.text_widget.configure(state=tk.DISABLED) self.log_queue.task_done() except tk.TclError as e_tcl_inner: print( f"TkinterTextHandler TclError during widget update: {e_tcl_inner}. Attempting to stop handler.", flush=True, ) self._is_active = False break except Exception as e_inner: print( f"Unexpected error updating text widget: {e_inner}. Attempting to stop handler.", flush=True, ) self._is_active = False break except tk.TclError as e_tcl_outer: print( f"TkinterTextHandler TclError in _process_log_queue (outer): {e_tcl_outer}", flush=True, ) if self._after_id_log_processor: try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass except Exception as e_cancel: print( f"Error cancelling after_id in _process_log_queue (TclError outer): {e_cancel}", flush=True, ) self._after_id_log_processor = None self._is_active = False return except Exception as e_outer: print( f"Unexpected error in TkinterTextHandler._process_log_queue (outer): {e_outer}", flush=True, ) self._is_active = False return if ( self._is_active and hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): try: self._after_id_log_processor = self.root_tk_instance.after( self.queue_poll_interval_ms, self._process_log_queue ) except tk.TclError: print( "Debug: TkinterTextHandler._process_log_queue: Root destroyed. Cannot reschedule.", flush=True, ) self._after_id_log_processor = None self._is_active = False except Exception as e_reschedule: print( f"Error rescheduling _process_log_queue: {e_reschedule}", flush=True ) self._after_id_log_processor = None self._is_active = False elif self._after_id_log_processor: if ( hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass except Exception as e_cancel_final: print( f"Error cancelling after_id in final _process_log_queue check: {e_cancel_final}", flush=True, ) self._after_id_log_processor = None def close(self): """ Cleans up resources, like stopping the log queue processor. Called when the handler is removed or logging system shuts down. """ self._is_active = False if self._after_id_log_processor: if ( hasattr(self.root_tk_instance, "winfo_exists") and self.root_tk_instance.winfo_exists() ): try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: print( f"Debug: TclError during after_cancel in TkinterTextHandler.close (root might be gone or invalid).", flush=True, ) pass except Exception as e_cancel_close: print( f"Error cancelling after_id in close: {e_cancel_close}", flush=True, ) pass self._after_id_log_processor = None while not self.log_queue.empty(): try: self.log_queue.get_nowait() pass except QueueEmpty: break except Exception as e_q_drain: print(f"Error draining log queue during close: {e_q_drain}", flush=True) break super().close() _tkinter_handler_instance: Optional[TkinterTextHandler] = None def setup_logging( gui_log_widget: Optional[tk.Text] = None, root_tk_instance: Optional[tk.Tk] = None, logging_config_dict: Optional[Dict[str, Any]] = None, ): """ Sets up application-wide logging based on the provided configuration dictionary. If logging_config_dict is None, uses basic default settings (console only). """ global _tkinter_handler_instance if logging_config_dict is None: print( "Warning: No logging_config_dict provided to setup_logging. Using basic default console logging.", flush=True, ) logging_config_dict = { "default_root_level": logging.INFO, "specific_levels": {}, "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", "date_format": "%Y-%m-%d %H:%M:%S", "colors": {logging.INFO: "black"}, "queue_poll_interval_ms": 100, "enable_console": True, "enable_file": False, } root_log_level = logging_config_dict.get("default_root_level", logging.INFO) specific_levels = logging_config_dict.get("specific_levels", {}) log_format_str = logging_config_dict.get( "format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S") level_colors = logging_config_dict.get("colors", {}) queue_poll_interval_ms = logging_config_dict.get("queue_poll_interval_ms", 100) enable_console = logging_config_dict.get("enable_console", True) # MODIFIED: De-commented the line to extract 'enable_file' from the config dictionary. # WHY: The variable 'enable_file' was used later in an 'if' statement but was not defined because this line was commented out. # HOW: Removed the '#' at the beginning of the line. enable_file = logging_config_dict.get("enable_file", False) formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) root_logger = logging.getLogger() handlers_to_remove = [] for handler in root_logger.handlers[:]: if isinstance(handler, (logging.StreamHandler, logging.FileHandler)): handlers_to_remove.append(handler) elif handler is _tkinter_handler_instance: pass else: module_logger_internal = logging.getLogger(__name__) module_logger_internal.warning( f"Removing unknown logger handler during setup: {handler}" ) handlers_to_remove.append(handler) for handler in handlers_to_remove: try: handler.close() root_logger.removeHandler(handler) module_logger_internal = logging.getLogger(__name__) module_logger_internal.debug(f"Removed and closed old handler: {handler}") except Exception as e: module_logger_internal = logging.getLogger(__name__) module_logger_internal.error( f"Error removing/closing old handler {handler}: {e}", exc_info=False ) if _tkinter_handler_instance: if _tkinter_handler_instance in root_logger.handlers: try: root_logger.removeHandler(_tkinter_handler_instance) module_logger_internal = logging.getLogger(__name__) module_logger_internal.debug("Removed old TkinterTextHandler instance.") except Exception as e: module_logger_internal = logging.getLogger(__name__) module_logger_internal.error( f"Error removing old TkinterTextHandler instance: {e}", exc_info=False, ) try: _tkinter_handler_instance.close() module_logger_internal = logging.getLogger(__name__) module_logger_internal.debug("Closed old TkinterTextHandler instance.") except Exception as e: module_logger_internal = logging.getLogger(__name__) module_logger_internal.error( f"Error closing old TkinterTextHandler instance: {e}", exc_info=False ) finally: _tkinter_handler_instance = None root_logger.setLevel(root_log_level) module_logger_internal = logging.getLogger(__name__) module_logger_internal.debug( f"Root logger level set to {logging.getLevelName(root_logger.level)}" ) for logger_name, level in specific_levels.items(): named_logger = logging.getLogger(logger_name) named_logger.setLevel(level) module_logger_internal.debug( f"Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}" ) if enable_console: try: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) module_logger_internal.debug("Console logging enabled and handler added.") except Exception as e: module_logger_internal.error( f"Error adding console handler: {e}", exc_info=False ) # MODIFIED: Added the block to configure the file handler if enable_file is True. # WHY: This logic was intended to be here. # HOW: Moved the block from the original code and ensured it's under the 'if enable_file:' condition. if enable_file: try: from logging.handlers import RotatingFileHandler file_path = logging_config_dict.get("file_path", "app.log") file_max_bytes = logging_config_dict.get("file_max_bytes", 10 * 1024 * 1024) file_backup_count = logging_config_dict.get("file_backup_count", 5) file_handler = RotatingFileHandler( file_path, maxBytes=file_max_bytes, backupCount=file_backup_count ) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) module_logger_internal.debug( f"File logging enabled to '{file_path}' and handler added." ) except Exception as e: module_logger_internal.error( f"Error adding file handler to '{file_path}': {e}", exc_info=True ) if gui_log_widget and root_tk_instance: is_widget_valid = ( isinstance(gui_log_widget, (tk.Text, ScrolledText)) and hasattr(gui_log_widget, "winfo_exists") and gui_log_widget.winfo_exists() ) is_root_valid = ( hasattr(root_tk_instance, "winfo_exists") and root_tk_instance.winfo_exists() ) if not is_widget_valid: print( f"ERROR: GUI log widget is not a valid tk.Text/ScrolledText or does not exist: {type(gui_log_widget)}", flush=True, ) elif not is_root_valid: print( "WARNING: Root Tk instance provided to setup_logging does not exist.", flush=True, ) else: try: _tkinter_handler_instance = TkinterTextHandler( text_widget=gui_log_widget, root_tk_instance=root_tk_instance, level_colors=level_colors, queue_poll_interval_ms=queue_poll_interval_ms, ) _tkinter_handler_instance.setFormatter(formatter) root_logger.addHandler(_tkinter_handler_instance) module_logger_internal = logging.getLogger(__name__) module_logger_internal.info( "GUI logging handler (thread-safe) initialized and attached." ) except Exception as e: _tkinter_handler_instance = None print( f"ERROR: Failed to initialize TkinterTextHandler: {e}", flush=True ) elif gui_log_widget and not root_tk_instance: print( "WARNING: GUI log widget provided, but root Tk instance is missing. Cannot initialize GUI logger.", flush=True, ) elif not gui_log_widget and root_tk_instance: print( "DEBUG: Root Tk instance provided, but no GUI log widget. GUI logger not initialized.", flush=True, ) def get_logger(name: str) -> logging.Logger: """ Retrieves a logger instance by name. Module-level loggers should use `get_logger(__name__)`. """ return logging.getLogger(name) def shutdown_gui_logging(): """ Closes and removes the TkinterTextHandler instance from the root logger. This should be called before the Tkinter root window is destroyed. """ global _tkinter_handler_instance root_logger = logging.getLogger() if _tkinter_handler_instance: if _tkinter_handler_instance in root_logger.handlers: print( f"INFO: Closing and removing GUI logging handler ({_tkinter_handler_instance.name}).", flush=True, ) try: root_logger.removeHandler(_tkinter_handler_instance) except Exception as e: print(f"Error removing GUI handler from logger: {e}", flush=True) try: _tkinter_handler_instance.close() print("INFO: GUI logging handler has been shut down.", flush=True) except Exception as e: print(f"Error closing GUI logging handler: {e}", flush=True) finally: _tkinter_handler_instance = None else: print("DEBUG: No active GUI logging handler to shut down.", flush=True)