import logging import logging.handlers import os import sys import time import platform import socket from typing import Dict, Optional, Any from datetime import datetime from config.settings import ( LOG_DIR, LOG_SUBDIRS, LOG_LEVEL, LOG_FORMAT, LOG_DATE_FORMAT, get_log_file, ensure_log_directories ) # Dictionary zur Speicherung der konfigurierten Logger _loggers: Dict[str, logging.Logger] = {} # ANSI-Farbcodes für Log-Level ANSI_COLORS = { 'RESET': '\033[0m', 'BOLD': '\033[1m', 'BLACK': '\033[30m', 'RED': '\033[31m', 'GREEN': '\033[32m', 'YELLOW': '\033[33m', 'BLUE': '\033[34m', 'MAGENTA': '\033[35m', 'CYAN': '\033[36m', 'WHITE': '\033[37m', 'BG_RED': '\033[41m', 'BG_GREEN': '\033[42m', 'BG_YELLOW': '\033[43m', 'BG_BLUE': '\033[44m' } # Emojis für verschiedene Log-Level und Kategorien LOG_EMOJIS = { 'DEBUG': '🔍', 'INFO': 'ℹ️', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'app': '🖥️', 'scheduler': '⏱️', 'auth': '🔐', 'jobs': '🖨️', 'printers': '🔧', 'errors': '💥', 'user': '👤', 'kiosk': '📺' } # ASCII-Fallback für Emojis bei Encoding-Problemen EMOJI_FALLBACK = { '🔍': '[DEBUG]', 'ℹ️': '[INFO]', '⚠️': '[WARN]', '❌': '[ERROR]', '🔥': '[CRIT]', '🖥️': '[APP]', '⏱️': '[SCHED]', '🔐': '[AUTH]', '🖨️': '[JOBS]', '🔧': '[PRINT]', '💥': '[ERR]', '👤': '[USER]', '📺': '[KIOSK]', '🐞': '[BUG]', '🚀': '[START]', '📂': '[FOLDER]', '📊': '[CHART]', '💻': '[PC]', '🌐': '[WEB]', '📅': '[TIME]', '📡': '[SIGNAL]', '🧩': '[CONTENT]', '📋': '[HEADER]', '✅': '[OK]', '📦': '[SIZE]' } def safe_emoji(emoji: str) -> str: """Gibt ein Emoji zurück oder einen ASCII-Fallback bei Encoding-Problemen.""" try: # Erste Priorität: Teste, ob das Emoji dargestellt werden kann test_encoding = sys.stdout.encoding or 'utf-8' emoji.encode(test_encoding) # Zweite Prüfung: Windows-spezifische cp1252-Codierung if os.name == 'nt': try: emoji.encode('cp1252') except UnicodeEncodeError: # Wenn cp1252 fehlschlägt, verwende Fallback return EMOJI_FALLBACK.get(emoji, '[?]') return emoji except (UnicodeEncodeError, LookupError, AttributeError): return EMOJI_FALLBACK.get(emoji, '[?]') # Prüfen, ob das Terminal ANSI-Farben unterstützt def supports_color() -> bool: """Prüft, ob das Terminal ANSI-Farben unterstützt.""" if os.name == 'nt': try: import ctypes kernel32 = ctypes.windll.kernel32 # Aktiviere VT100-Unterstützung unter Windows kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) # Setze Console-Output auf UTF-8 für bessere Emoji-Unterstützung try: kernel32.SetConsoleOutputCP(65001) # UTF-8 except: pass # Versuche UTF-8-Encoding für Emojis zu setzen try: import locale locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') except: try: # Fallback für deutsche Lokalisierung locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') except: pass return True except: return False else: return sys.stdout.isatty() USE_COLORS = supports_color() class ColoredFormatter(logging.Formatter): """Formatter, der Farben und Emojis für Logs hinzufügt.""" level_colors = { 'DEBUG': ANSI_COLORS['CYAN'], 'INFO': ANSI_COLORS['GREEN'], 'WARNING': ANSI_COLORS['YELLOW'], 'ERROR': ANSI_COLORS['RED'], 'CRITICAL': ANSI_COLORS['BG_RED'] + ANSI_COLORS['WHITE'] + ANSI_COLORS['BOLD'] } def format(self, record): try: # Basis-Format erstellen log_fmt = LOG_FORMAT date_fmt = LOG_DATE_FORMAT # Emoji dem Level und der Kategorie hinzufügen level_name = record.levelname category_name = record.name.split('.')[-1] if '.' in record.name else record.name level_emoji = safe_emoji(LOG_EMOJIS.get(level_name, '')) category_emoji = safe_emoji(LOG_EMOJIS.get(category_name, '')) # Record-Objekt modifizieren (aber temporär) original_levelname = record.levelname original_name = record.name # Emojis hinzufügen record.levelname = f"{level_emoji} {level_name}" record.name = f"{category_emoji} {category_name}" # Farbe hinzufügen wenn unterstützt if USE_COLORS: level_color = self.level_colors.get(original_levelname, ANSI_COLORS['RESET']) record.levelname = f"{level_color}{record.levelname}{ANSI_COLORS['RESET']}" record.name = f"{ANSI_COLORS['BOLD']}{record.name}{ANSI_COLORS['RESET']}" # Formatieren result = super().format(record) # Originale Werte wiederherstellen record.levelname = original_levelname record.name = original_name return result except (UnicodeEncodeError, UnicodeDecodeError, AttributeError) as e: # Fallback bei Unicode-Problemen: Verwende nur ASCII-Text original_levelname = record.levelname original_name = record.name # Emojis durch ASCII-Fallbacks ersetzen level_fallback = EMOJI_FALLBACK.get(LOG_EMOJIS.get(original_levelname, ''), '[LOG]') category_name = record.name.split('.')[-1] if '.' in record.name else record.name category_fallback = EMOJI_FALLBACK.get(LOG_EMOJIS.get(category_name, ''), '[CAT]') record.levelname = f"{level_fallback} {original_levelname}" record.name = f"{category_fallback} {category_name}" # Basis-Formatierung ohne Farben result = super().format(record) # Originale Werte wiederherstellen record.levelname = original_levelname record.name = original_name return result class DebugInfoFilter(logging.Filter): """Filter, der Debug-Informationen zu jedem Log-Eintrag hinzufügt.""" def __init__(self, add_hostname=True, add_process_info=True): super().__init__() self.add_hostname = add_hostname self.add_process_info = add_process_info self.hostname = socket.gethostname() if add_hostname else None self.pid = os.getpid() if add_process_info else None def filter(self, record): # Debug-Informationen hinzufügen if self.add_hostname and not hasattr(record, 'hostname'): record.hostname = self.hostname if self.add_process_info and not hasattr(record, 'pid'): record.pid = self.pid # Zusätzliche Infos für DEBUG-Level if record.levelno == logging.DEBUG: # Funktionsname und Zeilennummer hervorheben if USE_COLORS: record.funcName = f"{ANSI_COLORS['CYAN']}{record.funcName}{ANSI_COLORS['RESET']}" record.lineno = f"{ANSI_COLORS['CYAN']}{record.lineno}{ANSI_COLORS['RESET']}" return True def setup_logging(debug_mode: bool = False): """ Initialisiert das Logging-System und erstellt alle erforderlichen Verzeichnisse. Args: debug_mode: Wenn True, wird das Log-Level auf DEBUG gesetzt """ ensure_log_directories() # Log-Level festlegen log_level = logging.DEBUG if debug_mode else getattr(logging, LOG_LEVEL) # Root-Logger konfigurieren root_logger = logging.getLogger() root_logger.setLevel(log_level) # Alle Handler entfernen for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) # Formatter erstellen (mit und ohne Farben) colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT) file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT) # Filter für zusätzliche Debug-Informationen debug_filter = DebugInfoFilter() # Console Handler für alle Logs console_handler = logging.StreamHandler() console_handler.setLevel(log_level) console_handler.setFormatter(colored_formatter) console_handler.addFilter(debug_filter) # Windows PowerShell UTF-8 Encoding-Unterstützung if os.name == 'nt' and hasattr(console_handler.stream, 'reconfigure'): try: console_handler.stream.reconfigure(encoding='utf-8') except: pass root_logger.addHandler(console_handler) # File Handler für allgemeine App-Logs app_log_file = get_log_file("app") app_handler = logging.handlers.RotatingFileHandler( app_log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8' ) app_handler.setLevel(log_level) app_handler.setFormatter(file_formatter) root_logger.addHandler(app_handler) # Wenn Debug-Modus aktiv, Konfiguration loggen if debug_mode: bug_emoji = safe_emoji("🐞") root_logger.debug(f"{bug_emoji} Debug-Modus aktiviert - Ausführliche Logs werden generiert") def get_logger(category: str) -> logging.Logger: """ Gibt einen konfigurierten Logger für eine bestimmte Kategorie zurück. Args: category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors) Returns: logging.Logger: Konfigurierter Logger """ if category in _loggers: return _loggers[category] # Logger erstellen logger = logging.getLogger(f"myp.{category}") logger.setLevel(getattr(logging, LOG_LEVEL)) # Verhindere doppelte Logs durch Parent-Logger logger.propagate = False # Formatter erstellen (mit und ohne Farben) colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT) file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT) # Filter für zusätzliche Debug-Informationen debug_filter = DebugInfoFilter() # Console Handler console_handler = logging.StreamHandler() console_handler.setLevel(getattr(logging, LOG_LEVEL)) console_handler.setFormatter(colored_formatter) console_handler.addFilter(debug_filter) # Windows PowerShell UTF-8 Encoding-Unterstützung if os.name == 'nt' and hasattr(console_handler.stream, 'reconfigure'): try: console_handler.stream.reconfigure(encoding='utf-8') except: pass logger.addHandler(console_handler) # File Handler für spezifische Kategorie log_file = get_log_file(category) file_handler = logging.handlers.RotatingFileHandler( log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8' ) file_handler.setLevel(getattr(logging, LOG_LEVEL)) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) # Error-Logs zusätzlich in errors.log schreiben if category != "errors": error_log_file = get_log_file("errors") error_handler = logging.handlers.RotatingFileHandler( error_log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8' ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(file_formatter) logger.addHandler(error_handler) _loggers[category] = logger return logger def log_startup_info(): """Loggt Startup-Informationen.""" app_logger = get_logger("app") rocket_emoji = safe_emoji("🚀") folder_emoji = safe_emoji("📂") chart_emoji = safe_emoji("📊") computer_emoji = safe_emoji("💻") globe_emoji = safe_emoji("🌐") calendar_emoji = safe_emoji("📅") app_logger.info("=" * 50) app_logger.info(f"{rocket_emoji} MYP (Manage Your Printers) wird gestartet...") app_logger.info(f"{folder_emoji} Log-Verzeichnis: {LOG_DIR}") app_logger.info(f"{chart_emoji} Log-Level: {LOG_LEVEL}") app_logger.info(f"{computer_emoji} Betriebssystem: {platform.system()} {platform.release()}") app_logger.info(f"{globe_emoji} Hostname: {socket.gethostname()}") app_logger.info(f"{calendar_emoji} Startzeit: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") app_logger.info("=" * 50) # Hilfsfunktionen für das Debugging def debug_request(logger: logging.Logger, request): """ Loggt detaillierte Informationen über eine HTTP-Anfrage. Args: logger: Logger-Instanz request: Flask-Request-Objekt """ if logger.level > logging.DEBUG: return web_emoji = safe_emoji("🌐") signal_emoji = safe_emoji("📡") puzzle_emoji = safe_emoji("🧩") clipboard_emoji = safe_emoji("📋") search_emoji = safe_emoji("🔍") logger.debug(f"{web_emoji} HTTP-Anfrage: {request.method} {request.path}") logger.debug(f"{signal_emoji} Remote-Adresse: {request.remote_addr}") logger.debug(f"{puzzle_emoji} Inhaltstyp: {request.content_type}") # Nur relevante Headers ausgeben important_headers = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Authorization'] headers = {k: v for k, v in request.headers.items() if k in important_headers} if headers: logger.debug(f"{clipboard_emoji} Wichtige Headers: {headers}") # Request-Parameter (max. 1000 Zeichen) if request.args: args_str = str(request.args) if len(args_str) > 1000: args_str = args_str[:997] + "..." logger.debug(f"{search_emoji} URL-Parameter: {args_str}") def debug_response(logger: logging.Logger, response, duration_ms: float = None): """ Loggt detaillierte Informationen über eine HTTP-Antwort. Args: logger: Logger-Instanz response: Flask-Response-Objekt duration_ms: Verarbeitungsdauer in Millisekunden (optional) """ if logger.level > logging.DEBUG: return status_emoji = safe_emoji("✅") if response.status_code < 400 else safe_emoji("❌") logger.debug(f"{status_emoji} HTTP-Antwort: {response.status_code}") if duration_ms is not None: timer_emoji = safe_emoji("⏱️") logger.debug(f"{timer_emoji} Verarbeitungsdauer: {duration_ms:.2f} ms") content_length = response.content_length or 0 if content_length > 0: size_str = f"{content_length / 1024:.1f} KB" if content_length > 1024 else f"{content_length} Bytes" package_emoji = safe_emoji("📦") logger.debug(f"{package_emoji} Antwortgröße: {size_str}") def measure_execution_time(func=None, logger=None, task_name=None): """ Dekorator, der die Ausführungszeit einer Funktion misst und loggt. Args: func: Die zu dekorierende Funktion logger: Logger-Instanz (optional) task_name: Name der Aufgabe für das Logging (optional) Returns: Dekorierte Funktion """ from functools import wraps def decorator(f): @wraps(f) def wrapper(*args, **kwargs): start_time = time.time() result = f(*args, **kwargs) end_time = time.time() duration_ms = (end_time - start_time) * 1000 name = task_name or f.__name__ if logger: timer_emoji = safe_emoji('⏱️') if duration_ms > 1000: # Länger als 1 Sekunde logger.warning(f"{timer_emoji} Langsame Ausführung: {name} - {duration_ms:.2f} ms") else: logger.debug(f"{timer_emoji} Ausführungszeit: {name} - {duration_ms:.2f} ms") return result return wrapper if func: return decorator(func) return decorator