392 lines
14 KiB
Python
392 lines
14 KiB
Python
"""
|
|
Debug-Utilities für die MYP-Anwendung
|
|
Hilft bei der Diagnose und Behebung von Problemen in der Anwendung
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import json
|
|
import traceback
|
|
import inspect
|
|
from datetime import datetime
|
|
from functools import wraps
|
|
import logging
|
|
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
|
|
|
|
from utils.logging_config import get_logger
|
|
|
|
# Logger für dieses Modul erstellen
|
|
debug_logger = get_logger("app")
|
|
|
|
# Konstanten für Formatierung
|
|
DEBUG_SEPARATOR = "=" * 60
|
|
DEBUG_SUBSEPARATOR = "-" * 60
|
|
|
|
class DebugLevel:
|
|
"""Enum für Debug-Level"""
|
|
MINIMAL = 0 # Nur kritische Fehler
|
|
NORMAL = 1 # Standardfehler und wichtige Informationen
|
|
VERBOSE = 2 # Ausführliche Informationen
|
|
TRACE = 3 # Vollständige Trace-Informationen
|
|
|
|
# Aktuelles Debug-Level (kann zur Laufzeit geändert werden)
|
|
CURRENT_DEBUG_LEVEL = DebugLevel.NORMAL
|
|
|
|
def set_debug_level(level: int):
|
|
"""Setzt das aktuelle Debug-Level für die Anwendung"""
|
|
global CURRENT_DEBUG_LEVEL
|
|
CURRENT_DEBUG_LEVEL = level
|
|
debug_logger.info(f"🔧 Debug-Level gesetzt auf: {level}")
|
|
|
|
def debug_print(message: str, level: int = DebugLevel.NORMAL):
|
|
"""
|
|
Gibt eine Debug-Nachricht aus, wenn das aktuelle Debug-Level mindestens dem angegebenen entspricht.
|
|
|
|
Args:
|
|
message: Die auszugebende Nachricht
|
|
level: Das erforderliche Debug-Level
|
|
"""
|
|
if level <= CURRENT_DEBUG_LEVEL:
|
|
# Aktuelle Funktion und Zeilennummer ermitteln
|
|
frame = inspect.currentframe().f_back
|
|
func_name = frame.f_code.co_name
|
|
file_name = os.path.basename(frame.f_code.co_filename)
|
|
line_no = frame.f_lineno
|
|
|
|
# Debug-Ausgabe formatieren
|
|
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
|
debug_prefix = f"[DEBUG {timestamp} {file_name}:{func_name}:{line_no}]"
|
|
|
|
# Verschiedene Levels mit unterschiedlichen Emojis markieren
|
|
level_emoji = "🐞" if level >= DebugLevel.VERBOSE else "🔍"
|
|
|
|
# Ausgabe
|
|
print(f"{level_emoji} {debug_prefix} {message}")
|
|
|
|
def debug_dump(obj: Any, name: str = "Object", level: int = DebugLevel.VERBOSE):
|
|
"""
|
|
Gibt den Inhalt eines Objekts für Debug-Zwecke aus.
|
|
|
|
Args:
|
|
obj: Das zu untersuchende Objekt
|
|
name: Name des Objekts für die Ausgabe
|
|
level: Das erforderliche Debug-Level
|
|
"""
|
|
if level > CURRENT_DEBUG_LEVEL:
|
|
return
|
|
|
|
debug_print(f"📦 Debug-Dump von {name}:", level)
|
|
|
|
try:
|
|
# Für dict-ähnliche Objekte
|
|
if hasattr(obj, 'items'):
|
|
for k, v in obj.items():
|
|
debug_print(f" {k}: {v}", level)
|
|
# Für list/tuple-ähnliche Objekte
|
|
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes)):
|
|
for i, item in enumerate(obj):
|
|
debug_print(f" [{i}]: {item}", level)
|
|
# Für einfache Objekte
|
|
else:
|
|
# Versuche als JSON zu formatieren
|
|
try:
|
|
json_str = json.dumps(obj, indent=2, default=str)
|
|
debug_print(f" {json_str}", level)
|
|
except:
|
|
# Fallback auf einfache String-Darstellung
|
|
debug_print(f" {obj}", level)
|
|
except Exception as e:
|
|
debug_print(f" Fehler beim Dump: {e}", level)
|
|
|
|
def debug_trace(message: str = "Execution trace"):
|
|
"""
|
|
Gibt einen vollständigen Stack-Trace für Debug-Zwecke aus.
|
|
|
|
Args:
|
|
message: Begleitende Nachricht für den Trace
|
|
"""
|
|
if CURRENT_DEBUG_LEVEL < DebugLevel.TRACE:
|
|
return
|
|
|
|
debug_print(f"🔬 TRACE: {message}", DebugLevel.TRACE)
|
|
debug_print(DEBUG_SUBSEPARATOR, DebugLevel.TRACE)
|
|
|
|
# Stack-Trace sammeln
|
|
stack = traceback.extract_stack()
|
|
# Letzten Frame (diese Funktion) entfernen
|
|
stack = stack[:-1]
|
|
|
|
for frame in stack:
|
|
file_name = os.path.basename(frame.filename)
|
|
debug_print(f" {file_name}:{frame.lineno} - {frame.name}", DebugLevel.TRACE)
|
|
|
|
debug_print(DEBUG_SUBSEPARATOR, DebugLevel.TRACE)
|
|
|
|
def debug_function(func=None, level: int = DebugLevel.NORMAL):
|
|
"""
|
|
Dekorator, der Eingang und Ausgang einer Funktion sowie die Ausführungszeit loggt.
|
|
|
|
Args:
|
|
func: Die zu dekorierende Funktion
|
|
level: Das erforderliche Debug-Level
|
|
|
|
Returns:
|
|
Dekorierte Funktion
|
|
"""
|
|
def decorator(fn):
|
|
@wraps(fn)
|
|
def wrapper(*args, **kwargs):
|
|
if CURRENT_DEBUG_LEVEL < level:
|
|
return fn(*args, **kwargs)
|
|
|
|
# Funktionsaufruf loggen
|
|
arg_str = ", ".join([
|
|
*[str(arg) for arg in args],
|
|
*[f"{k}={v}" for k, v in kwargs.items()]
|
|
])
|
|
if len(arg_str) > 100:
|
|
arg_str = arg_str[:97] + "..."
|
|
|
|
debug_print(f"▶️ Starte {fn.__name__}({arg_str})", level)
|
|
|
|
# Ausführungszeit messen
|
|
start_time = time.time()
|
|
|
|
try:
|
|
# Funktion ausführen
|
|
result = fn(*args, **kwargs)
|
|
|
|
# Ausführungszeit und Ergebnis loggen
|
|
end_time = time.time()
|
|
duration = (end_time - start_time) * 1000
|
|
|
|
result_str = str(result)
|
|
if len(result_str) > 100:
|
|
result_str = result_str[:97] + "..."
|
|
|
|
duration_emoji = "⏱️" if duration < 1000 else "⏳"
|
|
debug_print(f"{duration_emoji} {fn.__name__} beendet in {duration:.2f} ms", level)
|
|
debug_print(f"📤 Ergebnis: {result_str}", level)
|
|
|
|
return result
|
|
except Exception as e:
|
|
# Fehler loggen
|
|
end_time = time.time()
|
|
duration = (end_time - start_time) * 1000
|
|
|
|
debug_print(f"❌ {fn.__name__} fehlgeschlagen nach {duration:.2f} ms: {str(e)}", level)
|
|
|
|
# Stack-Trace nur bei hohem Debug-Level
|
|
if CURRENT_DEBUG_LEVEL >= DebugLevel.VERBOSE:
|
|
debug_print(f"🔬 Stack-Trace für {fn.__name__}:", DebugLevel.VERBOSE)
|
|
traceback_str = traceback.format_exc()
|
|
for line in traceback_str.split('\n'):
|
|
debug_print(f" {line}", DebugLevel.VERBOSE)
|
|
|
|
# Exception weiterleiten
|
|
raise
|
|
|
|
return wrapper
|
|
|
|
if func:
|
|
return decorator(func)
|
|
return decorator
|
|
|
|
def debug_timer(name: str = None, level: int = DebugLevel.NORMAL):
|
|
"""
|
|
Kontext-Manager, der die Ausführungszeit eines Code-Blocks misst.
|
|
|
|
Args:
|
|
name: Name des Code-Blocks für die Ausgabe
|
|
level: Das erforderliche Debug-Level
|
|
|
|
Beispiel:
|
|
with debug_timer("Datenbankabfrage"):
|
|
result = db.execute_query()
|
|
"""
|
|
class Timer:
|
|
def __init__(self, block_name, debug_level):
|
|
self.block_name = block_name
|
|
self.debug_level = debug_level
|
|
self.start_time = None
|
|
|
|
def __enter__(self):
|
|
if CURRENT_DEBUG_LEVEL >= self.debug_level:
|
|
self.start_time = time.time()
|
|
block_name = self.block_name or "Code-Block"
|
|
debug_print(f"⏱️ Starte Timer für: {block_name}", self.debug_level)
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if CURRENT_DEBUG_LEVEL >= self.debug_level and self.start_time:
|
|
end_time = time.time()
|
|
duration = (end_time - self.start_time) * 1000
|
|
block_name = self.block_name or "Code-Block"
|
|
|
|
if exc_type:
|
|
debug_print(f"❌ {block_name} fehlgeschlagen nach {duration:.2f} ms: {exc_val}", self.debug_level)
|
|
else:
|
|
duration_emoji = "⏱️" if duration < 1000 else "⏳"
|
|
debug_print(f"{duration_emoji} {block_name} beendet in {duration:.2f} ms", self.debug_level)
|
|
|
|
return Timer(name, level)
|
|
|
|
def debug_exception_handler(logger: Optional[logging.Logger] = None):
|
|
"""
|
|
Dekorator, der Ausnahmen abfängt und Details loggt.
|
|
|
|
Args:
|
|
logger: Logger-Instanz für die Protokollierung (optional)
|
|
|
|
Returns:
|
|
Dekorierte Funktion
|
|
"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
# Logger verwenden oder Fallback auf Standardausgabe
|
|
log = logger or debug_logger
|
|
|
|
# Ausnahmedetails loggen
|
|
log.error(f"❌ Ausnahme in {func.__name__}: {str(e)}")
|
|
|
|
# Stack-Trace bei hohem Debug-Level
|
|
if CURRENT_DEBUG_LEVEL >= DebugLevel.VERBOSE:
|
|
log.error("🔬 Stack-Trace:")
|
|
traceback_str = traceback.format_exc()
|
|
for line in traceback_str.split('\n'):
|
|
if line.strip():
|
|
log.error(f" {line}")
|
|
|
|
# Ausnahme weiterleiten
|
|
raise
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
# Konsolen-Befehle für interaktives Debugging
|
|
|
|
def dump_all_loggers():
|
|
"""Gibt Informationen über alle konfigurierten Logger aus."""
|
|
import logging
|
|
|
|
debug_print("📋 Konfigurierte Logger:", DebugLevel.VERBOSE)
|
|
for name, logger in logging.Logger.manager.loggerDict.items():
|
|
if isinstance(logger, logging.Logger):
|
|
level_name = logging.getLevelName(logger.level)
|
|
handlers = len(logger.handlers)
|
|
debug_print(f" {name}: Level={level_name}, Handlers={handlers}", DebugLevel.VERBOSE)
|
|
|
|
def dump_environment():
|
|
"""Gibt Umgebungsvariablen und Systeminformationen aus."""
|
|
debug_print("🌐 Umgebungsinformationen:", DebugLevel.VERBOSE)
|
|
debug_print(f" Python: {sys.version}", DebugLevel.VERBOSE)
|
|
debug_print(f" Plattform: {sys.platform}", DebugLevel.VERBOSE)
|
|
debug_print(f" Arbeitsverzeichnis: {os.getcwd()}", DebugLevel.VERBOSE)
|
|
|
|
debug_print("🔑 Umgebungsvariablen:", DebugLevel.VERBOSE)
|
|
for key, value in sorted(os.environ.items()):
|
|
# Passwörter und Secrets ausblenden
|
|
if any(secret_key in key.lower() for secret_key in ['key', 'pass', 'secret', 'token', 'pwd']):
|
|
value = "********"
|
|
debug_print(f" {key}={value}", DebugLevel.VERBOSE)
|
|
|
|
def memory_usage(obj: Any = None) -> Dict[str, Any]:
|
|
"""
|
|
Gibt Informationen über den Speicherverbrauch zurück.
|
|
|
|
Args:
|
|
obj: Optional ein Objekt, dessen Größe gemessen werden soll
|
|
|
|
Returns:
|
|
Dict mit Speicherverbrauchsinformationen
|
|
"""
|
|
import psutil
|
|
import sys
|
|
|
|
process = psutil.Process(os.getpid())
|
|
memory_info = process.memory_info()
|
|
|
|
result = {
|
|
"rss": memory_info.rss / (1024 * 1024), # MB
|
|
"vms": memory_info.vms / (1024 * 1024), # MB
|
|
"percent": process.memory_percent(),
|
|
}
|
|
|
|
if obj is not None:
|
|
try:
|
|
import sys
|
|
result["object_size"] = sys.getsizeof(obj) / 1024 # KB
|
|
except:
|
|
result["object_size"] = "Nicht messbar"
|
|
|
|
return result
|
|
|
|
def log_memory_usage(obj_name: str = "Anwendung", obj: Any = None, logger: Optional[logging.Logger] = None):
|
|
"""
|
|
Loggt den aktuellen Speicherverbrauch.
|
|
|
|
Args:
|
|
obj_name: Name des Objekts oder der Anwendung
|
|
obj: Optional ein Objekt, dessen Größe gemessen werden soll
|
|
logger: Logger-Instanz für die Protokollierung (optional)
|
|
"""
|
|
log = logger or debug_logger
|
|
memory = memory_usage(obj)
|
|
|
|
log.info(f"📊 Speicherverbrauch von {obj_name}:")
|
|
log.info(f" RSS: {memory['rss']:.2f} MB")
|
|
log.info(f" VMS: {memory['vms']:.2f} MB")
|
|
log.info(f" Prozent: {memory['percent']:.2f}%")
|
|
|
|
if 'object_size' in memory:
|
|
if isinstance(memory['object_size'], (int, float)):
|
|
log.info(f" Objektgröße: {memory['object_size']:.2f} KB")
|
|
else:
|
|
log.info(f" Objektgröße: {memory['object_size']}")
|
|
|
|
def profile_function(func):
|
|
"""
|
|
Dekorator, der eine Funktion profiliert und Statistiken ausgibt.
|
|
|
|
Args:
|
|
func: Die zu profilierende Funktion
|
|
|
|
Returns:
|
|
Dekorierte Funktion
|
|
"""
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
import cProfile
|
|
import pstats
|
|
import io
|
|
|
|
# Profiler erstellen und Funktion ausführen
|
|
profiler = cProfile.Profile()
|
|
profiler.enable()
|
|
result = func(*args, **kwargs)
|
|
profiler.disable()
|
|
|
|
# Statistiken sammeln
|
|
s = io.StringIO()
|
|
ps = pstats.Stats(profiler, stream=s).sort_stats('cumulative')
|
|
ps.print_stats(20) # Top 20 Zeilen
|
|
|
|
# Statistiken ausgeben
|
|
debug_print(f"📊 Profiling-Ergebnis für {func.__name__}:", DebugLevel.VERBOSE)
|
|
for line in s.getvalue().split('\n'):
|
|
if line.strip():
|
|
debug_print(f" {line}", DebugLevel.VERBOSE)
|
|
|
|
return result
|
|
except ImportError:
|
|
debug_print(f"⚠️ cProfile nicht verfügbar, Funktion wird ohne Profiling ausgeführt", DebugLevel.NORMAL)
|
|
return func(*args, **kwargs)
|
|
|
|
return wrapper |