S1005403_RisCC/target_simulator/utils/logger.py

266 lines
8.9 KiB
Python

# scenario_simulator/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
from contextlib import contextmanager
from logging import Logger
from target_simulator.utils.config_manager import ConfigManager
import os
import json
# Module-level logger for utils.logging helpers
logger = logging.getLogger(__name__)
# --- 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)
logger.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)
@contextmanager
def temporary_log_level(logger: Logger, level: int):
"""Context manager to temporarily set a logger's level.
Usage:
with temporary_log_level(logging.getLogger('some.name'), logging.DEBUG):
# inside this block the logger will be DEBUG
...
"""
old_level = logger.level
logger.setLevel(level)
try:
yield
finally:
logger.setLevel(old_level)
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()
def apply_saved_logger_levels():
"""Apply saved logger levels from ConfigManager at startup.
Reads `general.logger_panel.saved_levels` from settings.json and sets
each configured logger to the saved level name.
"""
try:
cfg = ConfigManager()
except Exception:
return
try:
# Prefer a dedicated logger_prefs.json next to the main settings file
prefs_path = None
cfg_path = getattr(cfg, "filepath", None)
if cfg_path:
prefs_path = os.path.join(os.path.dirname(cfg_path), "logger_prefs.json")
saved = {}
if prefs_path and os.path.exists(prefs_path):
try:
with open(prefs_path, "r", encoding="utf-8") as f:
jp = json.load(f)
if isinstance(jp, dict):
saved = jp.get("saved_levels", {}) or {}
except Exception:
saved = {}
else:
# Fallback to settings.json general.logger_panel
try:
gen = cfg.get_general_settings()
lp = gen.get("logger_panel", {}) if isinstance(gen, dict) else {}
saved = lp.get("saved_levels", {}) if isinstance(lp, dict) else {}
except Exception:
saved = {}
for name, lvl_name in (saved or {}).items():
try:
lvl_val = logging.getLevelName(lvl_name)
if isinstance(lvl_val, int):
logging.getLogger(name).setLevel(lvl_val)
except Exception:
pass
except Exception:
pass