155 lines
7.9 KiB
Python
155 lines
7.9 KiB
Python
# FlightMonitor/utils/logger.py
|
|
import logging
|
|
import tkinter as tk
|
|
from tkinter.scrolledtext import ScrolledText
|
|
import threading # Per ottenere il main_thread
|
|
|
|
# Importazioni relative esplicite
|
|
from ..data import config as app_config
|
|
|
|
# ... (costanti come prima) ...
|
|
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
DEFAULT_LOG_LEVEL = logging.INFO
|
|
|
|
LOG_LEVEL_COLORS = {
|
|
logging.DEBUG: getattr(app_config, 'LOG_COLOR_DEBUG', 'gray'),
|
|
logging.INFO: getattr(app_config, 'LOG_COLOR_INFO', 'black'),
|
|
logging.WARNING: getattr(app_config, 'LOG_COLOR_WARNING', 'orange'),
|
|
logging.ERROR: getattr(app_config, 'LOG_COLOR_ERROR', 'red'),
|
|
logging.CRITICAL: getattr(app_config, 'LOG_COLOR_CRITICAL', 'red4')
|
|
}
|
|
|
|
class TkinterTextHandler(logging.Handler):
|
|
def __init__(self, text_widget: tk.Text):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
# Salva un riferimento al thread principale (GUI)
|
|
self.gui_thread = threading.current_thread() # O threading.main_thread() se si è sicuri che venga chiamato da lì
|
|
# threading.main_thread() è generalmente più sicuro per questo scopo.
|
|
|
|
# Verifica se text_widget è valido prima di configurare i tag
|
|
if self.text_widget and self.text_widget.winfo_exists():
|
|
for level, color in LOG_LEVEL_COLORS.items():
|
|
if color:
|
|
try:
|
|
self.text_widget.tag_config(logging.getLevelName(level), foreground=color)
|
|
except tk.TclError:
|
|
# Potrebbe accadere se il widget è in uno stato strano durante l'init
|
|
# anche se winfo_exists() dovrebbe coprirlo.
|
|
print(f"Warning: Could not config tag for {logging.getLevelName(level)} during TkinterTextHandler init.")
|
|
pass
|
|
else:
|
|
# Non fare nulla o logga un avviso se il widget non è valido all'inizio
|
|
# (anche se questo non dovrebbe accadere se setup_logging è chiamato correttamente)
|
|
print("Warning: TkinterTextHandler initialized with an invalid text_widget.")
|
|
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
msg = self.format(record)
|
|
level_name = record.levelname
|
|
|
|
# Controlla se siamo nel thread della GUI e se il widget esiste ancora ed è valido
|
|
# E, cosa più importante, se il mainloop è ancora "attivo" o se l'interprete è in shutdown
|
|
# Un modo semplice per verificarlo è vedere se il widget può essere acceduto senza errori Tcl.
|
|
# O, meglio, usare threading.main_thread().is_alive() è troppo generico.
|
|
# L'errore "main thread is not in main loop" è specifico di Tkinter.
|
|
# La cosa migliore è provare e catturare l'eccezione TclError.
|
|
|
|
if not (self.text_widget and self.text_widget.winfo_exists()):
|
|
# Se il widget non esiste più, non tentare di loggare su di esso.
|
|
# Potremmo voler loggare sulla console come fallback.
|
|
# print(f"Fallback to console (widget destroyed): {msg}")
|
|
return # Non fare nulla se il widget non esiste
|
|
|
|
try:
|
|
# Questo blocco è la parte critica. Se fallisce, il mainloop potrebbe non essere attivo.
|
|
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.text_widget.update_idletasks() # Questo potrebbe essere problematico se non nel mainloop
|
|
except tk.TclError as e:
|
|
# Questo errore ("main thread is not in main loop" o simili)
|
|
# indica che non possiamo più interagire con Tkinter.
|
|
# Logga sulla console come fallback.
|
|
# print(f"TkinterTextHandler TclError (GUI likely closing): {e}")
|
|
# print(f"Original log message (fallback to console): {msg}")
|
|
# Non stampare nulla qui per evitare confusione con il log della console già esistente
|
|
pass # Silenzia l'errore, il messaggio è già andato alla console
|
|
except RuntimeError as e:
|
|
# Ad esempio "Too early to create image" o altri errori di runtime di Tkinter
|
|
# durante la chiusura.
|
|
# print(f"TkinterTextHandler RuntimeError (GUI likely closing): {e}")
|
|
# print(f"Original log message (fallback to console): {msg}")
|
|
pass # Silenzia l'errore
|
|
except Exception as e:
|
|
# Altri errori imprevisti
|
|
print(f"Unexpected error in TkinterTextHandler.emit: {e}")
|
|
print(f"Original log message (fallback to console): {msg}")
|
|
|
|
# ... (setup_logging e get_logger come prima) ...
|
|
_tkinter_handler = None
|
|
|
|
def setup_logging(gui_log_widget: tk.Text = None):
|
|
global _tkinter_handler
|
|
log_level_str = getattr(app_config, 'LOG_LEVEL', 'INFO').upper()
|
|
log_level = getattr(logging, log_level_str, DEFAULT_LOG_LEVEL)
|
|
log_format = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT)
|
|
date_format = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT)
|
|
formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
|
|
# Usa il logger radice del nostro pacchetto se vogliamo isolarlo.
|
|
# Per ora, continuiamo con il root logger globale.
|
|
# logger_name_to_configure = 'flightmonitor' # o '' per il root globale
|
|
# current_logger = logging.getLogger(logger_name_to_configure)
|
|
current_logger = logging.getLogger() # Root logger
|
|
|
|
current_logger.setLevel(log_level)
|
|
|
|
# Rimuovi solo gli handler che potremmo aver aggiunto noi
|
|
# per evitare di rimuovere handler di altre librerie (se si usa il root logger globale)
|
|
# È più sicuro se TkinterTextHandler è l'unico handler che potremmo aggiungere più volte
|
|
# o se gestiamo esplicitamente quali handler rimuovere.
|
|
if _tkinter_handler and _tkinter_handler in current_logger.handlers:
|
|
current_logger.removeHandler(_tkinter_handler)
|
|
_tkinter_handler = None # Resetta così ne creiamo uno nuovo
|
|
|
|
# Console Handler (aggiungilo solo se non ne esiste già uno simile)
|
|
has_console_handler = any(isinstance(h, logging.StreamHandler) and \
|
|
not isinstance(h, TkinterTextHandler) for h in current_logger.handlers)
|
|
|
|
if not has_console_handler:
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setFormatter(formatter)
|
|
console_handler.setLevel(log_level)
|
|
current_logger.addHandler(console_handler)
|
|
# current_logger.info("Console logging handler added.") # Logga solo se aggiunto
|
|
# else:
|
|
# current_logger.debug("Console handler already exists.")
|
|
|
|
|
|
if gui_log_widget:
|
|
_tkinter_handler = TkinterTextHandler(gui_log_widget)
|
|
_tkinter_handler.setFormatter(formatter)
|
|
_tkinter_handler.setLevel(log_level)
|
|
current_logger.addHandler(_tkinter_handler)
|
|
if current_logger.isEnabledFor(logging.INFO): # Logga solo se il livello lo permette
|
|
current_logger.info("GUI logging handler initialized/updated.")
|
|
# else:
|
|
# if current_logger.isEnabledFor(logging.INFO):
|
|
# current_logger.info("Console logging active (no GUI widget provided for logging).")
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
# Se current_logger in setup_logging fosse 'flightmonitor', allora qui:
|
|
# if not name.startswith(logger_name_to_configure + '.'):
|
|
# qualified_name = logger_name_to_configure + '.' + name.lstrip('.')
|
|
# else:
|
|
# qualified_name = name
|
|
# return logging.getLogger(qualified_name)
|
|
return logging.getLogger(name)
|
|
|
|
# Il blocco if __name__ == "__main__" per il test di logger.py è meglio rimuoverlo o
|
|
# commentarlo pesantemente, dato che dipende da una struttura di pacchetto per
|
|
# l'import di app_config e ora ha una logica più complessa.
|
|
# Testare attraverso l'esecuzione dell'applicazione principale è più affidabile. |