526 lines
20 KiB
Python
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)
|