192 lines
6.9 KiB
Python
192 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() |