SXXXXXXX_RepoSync/reposync/utility/logger.py
2025-07-07 11:09:32 +02:00

199 lines
6.9 KiB
Python

# RepoSync/utility/logger.py
"""
Configures and provides access to the application-wide logging system.
This module sets up logging to the console, a file, and a thread-safe
Tkinter widget. It is designed to be configured once at application startup.
"""
import logging
import tkinter as tk
from logging.handlers import RotatingFileHandler
from queue import Queue, Empty as QueueEmpty
from tkinter.scrolledtext import ScrolledText
from typing import Optional, Dict, Any
# A global instance to hold the handler, managed by setup/shutdown functions.
_tkinter_handler_instance: Optional["TkinterTextHandler"] = None
class TkinterTextHandler(logging.Handler):
"""
A logging handler that directs records to a Tkinter Text widget.
It uses a thread-safe queue to avoid blocking the GUI and updates the
widget periodically via the Tkinter event loop (`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: Optional[str] = None
self._is_active = True
# Ensure widgets are valid before proceeding
if not (self.text_widget and self.text_widget.winfo_exists()):
print("Warning: TkinterTextHandler initialized with invalid text_widget.")
self._is_active = False
return
# Configure color tags on the text widget
for level, color in self.level_colors.items():
tag_name = logging.getLevelName(level)
try:
self.text_widget.tag_config(tag_name, foreground=color)
except tk.TclError as e:
print(f"Warning: Could not configure tag '{tag_name}': {e}")
# Start the queue processing loop
self._process_log_queue()
def emit(self, record: logging.LogRecord):
"""Adds a log record to the queue to be processed by the GUI thread."""
if self._is_active and self.root_tk_instance.winfo_exists():
msg = self.format(record)
self.log_queue.put_nowait((record.levelname, msg))
def _process_log_queue(self):
"""
Processes messages from the log queue and updates the text widget.
Schedules itself to run again.
"""
if not self._is_active or not self.root_tk_instance.winfo_exists():
self.close()
return
try:
# Process all available messages in the queue
while True:
level_name, msg = self.log_queue.get_nowait()
self.text_widget.configure(state=tk.NORMAL)
self.text_widget.insert(tk.END, msg + "\n", (level_name,))
self.text_widget.see(tk.END)
self.text_widget.configure(state=tk.DISABLED)
self.log_queue.task_done()
except QueueEmpty:
# Queue is empty, do nothing
pass
except Exception as e:
# Handle unexpected errors during widget update
print(f"Error updating log widget: {e}")
self.close() # Stop processing on error
return
# Schedule the next check
self._after_id = self.root_tk_instance.after(
self.queue_poll_interval_ms, self._process_log_queue
)
def close(self):
"""Stops the queue processor and cleans up resources."""
self._is_active = False
if self._after_id and self.root_tk_instance.winfo_exists():
self.root_tk_instance.after_cancel(self._after_id)
self._after_id = None
super().close()
def setup_logging(
gui_log_widget: Optional[tk.Text] = None,
root_tk_instance: Optional[tk.Tk] = None,
config: Optional[Dict[str, Any]] = None,
):
"""
Sets up application-wide logging based on a configuration dictionary.
Handles partial configurations by merging them with a set of defaults.
"""
global _tkinter_handler_instance
# 1. Define a complete default configuration
default_config = {
"level": logging.INFO,
"format": "%(asctime)s [%(levelname)-8s] (%(name)s) %(message)s",
"date_format": "%H:%M:%S",
"enable_console": True,
"enable_file": False,
"file_path": "RepoSync.log",
"file_max_bytes": 5 * 1024 * 1024,
"file_backup_count": 3,
"colors": {
logging.DEBUG: "gray",
logging.INFO: "black",
logging.WARNING: "orange",
logging.ERROR: "red",
logging.CRITICAL: "purple",
},
"queue_poll_interval_ms": 100,
}
# 2. If a user config is provided, update the defaults with it
if config:
default_config.update(config)
# From now on, use the merged 'default_config' which is guaranteed to be complete
final_config = default_config
formatter = logging.Formatter(
final_config["format"], datefmt=final_config["date_format"]
)
root_logger = logging.getLogger()
root_logger.setLevel(final_config["level"])
# Clear any existing handlers to avoid duplicates
for handler in root_logger.handlers[:]:
handler.close()
root_logger.removeHandler(handler)
_tkinter_handler_instance = None # Reset global instance
# --- Console Handler ---
if final_config["enable_console"]:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# --- File Handler (Rotating) ---
if final_config["enable_file"]:
file_handler = RotatingFileHandler(
final_config["file_path"],
maxBytes=final_config["file_max_bytes"],
backupCount=final_config["file_backup_count"],
encoding="utf-8",
)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# --- GUI Handler ---
if gui_log_widget and root_tk_instance:
if gui_log_widget.winfo_exists() and root_tk_instance.winfo_exists():
_tkinter_handler_instance = TkinterTextHandler(
text_widget=gui_log_widget,
root_tk_instance=root_tk_instance,
level_colors=final_config["colors"],
queue_poll_interval_ms=final_config["queue_poll_interval_ms"],
)
_tkinter_handler_instance.setFormatter(formatter)
root_logger.addHandler(_tkinter_handler_instance)
else:
print(
"Warning: Tkinter root or widget does not exist. GUI logger not created."
)
def shutdown_logging():
"""Closes and removes all logging handlers gracefully."""
logging.shutdown()