SXXXXXXX_ScenarioSimulator/scenario_simulator/utils/logger.py
2025-09-30 10:58:10 +02:00

159 lines
6.5 KiB
Python

# FlightMonitor/utils/logger.py
import logging
import logging.handlers # For RotatingFileHandler
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from queue import Queue, Empty as QueueEmpty
from typing import Optional, Dict, Any
# --- Module-level globals for the centralized logging queue system ---
_global_log_queue: Optional[Queue[logging.LogRecord]] = None
_actual_console_handler: Optional[logging.StreamHandler] = None
_actual_file_handler: Optional[logging.handlers.RotatingFileHandler] = None
_actual_tkinter_handler: Optional["TkinterTextHandler"] = None
_log_processor_after_id: Optional[str] = None
_logging_system_active: bool = False
_tk_root_instance_for_processing: Optional[tk.Tk] = None
_base_formatter: Optional[logging.Formatter] = None
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS = 100
class TkinterTextHandler(logging.Handler):
"""
A logging handler that directs log messages to a Tkinter Text widget.
This handler is called directly from the GUI thread's processing loop.
"""
def __init__(self, text_widget: tk.Text, level_colors: Dict[int, str]):
super().__init__()
self.text_widget = text_widget
self.level_colors = level_colors
self._configure_tags()
def _configure_tags(self):
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:
pass # Widget might not be ready
def emit(self, record: logging.LogRecord):
try:
if not self.text_widget.winfo_exists():
return
msg = self.format(record)
level_name = record.levelname
self.text_widget.configure(state=tk.NORMAL)
self.text_widget.insert(tk.END, msg + "\n", (level_name,))
self.text_widget.configure(state=tk.DISABLED)
self.text_widget.see(tk.END)
except Exception as e:
print(f"Error in TkinterTextHandler.emit: {e}", flush=True)
class QueuePuttingHandler(logging.Handler):
"""
A simple handler that puts any received LogRecord into a global queue.
"""
def __init__(self, handler_queue: Queue[logging.LogRecord]):
super().__init__()
self.handler_queue = handler_queue
def emit(self, record: logging.LogRecord):
self.handler_queue.put_nowait(record)
def _process_global_log_queue():
"""
GUI Thread: Periodically processes LogRecords from the _global_log_queue
and dispatches them to the actual configured handlers.
"""
global _logging_system_active, _log_processor_after_id
if not _logging_system_active or not _tk_root_instance_for_processing or not _tk_root_instance_for_processing.winfo_exists():
return
try:
while _global_log_queue and not _global_log_queue.empty():
record = _global_log_queue.get_nowait()
if _actual_console_handler:
_actual_console_handler.handle(record)
if _actual_file_handler:
_actual_file_handler.handle(record)
if _actual_tkinter_handler:
_actual_tkinter_handler.handle(record)
_global_log_queue.task_done()
except QueueEmpty:
pass
except Exception as e:
print(f"Error in log processing queue: {e}", flush=True)
finally:
if _logging_system_active:
_log_processor_after_id = _tk_root_instance_for_processing.after(
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue
)
def setup_basic_logging(root_tk_instance_for_processor: tk.Tk, logging_config_dict: Optional[Dict[str, Any]] = None):
global _global_log_queue, _actual_console_handler, _actual_file_handler, _logging_system_active
global _tk_root_instance_for_processing, _log_processor_after_id, _base_formatter
if _logging_system_active: return
if logging_config_dict is None: logging_config_dict = {}
log_format_str = logging_config_dict.get("format", "% (asctime)s [%(levelname)-8s] %(name)-25s : %(message)s")
log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S")
_base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str)
_global_log_queue = Queue()
_tk_root_instance_for_processing = root_tk_instance_for_processor
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]: root_logger.removeHandler(handler)
root_logger.setLevel(logging_config_dict.get("default_root_level", logging.INFO))
if logging_config_dict.get("enable_console", True):
_actual_console_handler = logging.StreamHandler()
_actual_console_handler.setFormatter(_base_formatter)
_actual_console_handler.setLevel(logging.DEBUG)
queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue)
queue_putter.setLevel(logging.DEBUG)
root_logger.addHandler(queue_putter)
_logging_system_active = True
_log_processor_after_id = _tk_root_instance_for_processing.after(GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue)
def add_tkinter_handler(gui_log_widget: tk.Text, logging_config_dict: Dict[str, Any]):
global _actual_tkinter_handler, _base_formatter
if not _logging_system_active or not _base_formatter: return
if _actual_tkinter_handler: _actual_tkinter_handler.close()
if isinstance(gui_log_widget, (tk.Text, ScrolledText)) and gui_log_widget.winfo_exists():
level_colors = logging_config_dict.get("colors", {})
_actual_tkinter_handler = TkinterTextHandler(text_widget=gui_log_widget, level_colors=level_colors)
_actual_tkinter_handler.setFormatter(_base_formatter)
_actual_tkinter_handler.setLevel(logging.DEBUG)
logging.getLogger(__name__).info("Tkinter log handler added successfully.")
else:
print("ERROR: GUI log widget invalid, cannot add TkinterTextHandler.", flush=True)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def shutdown_logging_system():
global _logging_system_active, _log_processor_after_id
if not _logging_system_active: return
_logging_system_active = False
if _log_processor_after_id and _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists():
_tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
# Final flush of the queue
_process_global_log_queue()
logging.shutdown()