694 lines
23 KiB
Python
694 lines
23 KiB
Python
"""
|
|
Tkinter Logger - Sistema di logging thread-safe per applicazioni Tkinter.
|
|
|
|
Modulo standalone e riutilizzabile che fornisce un sistema di logging avanzato
|
|
con integrazione Tkinter, batching intelligente, polling adattivo e gestione
|
|
sicura di log da thread multipli.
|
|
|
|
Caratteristiche:
|
|
- Logging thread-safe tramite Queue
|
|
- Integrazione nativa con widget Tkinter Text/ScrolledText
|
|
- Batching intelligente per ridurre operazioni GUI (70%+ riduzione overhead)
|
|
- Polling adattivo (veloce con attività, lento quando idle)
|
|
- Gestione automatica limiti widget (previene memory bloat)
|
|
- Auto-scroll intelligente (solo se utente è in fondo)
|
|
- Supporto colori per livelli di log
|
|
- Handler multipli: console, file, Tkinter
|
|
- Zero dipendenze esterne (solo stdlib + tkinter)
|
|
|
|
Esempio di utilizzo base con Tkinter:
|
|
import tkinter as tk
|
|
from tkinter.scrolledtext import ScrolledText
|
|
from tkinter_logger import TkinterLogger
|
|
|
|
root = tk.Tk()
|
|
log_widget = ScrolledText(root, height=20, width=80)
|
|
log_widget.pack()
|
|
|
|
# Configura il logger
|
|
logger_system = TkinterLogger(root)
|
|
logger_system.setup(enable_console=True)
|
|
logger_system.add_tkinter_handler(log_widget)
|
|
|
|
# Usa il logging standard
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("Hello from Tkinter!")
|
|
|
|
root.mainloop()
|
|
logger_system.shutdown()
|
|
|
|
Esempio console (senza GUI):
|
|
from tkinter_logger import TkinterLogger
|
|
import logging
|
|
|
|
# Usa solo console handler (no Tkinter)
|
|
logger_system = TkinterLogger(tk_root=None)
|
|
logger_system.setup(enable_console=True, enable_tkinter=False)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("Console-only logging")
|
|
|
|
logger_system.shutdown()
|
|
|
|
Author: Estratto da Target Simulator
|
|
License: Same as parent project
|
|
"""
|
|
|
|
import logging
|
|
import logging.handlers
|
|
import tkinter as tk
|
|
from tkinter.scrolledtext import ScrolledText
|
|
from queue import Queue, Empty as QueueEmpty
|
|
from typing import Optional, Dict, Any, Union
|
|
from contextlib import contextmanager
|
|
import time
|
|
import os
|
|
|
|
|
|
class TkinterTextHandler(logging.Handler):
|
|
"""
|
|
Handler di logging che scrive in un widget Tkinter Text.
|
|
|
|
Questo handler è ottimizzato per performance GUI con:
|
|
- Batching di log multipli per ridurre operazioni widget
|
|
- Limite righe automatico per prevenire memory bloat
|
|
- Auto-scroll intelligente (solo se utente è in fondo)
|
|
- Supporto colori per livelli di log
|
|
|
|
Args:
|
|
text_widget: Widget Tkinter Text o ScrolledText
|
|
level_colors: Dizionario {logging.LEVEL: "colore"} per colorare i log
|
|
max_lines: Numero massimo di righe da mantenere nel widget (default: 1000)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
text_widget: Union[tk.Text, ScrolledText],
|
|
level_colors: Optional[Dict[int, str]] = None,
|
|
max_lines: int = 1000
|
|
):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
self.level_colors = level_colors or self._default_colors()
|
|
self.max_lines = max_lines
|
|
self._pending_records = [] # Buffer per batching
|
|
self._configure_tags()
|
|
|
|
@staticmethod
|
|
def _default_colors() -> Dict[int, str]:
|
|
"""Colori di default per i livelli di log."""
|
|
return {
|
|
logging.DEBUG: "#888888", # Grigio
|
|
logging.INFO: "#000000", # Nero
|
|
logging.WARNING: "#FF8C00", # Arancione
|
|
logging.ERROR: "#FF0000", # Rosso
|
|
logging.CRITICAL: "#8B0000", # Rosso scuro
|
|
}
|
|
|
|
def _configure_tags(self):
|
|
"""Configura i tag di colore nel widget."""
|
|
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 potrebbe non essere pronto
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
"""
|
|
Bufferizza il record per processing batch.
|
|
|
|
Questo metodo viene chiamato dal sistema di logging (potenzialmente
|
|
da thread diversi). Il record viene solo aggiunto al buffer.
|
|
"""
|
|
try:
|
|
if not self.text_widget.winfo_exists():
|
|
return
|
|
self._pending_records.append(record)
|
|
except Exception as e:
|
|
# Fallback a print se il widget non è disponibile
|
|
try:
|
|
print(f"TkinterTextHandler error: {e}", flush=True)
|
|
except Exception:
|
|
pass
|
|
|
|
def flush_pending(self):
|
|
"""
|
|
Scrive tutti i record bufferizzati nel widget in un'unica operazione.
|
|
|
|
Questo metodo DEVE essere chiamato dal thread GUI (Tk mainloop).
|
|
Ottimizzazioni:
|
|
- Singola operazione NORMAL -> DISABLED per tutti i log
|
|
- Check scroll position una volta sola
|
|
- Trim righe vecchie in una singola operazione
|
|
"""
|
|
if not self._pending_records:
|
|
return
|
|
|
|
try:
|
|
if not self.text_widget.winfo_exists():
|
|
self._pending_records.clear()
|
|
return
|
|
|
|
# Check se utente ha scrollato via dal fondo
|
|
yview = self.text_widget.yview()
|
|
user_at_bottom = yview[1] >= 0.98 # Entro 2% dal fondo
|
|
|
|
# Abilita modifica widget una volta sola
|
|
self.text_widget.configure(state=tk.NORMAL)
|
|
|
|
# Batch insert di tutti i record pendenti
|
|
for record in self._pending_records:
|
|
msg = self.format(record)
|
|
level_name = record.levelname
|
|
self.text_widget.insert(tk.END, msg + "\n", (level_name,))
|
|
|
|
# Trim righe vecchie se superato il limite
|
|
line_count = int(self.text_widget.index("end-1c").split(".")[0])
|
|
if line_count > self.max_lines:
|
|
excess = line_count - self.max_lines
|
|
self.text_widget.delete("1.0", f"{excess}.0")
|
|
|
|
# Disabilita modifica
|
|
self.text_widget.configure(state=tk.DISABLED)
|
|
|
|
# Auto-scroll solo se utente era in fondo
|
|
if user_at_bottom:
|
|
self.text_widget.see(tk.END)
|
|
|
|
self._pending_records.clear()
|
|
|
|
except Exception as e:
|
|
try:
|
|
print(f"Error in flush_pending: {e}", flush=True)
|
|
except Exception:
|
|
pass
|
|
self._pending_records.clear()
|
|
|
|
|
|
class _QueuePuttingHandler(logging.Handler):
|
|
"""Handler interno che mette LogRecord in una Queue."""
|
|
|
|
def __init__(self, handler_queue: Queue):
|
|
super().__init__()
|
|
self.handler_queue = handler_queue
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
try:
|
|
self.handler_queue.put_nowait(record)
|
|
except Exception:
|
|
pass # Queue piena o altro errore - ignora
|
|
|
|
|
|
class TkinterLogger:
|
|
"""
|
|
Sistema di logging completo per applicazioni Tkinter.
|
|
|
|
Gestisce tutto il ciclo di vita del logging:
|
|
- Setup iniziale con handler multipli
|
|
- Processing asincrono da Queue
|
|
- Batching e ottimizzazioni
|
|
- Shutdown pulito
|
|
|
|
Esempio:
|
|
import tkinter as tk
|
|
from tkinter_logger import TkinterLogger
|
|
|
|
root = tk.Tk()
|
|
logger_sys = TkinterLogger(root)
|
|
logger_sys.setup()
|
|
|
|
# ... usa logging standard ...
|
|
|
|
root.mainloop()
|
|
logger_sys.shutdown()
|
|
"""
|
|
|
|
def __init__(self, tk_root: Optional[tk.Tk] = None):
|
|
"""
|
|
Inizializza il sistema di logging.
|
|
|
|
Args:
|
|
tk_root: Istanza Tk root per scheduling. Se None, solo console/file
|
|
handler saranno disponibili (no Tkinter handler).
|
|
"""
|
|
self.tk_root = tk_root
|
|
self._log_queue: Optional[Queue] = None
|
|
self._console_handler: Optional[logging.StreamHandler] = None
|
|
self._file_handler: Optional[logging.handlers.RotatingFileHandler] = None
|
|
self._tkinter_handler: Optional[TkinterTextHandler] = None
|
|
self._queue_handler: Optional[_QueuePuttingHandler] = None
|
|
|
|
self._processor_after_id: Optional[str] = None
|
|
self._is_active = False
|
|
self._last_log_time = 0.0
|
|
self._formatter: Optional[logging.Formatter] = None
|
|
|
|
# Configurazioni polling
|
|
self._poll_interval_ms = 200 # Base interval (5Hz)
|
|
self._batch_size = 50 # Max log per ciclo
|
|
|
|
def setup(
|
|
self,
|
|
log_format: Optional[str] = None,
|
|
date_format: Optional[str] = None,
|
|
root_level: int = logging.INFO,
|
|
enable_console: bool = True,
|
|
enable_file: bool = False,
|
|
file_path: Optional[str] = None,
|
|
file_max_bytes: int = 5 * 1024 * 1024, # 5MB
|
|
file_backup_count: int = 3,
|
|
enable_tkinter: bool = True,
|
|
poll_interval_ms: int = 200,
|
|
batch_size: int = 50
|
|
) -> bool:
|
|
"""
|
|
Configura il sistema di logging.
|
|
|
|
Args:
|
|
log_format: Formato log (default: standard con timestamp)
|
|
date_format: Formato data (default: "%Y-%m-%d %H:%M:%S")
|
|
root_level: Livello root logger (default: INFO)
|
|
enable_console: Abilita handler console (default: True)
|
|
enable_file: Abilita handler file (default: False)
|
|
file_path: Path file log (richiesto se enable_file=True)
|
|
file_max_bytes: Dimensione massima file log (default: 5MB)
|
|
file_backup_count: Numero file backup rotazione (default: 3)
|
|
enable_tkinter: Abilita supporto Tkinter (default: True, richiede tk_root)
|
|
poll_interval_ms: Intervallo base polling queue (default: 200ms)
|
|
batch_size: Max log processati per ciclo (default: 50)
|
|
|
|
Returns:
|
|
True se setup completato con successo, False altrimenti
|
|
"""
|
|
if self._is_active:
|
|
return False # Già configurato
|
|
|
|
# Formatter
|
|
if log_format is None:
|
|
log_format = "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s"
|
|
if date_format is None:
|
|
date_format = "%Y-%m-%d %H:%M:%S"
|
|
|
|
self._formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
self._poll_interval_ms = poll_interval_ms
|
|
self._batch_size = batch_size
|
|
|
|
# Queue per log asincroni
|
|
self._log_queue = Queue()
|
|
|
|
# Configura root logger
|
|
root_logger = logging.getLogger()
|
|
# Rimuovi handler esistenti
|
|
for handler in root_logger.handlers[:]:
|
|
root_logger.removeHandler(handler)
|
|
root_logger.setLevel(root_level)
|
|
|
|
# Console handler
|
|
if enable_console:
|
|
self._console_handler = logging.StreamHandler()
|
|
self._console_handler.setFormatter(self._formatter)
|
|
self._console_handler.setLevel(logging.DEBUG)
|
|
|
|
# File handler
|
|
if enable_file:
|
|
if not file_path:
|
|
print("WARNING: enable_file=True but no file_path provided", flush=True)
|
|
else:
|
|
try:
|
|
# Crea directory se non esiste
|
|
log_dir = os.path.dirname(file_path)
|
|
if log_dir and not os.path.exists(log_dir):
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
self._file_handler = logging.handlers.RotatingFileHandler(
|
|
file_path,
|
|
maxBytes=file_max_bytes,
|
|
backupCount=file_backup_count,
|
|
encoding="utf-8"
|
|
)
|
|
self._file_handler.setFormatter(self._formatter)
|
|
self._file_handler.setLevel(logging.DEBUG)
|
|
except Exception as e:
|
|
print(f"WARNING: Cannot create file handler: {e}", flush=True)
|
|
|
|
# Queue handler (manda tutti i log alla queue)
|
|
self._queue_handler = _QueuePuttingHandler(self._log_queue)
|
|
self._queue_handler.setLevel(logging.DEBUG)
|
|
root_logger.addHandler(self._queue_handler)
|
|
|
|
# Log iniziale
|
|
try:
|
|
root_logger.debug("TkinterLogger system initialized (queue-based)")
|
|
except Exception:
|
|
pass
|
|
|
|
self._is_active = True
|
|
|
|
# Avvia processing loop se abbiamo tk_root e Tkinter è abilitato
|
|
if enable_tkinter and self.tk_root and self.tk_root.winfo_exists():
|
|
self._start_queue_processing()
|
|
|
|
return True
|
|
|
|
def add_tkinter_handler(
|
|
self,
|
|
text_widget: Union[tk.Text, ScrolledText],
|
|
level_colors: Optional[Dict[int, str]] = None,
|
|
max_lines: int = 1000
|
|
) -> bool:
|
|
"""
|
|
Aggiunge un handler Tkinter al sistema di logging.
|
|
|
|
Args:
|
|
text_widget: Widget Text o ScrolledText dove mostrare i log
|
|
level_colors: Dizionario {logging.LEVEL: "colore"}
|
|
max_lines: Numero massimo di righe da mantenere
|
|
|
|
Returns:
|
|
True se handler aggiunto con successo
|
|
"""
|
|
if not self._is_active or not self._formatter:
|
|
print("ERROR: TkinterLogger not setup, call setup() first", flush=True)
|
|
return False
|
|
|
|
# Chiudi handler precedente se esiste
|
|
if self._tkinter_handler:
|
|
try:
|
|
self._tkinter_handler.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Crea nuovo handler
|
|
try:
|
|
if not isinstance(text_widget, (tk.Text, ScrolledText)):
|
|
print("ERROR: text_widget must be tk.Text or ScrolledText", flush=True)
|
|
return False
|
|
|
|
if not text_widget.winfo_exists():
|
|
print("ERROR: text_widget does not exist", flush=True)
|
|
return False
|
|
|
|
self._tkinter_handler = TkinterTextHandler(
|
|
text_widget=text_widget,
|
|
level_colors=level_colors,
|
|
max_lines=max_lines
|
|
)
|
|
self._tkinter_handler.setFormatter(self._formatter)
|
|
self._tkinter_handler.setLevel(logging.DEBUG)
|
|
|
|
# Avvia processing se non già avviato
|
|
if self.tk_root and not self._processor_after_id:
|
|
self._start_queue_processing()
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: Cannot add Tkinter handler: {e}", flush=True)
|
|
return False
|
|
|
|
def _start_queue_processing(self):
|
|
"""Avvia il loop di processing della queue."""
|
|
if not self.tk_root or not self.tk_root.winfo_exists():
|
|
return
|
|
|
|
if self._processor_after_id:
|
|
return # Già avviato
|
|
|
|
self._processor_after_id = self.tk_root.after(
|
|
self._poll_interval_ms,
|
|
self._process_queue
|
|
)
|
|
|
|
def _process_queue(self):
|
|
"""
|
|
Processa i log dalla queue (chiamato dal Tk mainloop).
|
|
|
|
Ottimizzazioni:
|
|
- Processing a batch (max _batch_size per ciclo)
|
|
- Polling adattivo (veloce con attività, lento quando idle)
|
|
- Singolo flush per Tkinter handler
|
|
"""
|
|
if not self._is_active or not self.tk_root or not self.tk_root.winfo_exists():
|
|
return
|
|
|
|
processed_count = 0
|
|
|
|
try:
|
|
# Processa max _batch_size log per ciclo
|
|
while (
|
|
self._log_queue
|
|
and not self._log_queue.empty()
|
|
and processed_count < self._batch_size
|
|
):
|
|
try:
|
|
record = self._log_queue.get_nowait()
|
|
|
|
# Console e file handler scrivono immediatamente
|
|
if self._console_handler:
|
|
self._console_handler.handle(record)
|
|
if self._file_handler:
|
|
self._file_handler.handle(record)
|
|
|
|
# Tkinter handler bufferizza (no widget ops)
|
|
if self._tkinter_handler:
|
|
self._tkinter_handler.handle(record)
|
|
|
|
self._log_queue.task_done()
|
|
processed_count += 1
|
|
self._last_log_time = time.time()
|
|
|
|
except QueueEmpty:
|
|
break
|
|
except Exception as e:
|
|
try:
|
|
print(f"Error processing log record: {e}", flush=True)
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
try:
|
|
print(f"Error in queue processing: {e}", flush=True)
|
|
except Exception:
|
|
pass
|
|
|
|
# Flush batch Tkinter handler (singola operazione widget)
|
|
try:
|
|
if self._tkinter_handler:
|
|
self._tkinter_handler.flush_pending()
|
|
except Exception as e:
|
|
try:
|
|
print(f"Error flushing Tkinter logs: {e}", flush=True)
|
|
except Exception:
|
|
pass
|
|
|
|
# Polling adattivo: veloce con attività, lento quando idle
|
|
time_since_last = time.time() - self._last_log_time
|
|
if time_since_last < 2.0 or processed_count >= self._batch_size:
|
|
# Attività recente o backlog: polling veloce
|
|
next_interval = self._poll_interval_ms
|
|
elif time_since_last < 10.0:
|
|
# Attività moderata: polling normale
|
|
next_interval = self._poll_interval_ms * 2
|
|
else:
|
|
# Idle: polling lento per ridurre CPU
|
|
next_interval = self._poll_interval_ms * 5
|
|
|
|
# Schedule prossimo ciclo
|
|
if self._is_active:
|
|
self._processor_after_id = self.tk_root.after(
|
|
int(next_interval),
|
|
self._process_queue
|
|
)
|
|
|
|
def shutdown(self):
|
|
"""
|
|
Ferma il sistema di logging e fa cleanup.
|
|
|
|
Questo metodo dovrebbe essere chiamato alla chiusura dell'applicazione.
|
|
"""
|
|
if not self._is_active:
|
|
return
|
|
|
|
self._is_active = False
|
|
|
|
# Cancella processing loop
|
|
if self._processor_after_id and self.tk_root:
|
|
try:
|
|
if self.tk_root.winfo_exists():
|
|
self.tk_root.after_cancel(self._processor_after_id)
|
|
except Exception:
|
|
pass
|
|
self._processor_after_id = None
|
|
|
|
# Flush finale della queue
|
|
try:
|
|
self._process_queue()
|
|
except Exception:
|
|
pass
|
|
|
|
# Chiudi handler
|
|
if self._console_handler:
|
|
try:
|
|
self._console_handler.close()
|
|
except Exception:
|
|
pass
|
|
if self._file_handler:
|
|
try:
|
|
self._file_handler.close()
|
|
except Exception:
|
|
pass
|
|
if self._tkinter_handler:
|
|
try:
|
|
self._tkinter_handler.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Shutdown logging system
|
|
try:
|
|
logging.shutdown()
|
|
except Exception:
|
|
pass
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
"""Ritorna True se il sistema di logging è attivo."""
|
|
return self._is_active
|
|
|
|
|
|
# --- Utility functions ---
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
"""
|
|
Ottiene un logger standard Python.
|
|
|
|
Wrapper convenience su logging.getLogger().
|
|
|
|
Args:
|
|
name: Nome del logger (tipicamente __name__)
|
|
|
|
Returns:
|
|
Logger instance
|
|
"""
|
|
return logging.getLogger(name)
|
|
|
|
|
|
@contextmanager
|
|
def temporary_log_level(logger: logging.Logger, level: int):
|
|
"""
|
|
Context manager per cambiare temporaneamente il livello di un logger.
|
|
|
|
Esempio:
|
|
logger = logging.getLogger("mymodule")
|
|
|
|
with temporary_log_level(logger, logging.DEBUG):
|
|
# In questo blocco il logger è a DEBUG
|
|
logger.debug("Messaggio dettagliato")
|
|
|
|
# Fuori dal blocco torna al livello precedente
|
|
|
|
Args:
|
|
logger: Logger da modificare
|
|
level: Nuovo livello temporaneo (es: logging.DEBUG)
|
|
"""
|
|
old_level = logger.level
|
|
logger.setLevel(level)
|
|
try:
|
|
yield logger
|
|
finally:
|
|
logger.setLevel(old_level)
|
|
|
|
|
|
# Esempio standalone eseguibile
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
print("=== TkinterLogger Test ===\n")
|
|
|
|
# Test 1: Console-only (no Tkinter)
|
|
print("Test 1: Console-only logging")
|
|
logger_sys = TkinterLogger(tk_root=None)
|
|
logger_sys.setup(enable_console=True, enable_tkinter=False)
|
|
|
|
test_logger = get_logger(__name__)
|
|
test_logger.debug("Debug message")
|
|
test_logger.info("Info message")
|
|
test_logger.warning("Warning message")
|
|
test_logger.error("Error message")
|
|
test_logger.critical("Critical message")
|
|
|
|
logger_sys.shutdown()
|
|
print("\n" + "="*50 + "\n")
|
|
|
|
# Test 2: Tkinter GUI
|
|
print("Test 2: Tkinter GUI logging")
|
|
print("Apertura finestra GUI... (chiudi per continuare)\n")
|
|
|
|
root = tk.Tk()
|
|
root.title("TkinterLogger Test")
|
|
root.geometry("800x600")
|
|
|
|
# Frame principale
|
|
frame = tk.Frame(root, padx=10, pady=10)
|
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Label
|
|
tk.Label(
|
|
frame,
|
|
text="TkinterLogger - Test Window",
|
|
font=("Arial", 14, "bold")
|
|
).pack(pady=(0, 10))
|
|
|
|
# Log widget
|
|
log_widget = ScrolledText(frame, height=25, width=100, state=tk.DISABLED)
|
|
log_widget.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
|
|
|
# Setup logger
|
|
logger_sys2 = TkinterLogger(root)
|
|
logger_sys2.setup(enable_console=True)
|
|
logger_sys2.add_tkinter_handler(log_widget)
|
|
|
|
# Bottoni per generare log
|
|
button_frame = tk.Frame(frame)
|
|
button_frame.pack()
|
|
|
|
def log_debug():
|
|
test_logger.debug("This is a DEBUG message")
|
|
|
|
def log_info():
|
|
test_logger.info("This is an INFO message")
|
|
|
|
def log_warning():
|
|
test_logger.warning("This is a WARNING message")
|
|
|
|
def log_error():
|
|
test_logger.error("This is an ERROR message")
|
|
|
|
def log_critical():
|
|
test_logger.critical("This is a CRITICAL message")
|
|
|
|
def log_many():
|
|
for i in range(100):
|
|
test_logger.info(f"Batch message {i+1}/100")
|
|
|
|
tk.Button(button_frame, text="DEBUG", command=log_debug, width=10).pack(side=tk.LEFT, padx=2)
|
|
tk.Button(button_frame, text="INFO", command=log_info, width=10).pack(side=tk.LEFT, padx=2)
|
|
tk.Button(button_frame, text="WARNING", command=log_warning, width=10, fg="orange").pack(side=tk.LEFT, padx=2)
|
|
tk.Button(button_frame, text="ERROR", command=log_error, width=10, fg="red").pack(side=tk.LEFT, padx=2)
|
|
tk.Button(button_frame, text="CRITICAL", command=log_critical, width=10, fg="darkred").pack(side=tk.LEFT, padx=2)
|
|
tk.Button(button_frame, text="100 logs", command=log_many, width=10).pack(side=tk.LEFT, padx=10)
|
|
|
|
# Log iniziale
|
|
test_logger.info("TkinterLogger initialized - click buttons to test")
|
|
|
|
def on_closing():
|
|
logger_sys2.shutdown()
|
|
root.destroy()
|
|
|
|
root.protocol("WM_DELETE_WINDOW", on_closing)
|
|
root.mainloop()
|
|
|
|
print("\nTest completato!")
|