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': '📺' } # 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) 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): # 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 = LOG_EMOJIS.get(level_name, '') category_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 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) 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 ) app_handler.setLevel(log_level) app_handler.setFormatter(file_formatter) root_logger.addHandler(app_handler) # Wenn Debug-Modus aktiv, Konfiguration loggen if debug_mode: root_logger.debug(f"🐞 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) 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 ) 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 ) 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") app_logger.info("=" * 50) app_logger.info("🚀 MYP (Manage Your Printers) wird gestartet...") app_logger.info(f"📂 Log-Verzeichnis: {LOG_DIR}") app_logger.info(f"📊 Log-Level: {LOG_LEVEL}") app_logger.info(f"💻 Betriebssystem: {platform.system()} {platform.release()}") app_logger.info(f"🌐 Hostname: {socket.gethostname()}") app_logger.info(f"📅 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 logger.debug(f"🌐 HTTP-Anfrage: {request.method} {request.path}") logger.debug(f"📡 Remote-Adresse: {request.remote_addr}") logger.debug(f"🧩 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"📋 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"🔍 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 = "✅" if response.status_code < 400 else "❌" logger.debug(f"{status_emoji} HTTP-Antwort: {response.status_code}") if duration_ms is not None: logger.debug(f"⏱️ 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" logger.debug(f"📦 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: if duration_ms > 1000: # Länger als 1 Sekunde logger.warning(f"⏱️ Langsame Ausführung: {name} - {duration_ms:.2f} ms") else: logger.debug(f"⏱️ Ausführungszeit: {name} - {duration_ms:.2f} ms") return result return wrapper if func: return decorator(func) return decorator