SXXXXXXX_FlightMonitor/flightmonitor/utils/logger.py
2025-05-27 07:33:08 +02:00

526 lines
20 KiB
Python

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