SXXXXXXX_PyMsc/pymsc/utils/logger.py

123 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
import logging
import queue
import tkinter as tk
from logging.handlers import QueueHandler, QueueListener
from tkinter.scrolledtext import ScrolledText
class TkinterLogHandler(logging.Handler):
"""
Redirects logs to a Tkinter text widget in a thread-safe manner.
"""
def __init__(self, text_widget: ScrolledText):
super().__init__()
self.text_widget = text_widget
def emit(self, record: logging.LogRecord):
"""
Thread-safe emit method using Tkinter's 'after' to schedule
updates on the main loop.
"""
try:
message = self.format(record)
# Use level first letter as tag (D, I, W, E) for coloring
tag = record.levelname[0]
# Schedule the actual UI update to run on the main thread
self.text_widget.after(0, self._safe_append, message, tag)
except Exception:
self.handleError(record)
def _safe_append(self, message: str, tag: str):
"""
Executes the widget modification. This method is always
called from the main thread.
"""
self.text_widget.configure(state='normal')
self.text_widget.insert(tk.END, message + '\n', tag)
self.text_widget.configure(state='disabled')
self.text_widget.yview(tk.END)
class CustomLogger:
"""
Singleton logger manager for PyMsc.
Handles multi-threaded logging to console, file, and Tkinter UI.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(CustomLogger, cls).__new__(cls)
return cls._instance
def __init__(self, log_file: str = 'pymsc.log', use_console: bool = True):
if hasattr(self, 'initialized'):
return
self.initialized = True
self.log_queue = queue.Queue()
# Main logger configuration
self.logger = logging.getLogger('PyMsc')
self.logger.setLevel(logging.DEBUG)
# Standard formatter
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s',
datefmt='%y%m%d%H%M%S'
)
# 1. Console Handler
if use_console:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
# 2. File Handler
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
self.listener = None
def setup_tkinter_logging(self, parent_frame: tk.Frame):
"""
Initializes the ScrolledText widget and starts the QueueListener
to capture logs from all threads.
"""
# UI Component for logs
self.scrolled_text = ScrolledText(
parent_frame,
state='disabled',
height=5,
bg="black",
fg="white",
font=("Consolas", 8)
)
self.scrolled_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# Define color tags matching record.levelname[0]
self.scrolled_text.tag_configure('D', foreground='lightgray') # Debug
self.scrolled_text.tag_configure('I', foreground='white') # Info
self.scrolled_text.tag_configure('W', foreground='yellow') # Warning
self.scrolled_text.tag_configure('E', foreground='red') # Error
# Attach QueueHandler to the logger to redirect all logs to the queue
queue_handler = QueueHandler(self.log_queue)
self.logger.addHandler(queue_handler)
# Create the custom Tkinter handler
tk_handler = TkinterLogHandler(self.scrolled_text)
# QueueListener will pull from queue and call tk_handler.emit in its thread
self.listener = QueueListener(self.log_queue, tk_handler)
self.listener.start()
def get_logger(self) -> logging.Logger:
"""Returns the logger instance."""
return self.logger
def stop_listener(self):
"""Stops the queue listener thread safely."""
if self.listener:
self.listener.stop()