diff --git a/backend/app/app.py b/backend/app/app.py index 9b84e72e..6f3da578 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -222,13 +222,6 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'): setup_logging() log_startup_info() -# Error-Monitoring-System starten -try: - from utils.logging_config import setup_error_monitoring - setup_error_monitoring() -except Exception as e: - print(f"⚠️ Error-Monitoring konnte nicht gestartet werden: {str(e)}") - # Logger für verschiedene Komponenten app_logger = get_logger("app") auth_logger = get_logger("auth") @@ -331,23 +324,23 @@ def login(): remember_me = False try: - if is_json_request: + if is_json_request: # JSON-Request verarbeiten try: data = request.get_json(force=True) or {} username = data.get("username") or data.get("email") - password = data.get("password") - remember_me = data.get("remember_me", False) + password = data.get("password") + remember_me = data.get("remember_me", False) except Exception as json_error: auth_logger.warning(f"JSON-Parsing fehlgeschlagen: {str(json_error)}") # Fallback zu Form-Daten username = request.form.get("email") password = request.form.get("password") remember_me = request.form.get("remember_me") == "on" - else: + else: # Form-Request verarbeiten username = request.form.get("email") - password = request.form.get("password") + password = request.form.get("password") remember_me = request.form.get("remember_me") == "on" # Zusätzlicher Fallback für verschiedene Feldnamen @@ -2306,16 +2299,16 @@ def finish_job(job_id): def cancel_job(job_id): """Bricht einen Job ab.""" try: - db_session = get_db_session() + db_session = get_db_session() job = db_session.query(Job).get(job_id) if not job: - db_session.close() + db_session.close() return jsonify({"error": "Job nicht gefunden"}), 404 # Prüfen, ob der Job abgebrochen werden kann if job.status not in ["scheduled", "running"]: - db_session.close() + db_session.close() return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400 # Job als abgebrochen markieren @@ -2327,7 +2320,7 @@ def cancel_job(job_id): from utils.job_scheduler import toggle_plug toggle_plug(job.printer_id, False) - db_session.commit() + db_session.commit() job_dict = job.to_dict() db_session.close() @@ -2345,16 +2338,16 @@ def cancel_job(job_id): def start_job(job_id): """Startet einen Job manuell.""" try: - db_session = get_db_session() + db_session = get_db_session() job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id) - + if not job: db_session.close() return jsonify({"error": "Job nicht gefunden"}), 404 # Prüfen, ob der Job gestartet werden kann if job.status not in ["scheduled", "queued", "waiting_for_printer"]: - db_session.close() + db_session.close() return jsonify({"error": f"Job kann im Status '{job.status}' nicht gestartet werden"}), 400 # Drucker einschalten falls verfügbar @@ -2365,7 +2358,7 @@ def start_job(job_id): jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet") else: jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten") - except Exception as e: + except Exception as e: jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}") # Job als laufend markieren @@ -2405,7 +2398,7 @@ def pause_job(job_id): # Prüfen, ob der Job pausiert werden kann if job.status != "running": - db_session.close() + db_session.close() return jsonify({"error": f"Job kann im Status '{job.status}' nicht pausiert werden"}), 400 # Drucker ausschalten @@ -2416,7 +2409,7 @@ def pause_job(job_id): jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} ausgeschaltet (Pause)") else: jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht ausschalten") - except Exception as e: + except Exception as e: jobs_logger.warning(f"Fehler beim Ausschalten des Druckers für Job {job_id}: {str(e)}") # Job als pausiert markieren @@ -2426,7 +2419,7 @@ def pause_job(job_id): db_session.commit() job_dict = job.to_dict() - db_session.close() + db_session.close() jobs_logger.info(f"Job {job_id} pausiert von Benutzer {current_user.id}") return jsonify({ @@ -2465,7 +2458,7 @@ def resume_job(job_id): jobs_logger.info(f"Drucker {job.printer.name} für Job {job_id} eingeschaltet (Resume)") else: jobs_logger.warning(f"Konnte Drucker {job.printer.name} für Job {job_id} nicht einschalten") - except Exception as e: + except Exception as e: jobs_logger.warning(f"Fehler beim Einschalten des Druckers für Job {job_id}: {str(e)}") # Job als laufend markieren @@ -4526,7 +4519,7 @@ if __name__ == "__main__": os.environ['FLASK_ENV'] = 'development' os.environ['PYTHONIOENCODING'] = 'utf-8' os.environ['PYTHONUTF8'] = '1' - + # Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown def signal_handler(sig, frame): """Signal-Handler für ordnungsgemäßes Shutdown.""" @@ -4534,7 +4527,7 @@ if __name__ == "__main__": try: # Queue Manager stoppen app_logger.info("🔄 Beende Queue Manager...") - stop_queue_manager() + stop_queue_manager() # Scheduler stoppen falls aktiviert if SCHEDULER_ENABLED and scheduler: @@ -4590,15 +4583,15 @@ if __name__ == "__main__": # Signal-Handler registrieren (Windows-kompatibel) - if os.name == 'nt': # Windows - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) + if os.name == 'nt': # Windows + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) # Zusätzlich für Flask-Development-Server - signal.signal(signal.SIGBREAK, signal_handler) - else: # Unix/Linux - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) + signal.signal(signal.SIGBREAK, signal_handler) + else: # Unix/Linux + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) try: # Datenbank initialisieren @@ -4606,24 +4599,24 @@ if __name__ == "__main__": create_initial_admin() # Template-Hilfsfunktionen registrieren - register_template_helpers(app) + register_template_helpers(app) # Drucker-Monitor Steckdosen-Initialisierung beim Start try: app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...") - initialization_results = printer_monitor.initialize_all_outlets_on_startup() - - if initialization_results: - success_count = sum(1 for success in initialization_results.values() if success) - total_count = len(initialization_results) + initialization_results = printer_monitor.initialize_all_outlets_on_startup() + + if initialization_results: + success_count = sum(1 for success in initialization_results.values() if success) + total_count = len(initialization_results) app_logger.info(f"✅ Steckdosen-Initialisierung: {success_count}/{total_count} Drucker erfolgreich") - - if success_count < total_count: - app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden") - else: + + if success_count < total_count: + app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden") + else: app_logger.info("ℹ️ Keine Drucker zur Initialisierung gefunden") - - except Exception as e: + + except Exception as e: app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}") # Queue-Manager für automatische Drucker-Überwachung starten @@ -4666,15 +4659,15 @@ if __name__ == "__main__": # Connection-Pool ordnungsgemäß schließen engine.dispose() app_logger.info("✅ Finales Datenbank-Cleanup abgeschlossen") - - except Exception as e: + + except Exception as e: app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}") atexit.register(cleanup_database) except Exception as e: app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") - else: + else: app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung") # Scheduler starten (falls aktiviert) @@ -4682,7 +4675,7 @@ if __name__ == "__main__": try: scheduler.start() app_logger.info("Job-Scheduler gestartet") - except Exception as e: + except Exception as e: app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}") if debug_mode: @@ -4703,28 +4696,28 @@ if __name__ == "__main__": run_kwargs["passthrough_errors"] = False app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert") - app.run(**run_kwargs) + app.run(**run_kwargs) else: # Produktions-Modus: HTTPS auf Port 443 ssl_context = get_ssl_context() if ssl_context: app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443") - app.run( + app.run( host="0.0.0.0", - port=443, - debug=False, - ssl_context=ssl_context, - threaded=True - ) + port=443, + debug=False, + ssl_context=ssl_context, + threaded=True + ) else: app_logger.info("Starte HTTP-Server auf 0.0.0.0:8080") - app.run( + app.run( host="0.0.0.0", - port=8080, - debug=False, - threaded=True - ) + port=8080, + debug=False, + threaded=True + ) except KeyboardInterrupt: app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...") signal_handler(signal.SIGINT, None) @@ -4866,54 +4859,4 @@ def get_printers(): return jsonify({ "error": f"Fehler beim Laden der Drucker: {str(e)}", "printers": [] - }), 500 - -@app.route('/api/admin/error-monitor/stats', methods=['GET']) -@admin_required -def get_error_monitor_stats(): - """Gibt Statistiken des Error-Monitoring-Systems zurück.""" - try: - from utils.error_monitor import get_error_monitor - - monitor = get_error_monitor() - stats = monitor.get_stats() - - return jsonify({ - "success": True, - "stats": stats - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Error-Monitor-Statistiken: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/error-monitor/force-report', methods=['POST']) -@admin_required -def force_error_report(): - """Erzwingt das sofortige Senden eines Error-Reports (für Tests).""" - try: - from utils.error_monitor import get_error_monitor - - monitor = get_error_monitor() - success = monitor.force_report() - - if success: - return jsonify({ - "success": True, - "message": "Error Report erfolgreich versendet" - }) - else: - return jsonify({ - "success": False, - "error": "Report konnte nicht versendet werden" - }), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Erzwingen des Error-Reports: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) }), 500 \ No newline at end of file diff --git a/backend/app/config/error_monitoring.env.example b/backend/app/config/error_monitoring.env.example deleted file mode 100644 index 352bff7a..00000000 --- a/backend/app/config/error_monitoring.env.example +++ /dev/null @@ -1,106 +0,0 @@ -# Mercedes-Benz MYP Platform - Error Monitoring Configuration -# ========================================================== -# -# Diese Datei enthält alle Umgebungsvariablen für das automatische -# Error-Monitoring-System mit E-Mail-Benachrichtigungen. -# -# Kopieren Sie diese Datei zu 'error_monitoring.env' und passen Sie -# die Werte an Ihre Umgebung an. - -# ===== E-MAIL-KONFIGURATION ===== - -# SMTP-Server-Einstellungen -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USE_TLS=true - -# SMTP-Anmeldedaten (für Gmail: App-Passwort verwenden!) -SMTP_USERNAME=myp-system@mercedes-benz.com -SMTP_PASSWORD=your-app-password-here - -# E-Mail-Adressen -ALERT_EMAIL=admin@mercedes-benz.com -FROM_EMAIL=myp-system@mercedes-benz.com -FROM_NAME=MYP Platform Error Monitor - -# ===== MONITORING-KONFIGURATION ===== - -# Report-Intervall in Sekunden (Standard: 3600 = 1 Stunde) -ERROR_REPORT_INTERVAL=3600 - -# Minimale Anzahl neuer Fehler für E-Mail-Versand -MIN_ERRORS_FOR_EMAIL=1 - -# Maximale Anzahl Fehler pro Report (verhindert zu lange E-Mails) -MAX_ERRORS_PER_REPORT=50 - -# ===== ERWEITERTE EINSTELLUNGEN ===== - -# Speicherort der Error-Monitor-Datenbank (relativ zu app-Verzeichnis) -ERROR_DB_PATH=database/error_monitor.db - -# Log-Level für Error-Monitor selbst (WARNING, ERROR, CRITICAL) -ERROR_MONITOR_LOG_LEVEL=WARNING - -# ===== BEISPIEL-KONFIGURATIONEN ===== - -# Für Development (alle 5 Minuten, ab 1 Fehler) -# ERROR_REPORT_INTERVAL=300 -# MIN_ERRORS_FOR_EMAIL=1 - -# Für Production (alle 2 Stunden, ab 3 Fehlern) -# ERROR_REPORT_INTERVAL=7200 -# MIN_ERRORS_FOR_EMAIL=3 - -# Für Testing (sofortige Reports) -# ERROR_REPORT_INTERVAL=60 -# MIN_ERRORS_FOR_EMAIL=1 - -# ===== SMTP-PROVIDER-BEISPIELE ===== - -# Gmail (App-Passwort erforderlich) -# SMTP_HOST=smtp.gmail.com -# SMTP_PORT=587 -# SMTP_USE_TLS=true - -# Outlook/Hotmail -# SMTP_HOST=smtp-mail.outlook.com -# SMTP_PORT=587 -# SMTP_USE_TLS=true - -# Yahoo -# SMTP_HOST=smtp.mail.yahoo.com -# SMTP_PORT=587 -# SMTP_USE_TLS=true - -# Mercedes-Benz Corporate (Beispiel) -# SMTP_HOST=mail.mercedes-benz.com -# SMTP_PORT=587 -# SMTP_USE_TLS=true - -# ===== SICHERHEITSHINWEISE ===== - -# 1. Verwenden Sie App-Passwörter anstatt normale Passwörter -# 2. Diese Datei sollte NIEMALS in Git committed werden -# 3. Setzen Sie restriktive Dateiberechtigungen (600) -# 4. Verwenden Sie separate E-Mail-Accounts für System-Alerts -# 5. Testen Sie die SMTP-Konfiguration bevor Sie sie produktiv setzen - -# ===== INSTALLATION ===== - -# 1. Kopieren Sie diese Datei: -# cp config/error_monitoring.env.example config/error_monitoring.env -# -# 2. Passen Sie die Werte an Ihre Umgebung an -# -# 3. Laden Sie die Umgebungsvariablen: -# source config/error_monitoring.env -# -# 4. Oder verwenden Sie python-dotenv: -# pip install python-dotenv -# # Dann in Ihrer app.py: load_dotenv('config/error_monitoring.env') - -# ===== TESTING ===== - -# Zum Testen des Error-Monitoring-Systems: -# python -c "from utils.error_monitor import test_error_monitoring; test_error_monitoring()" \ No newline at end of file diff --git a/backend/app/utils/error_monitor.py b/backend/app/utils/error_monitor.py deleted file mode 100644 index b6a300f1..00000000 --- a/backend/app/utils/error_monitor.py +++ /dev/null @@ -1,593 +0,0 @@ -""" -Mercedes-Benz MYP Platform - Live Error Monitoring System -========================================================= - -Custom Logging Handler der ERROR-Level Logs abfängt und gruppiert. -Automatischer E-Mail-Versand von Fehlerzusammenfassungen. - -Features: -- Custom LoggingHandler für ERROR-Level-Abfang -- Gruppierung nach Exception-Typ und Stacktrace-Hash -- Stündliche Markdown-Tabellen-Berichte -- SMTP-Versand nur bei neuen Fehlern -- Thread-sichere Implementierung -- Umgebungsvariablen-Konfiguration - -Autor: MYP Platform Team -Datum: 30.05.2025 -Version: 1.0 -""" - -import logging -import hashlib -import smtplib -import threading -import time -import traceback -from collections import defaultdict, Counter -from datetime import datetime, timedelta -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from typing import Dict, List, Optional, Tuple -import os -import json -import sqlite3 -from contextlib import contextmanager - -class ErrorMonitorHandler(logging.Handler): - """ - Custom Logging Handler der ERROR-Level Logs abfängt und für - automatische E-Mail-Berichte sammelt. - """ - - def __init__(self, monitor_instance): - super().__init__(level=logging.ERROR) - self.monitor = monitor_instance - self.setFormatter(logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - )) - - def emit(self, record): - """ - Verarbeitet ERROR-Level Log-Records und leitet sie an den ErrorMonitor weiter. - """ - try: - # Exception-Informationen extrahieren - exc_type = None - exc_message = "" - stack_trace = "" - - if record.exc_info: - exc_type = record.exc_info[0].__name__ if record.exc_info[0] else "UnknownException" - exc_message = str(record.exc_info[1]) if record.exc_info[1] else record.getMessage() - stack_trace = ''.join(traceback.format_exception(*record.exc_info)) - else: - # Fallback wenn keine exc_info vorhanden - exc_type = "LoggedError" - exc_message = record.getMessage() - stack_trace = f"{record.name}:{record.lineno} - {exc_message}" - - # Error an Monitor weiterleiten - self.monitor.add_error( - exc_type=exc_type, - message=exc_message, - stack_trace=stack_trace, - logger_name=record.name, - level=record.levelname, - timestamp=datetime.fromtimestamp(record.created) - ) - - except Exception as e: - # Fehler beim Fehler-Handling - nur in Konsole ausgeben um Loop zu vermeiden - print(f"❌ Error in ErrorMonitorHandler: {str(e)}") - -class ErrorMonitor: - """ - Haupt-Error-Monitoring-Klasse die Fehler sammelt, gruppiert und E-Mail-Berichte sendet. - """ - - def __init__(self): - # Konfiguration aus Umgebungsvariablen - self.smtp_host = os.getenv('SMTP_HOST', 'localhost') - self.smtp_port = int(os.getenv('SMTP_PORT', '587')) - self.smtp_username = os.getenv('SMTP_USERNAME', '') - self.smtp_password = os.getenv('SMTP_PASSWORD', '') - self.smtp_use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true' - - self.alert_email = os.getenv('ALERT_EMAIL', 'admin@mercedes-benz.com') - self.from_email = os.getenv('FROM_EMAIL', 'myp-system@mercedes-benz.com') - self.from_name = os.getenv('FROM_NAME', 'MYP Platform Error Monitor') - - # Monitoring-Konfiguration - self.report_interval = int(os.getenv('ERROR_REPORT_INTERVAL', '3600')) # Standard: 1 Stunde - self.max_errors_per_report = int(os.getenv('MAX_ERRORS_PER_REPORT', '50')) - self.min_errors_for_email = int(os.getenv('MIN_ERRORS_FOR_EMAIL', '1')) - - # Thread-sichere Datenstrukturen - self._lock = threading.RLock() - self._errors = defaultdict(list) # {error_hash: [error_instances]} - self._error_counts = Counter() # {error_hash: count} - self._last_report_time = datetime.now() - self._sent_hashes = set() # Bereits gesendete Error-Hashes - - # Persistente Speicherung - self.db_path = os.path.join(os.path.dirname(__file__), '..', 'database', 'error_monitor.db') - self._init_database() - self._load_sent_hashes() - - # Background-Thread für periodische Reports - self._monitoring = True - self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) - self._monitor_thread.start() - - # Logger für dieses System (niedriger Level um Loops zu vermeiden) - self.logger = logging.getLogger('error_monitor') - self.logger.setLevel(logging.WARNING) # Nur WARNING und höher - - # Custom Handler registrieren - self.handler = ErrorMonitorHandler(self) - - print(f"✅ ErrorMonitor gestartet - Reports alle {self.report_interval}s an {self.alert_email}") - - def _init_database(self): - """Initialisiert die SQLite-Datenbank für persistente Speicherung.""" - try: - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - - with self._get_db_connection() as conn: - conn.execute(''' - CREATE TABLE IF NOT EXISTS sent_errors ( - error_hash TEXT PRIMARY KEY, - first_sent TIMESTAMP, - last_sent TIMESTAMP, - send_count INTEGER DEFAULT 1 - ) - ''') - - conn.execute(''' - CREATE TABLE IF NOT EXISTS error_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - error_hash TEXT, - exc_type TEXT, - message TEXT, - logger_name TEXT, - timestamp TIMESTAMP, - INDEX(error_hash), - INDEX(timestamp) - ) - ''') - - conn.commit() - - except Exception as e: - print(f"❌ Database init error: {str(e)}") - - @contextmanager - def _get_db_connection(self): - """Context Manager für sichere Datenbankverbindungen.""" - conn = None - try: - conn = sqlite3.connect(self.db_path, timeout=10.0) - conn.row_factory = sqlite3.Row - yield conn - finally: - if conn: - conn.close() - - def _load_sent_hashes(self): - """Lädt bereits gesendete Error-Hashes aus der Datenbank.""" - try: - with self._get_db_connection() as conn: - cursor = conn.execute( - 'SELECT error_hash FROM sent_errors WHERE last_sent > ?', - (datetime.now() - timedelta(days=7),) # Nur letzte 7 Tage laden - ) - self._sent_hashes = {row['error_hash'] for row in cursor.fetchall()} - - print(f"📋 {len(self._sent_hashes)} bereits gesendete Fehler-Hashes geladen") - - except Exception as e: - print(f"❌ Error loading sent hashes: {str(e)}") - self._sent_hashes = set() - - def _generate_error_hash(self, exc_type: str, message: str, stack_trace: str) -> str: - """ - Generiert einen Hash für einen Fehler basierend auf Typ, Message und Stacktrace. - Ähnliche Fehler bekommen den gleichen Hash für Gruppierung. - """ - # Stack-Trace normalisieren (Zeilen-Nummern und Timestamps entfernen) - normalized_stack = [] - for line in stack_trace.split('\n'): - # Entferne Datei-Pfade, Zeilen-Nummern und Timestamps - if 'File "' in line and ', line ' in line: - # Nur Dateiname und Funktion behalten - parts = line.split(', ') - if len(parts) >= 2: - file_part = parts[0].split('/')[-1] if '/' in parts[0] else parts[0] - func_part = parts[-1] if 'in ' in parts[-1] else '' - normalized_stack.append(f"{file_part} {func_part}") - elif line.strip() and not line.strip().startswith('2025-'): # Timestamps ignorieren - normalized_stack.append(line.strip()) - - # Hash-Input erstellen - hash_input = f"{exc_type}|{message[:200]}|{'|'.join(normalized_stack[:5])}" - - return hashlib.md5(hash_input.encode('utf-8')).hexdigest()[:12] - - def add_error(self, exc_type: str, message: str, stack_trace: str, - logger_name: str, level: str, timestamp: datetime): - """ - Fügt einen neuen Fehler zur Sammlung hinzu. - """ - try: - error_hash = self._generate_error_hash(exc_type, message, stack_trace) - - error_data = { - 'hash': error_hash, - 'exc_type': exc_type, - 'message': message, - 'stack_trace': stack_trace, - 'logger_name': logger_name, - 'level': level, - 'timestamp': timestamp - } - - with self._lock: - self._errors[error_hash].append(error_data) - self._error_counts[error_hash] += 1 - - # In Datenbank speichern für Historien-Tracking - self._store_error_in_db(error_hash, exc_type, message, logger_name, timestamp) - - except Exception as e: - print(f"❌ Error adding error to monitor: {str(e)}") - - def _store_error_in_db(self, error_hash: str, exc_type: str, message: str, - logger_name: str, timestamp: datetime): - """Speichert Fehler in der Datenbank für Historien-Tracking.""" - try: - with self._get_db_connection() as conn: - conn.execute(''' - INSERT INTO error_history (error_hash, exc_type, message, logger_name, timestamp) - VALUES (?, ?, ?, ?, ?) - ''', (error_hash, exc_type, message[:500], logger_name, timestamp)) - conn.commit() - - except Exception as e: - print(f"❌ Error storing in DB: {str(e)}") - - def _monitor_loop(self): - """Background-Thread der periodische Reports sendet.""" - while self._monitoring: - try: - time.sleep(60) # Prüfe jede Minute - - now = datetime.now() - time_since_last_report = (now - self._last_report_time).total_seconds() - - if time_since_last_report >= self.report_interval: - self._send_error_report() - self._last_report_time = now - - except Exception as e: - print(f"❌ Error in monitor loop: {str(e)}") - time.sleep(300) # 5 Minuten warten bei Fehler - - def _send_error_report(self): - """Sendet einen E-Mail-Report mit gesammelten Fehlern.""" - try: - with self._lock: - if not self._errors: - print("📧 Keine neuen Fehler - kein Report versendet") - return - - # Neue Fehler identifizieren - new_errors = { - hash_val: errors for hash_val, errors in self._errors.items() - if hash_val not in self._sent_hashes - } - - if len(new_errors) < self.min_errors_for_email: - print(f"📧 Nur {len(new_errors)} neue Fehler - Minimum {self.min_errors_for_email} nicht erreicht") - return - - # Report generieren und senden - report = self._generate_markdown_report(new_errors) - self._send_email(report, len(new_errors)) - - # Als gesendet markieren - for error_hash in new_errors.keys(): - self._sent_hashes.add(error_hash) - self._mark_as_sent(error_hash) - - # Fehler-Cache leeren - self._errors.clear() - self._error_counts.clear() - - print(f"📧 Error Report versendet: {len(new_errors)} neue Fehlertypen") - - except Exception as e: - print(f"❌ Error sending report: {str(e)}") - - def _generate_markdown_report(self, errors: Dict) -> str: - """Generiert einen Markdown-Report der Fehler.""" - now = datetime.now() - - # Header - report = f"""# 🚨 MYP Platform - Error Report - -**Zeitraum:** {self._last_report_time.strftime('%d.%m.%Y %H:%M')} - {now.strftime('%d.%m.%Y %H:%M')} -**Neue Fehlertypen:** {len(errors)} -**System:** Mercedes-Benz MYP Platform - ---- - -## 📊 Fehlerübersicht - -| Exception-Typ | Anzahl | Letzter Zeitstempel | Logger | Hash | -|---------------|--------|-------------------|--------|------| -""" - - # Fehler nach Anzahl sortieren - sorted_errors = sorted( - errors.items(), - key=lambda x: len(x[1]), - reverse=True - ) - - for error_hash, error_list in sorted_errors[:self.max_errors_per_report]: - latest_error = max(error_list, key=lambda x: x['timestamp']) - count = len(error_list) - - exc_type = latest_error['exc_type'] - timestamp = latest_error['timestamp'].strftime('%d.%m.%Y %H:%M:%S') - logger_name = latest_error['logger_name'] - - report += f"| `{exc_type}` | **{count}x** | {timestamp} | {logger_name} | `{error_hash}` |\n" - - # Details zu den häufigsten Fehlern - report += "\n---\n\n## 🔍 Fehlerdetails\n\n" - - for i, (error_hash, error_list) in enumerate(sorted_errors[:5]): # Top 5 - latest_error = max(error_list, key=lambda x: x['timestamp']) - count = len(error_list) - - report += f"### {i+1}. {latest_error['exc_type']} ({count}x)\n\n" - report += f"**Hash:** `{error_hash}`\n" - report += f"**Logger:** {latest_error['logger_name']}\n" - report += f"**Letzter Auftritt:** {latest_error['timestamp'].strftime('%d.%m.%Y %H:%M:%S')}\n\n" - report += f"**Message:**\n```\n{latest_error['message'][:300]}{'...' if len(latest_error['message']) > 300 else ''}\n```\n\n" - - # Stack-Trace (gekürzt) - stack_lines = latest_error['stack_trace'].split('\n') - relevant_stack = [line for line in stack_lines if 'File "' in line or 'Error:' in line or 'Exception:' in line] - if relevant_stack: - report += f"**Stack-Trace (gekürzt):**\n```\n" + '\n'.join(relevant_stack[-5:]) + "\n```\n\n" - - report += "---\n\n" - - # Footer - report += f""" -## 📈 Statistiken - -- **Monitoring-Intervall:** {self.report_interval / 3600:.1f} Stunden -- **Überwachte Logger:** Alle ERROR-Level Logs -- **Report generiert:** {now.strftime('%d.%m.%Y %H:%M:%S')} -- **System-Status:** Online ✅ - ---- - -*Dieses ist ein automatisch generierter Report des MYP Platform Error Monitoring Systems.* -*Bei kritischen Fehlern kontaktieren Sie bitte sofort das Entwicklerteam.* -""" - - return report - - def _send_email(self, report: str, error_count: int): - """Sendet den Error-Report per E-Mail.""" - try: - # E-Mail zusammenstellen - msg = MIMEMultipart('alternative') - msg['Subject'] = f"🚨 MYP Platform Error Report - {error_count} neue Fehler" - msg['From'] = f"{self.from_name} <{self.from_email}>" - msg['To'] = self.alert_email - msg['X-Priority'] = '2' # High Priority - - # Markdown als Plain Text - text_part = MIMEText(report, 'plain', 'utf-8') - msg.attach(text_part) - - # E-Mail senden - if not self.smtp_username: - print("📧 SMTP nicht konfiguriert - Report nur in Console ausgegeben") - print("\n" + "="*60) - print(report) - print("="*60 + "\n") - return - - with smtplib.SMTP(self.smtp_host, self.smtp_port) as server: - if self.smtp_use_tls: - server.starttls() - - server.login(self.smtp_username, self.smtp_password) - server.send_message(msg) - - print(f"📧 Error Report erfolgreich versendet an {self.alert_email}") - - except Exception as e: - print(f"❌ E-Mail-Versand fehlgeschlagen: {str(e)}") - # Fallback: Report in Console ausgeben - print("\n" + "="*60) - print("FALLBACK ERROR REPORT:") - print(report) - print("="*60 + "\n") - - def _mark_as_sent(self, error_hash: str): - """Markiert einen Fehler-Hash als versendet in der Datenbank.""" - try: - with self._get_db_connection() as conn: - # Prüfen ob bereits vorhanden - cursor = conn.execute( - 'SELECT send_count FROM sent_errors WHERE error_hash = ?', - (error_hash,) - ) - existing = cursor.fetchone() - - now = datetime.now() - - if existing: - # Update - conn.execute(''' - UPDATE sent_errors - SET last_sent = ?, send_count = send_count + 1 - WHERE error_hash = ? - ''', (now, error_hash)) - else: - # Insert - conn.execute(''' - INSERT INTO sent_errors (error_hash, first_sent, last_sent, send_count) - VALUES (?, ?, ?, 1) - ''', (error_hash, now, now)) - - conn.commit() - - except Exception as e: - print(f"❌ Error marking as sent: {str(e)}") - - def get_stats(self) -> Dict: - """Gibt aktuelle Monitoring-Statistiken zurück.""" - with self._lock: - current_errors = len(self._errors) - total_instances = sum(len(errors) for errors in self._errors.values()) - - try: - with self._get_db_connection() as conn: - cursor = conn.execute('SELECT COUNT(*) as total FROM error_history') - historical_total = cursor.fetchone()['total'] - - cursor = conn.execute('SELECT COUNT(*) as sent FROM sent_errors') - sent_count = cursor.fetchone()['sent'] - except: - historical_total = 0 - sent_count = 0 - - return { - 'current_error_types': current_errors, - 'current_total_instances': total_instances, - 'historical_total_errors': historical_total, - 'sent_error_types': sent_count, - 'last_report': self._last_report_time.isoformat(), - 'monitoring_interval': self.report_interval, - 'alert_email': self.alert_email, - 'monitoring_active': self._monitoring - } - - def force_report(self) -> bool: - """Erzwingt das sofortige Senden eines Reports (für Tests/Debug).""" - try: - print("🔧 Erzwinge sofortigen Error Report...") - self._send_error_report() - return True - except Exception as e: - print(f"❌ Force report failed: {str(e)}") - return False - - def stop(self): - """Stoppt das Error-Monitoring.""" - self._monitoring = False - if self._monitor_thread.is_alive(): - self._monitor_thread.join(timeout=5.0) - print("⏹️ ErrorMonitor gestoppt") - - def install_to_logger(self, logger_name: Optional[str] = None): - """ - Installiert den ErrorMonitorHandler zu einem bestimmten Logger oder Root-Logger. - """ - if logger_name: - logger = logging.getLogger(logger_name) - else: - logger = logging.getLogger() # Root logger - - # Verhindere Duplikate - for handler in logger.handlers: - if isinstance(handler, ErrorMonitorHandler): - logger.removeHandler(handler) - - logger.addHandler(self.handler) - print(f"✅ ErrorMonitorHandler installiert zu Logger: {logger_name or 'root'}") - -# Globale Instanz -_error_monitor_instance = None - -def get_error_monitor() -> ErrorMonitor: - """Gibt die globale ErrorMonitor-Instanz zurück (Singleton).""" - global _error_monitor_instance - if _error_monitor_instance is None: - _error_monitor_instance = ErrorMonitor() - return _error_monitor_instance - -def install_error_monitoring(logger_names: Optional[List[str]] = None): - """ - Installiert Error-Monitoring für bestimmte Logger oder alle Logger. - - Args: - logger_names: Liste der Logger-Namen oder None für Root-Logger - """ - monitor = get_error_monitor() - - if logger_names: - for logger_name in logger_names: - monitor.install_to_logger(logger_name) - else: - # Root logger - fängt alle Errors ab - monitor.install_to_logger() - - print(f"🔍 Error Monitoring aktiviert für {len(logger_names) if logger_names else 'alle'} Logger") - -def stop_error_monitoring(): - """Stoppt das Error-Monitoring.""" - global _error_monitor_instance - if _error_monitor_instance: - _error_monitor_instance.stop() - _error_monitor_instance = None - -# Test-Funktion -def test_error_monitoring(): - """Testet das Error-Monitoring-System.""" - print("🧪 Teste Error-Monitoring-System...") - - monitor = get_error_monitor() - monitor.install_to_logger() - - # Test-Logger erstellen - test_logger = logging.getLogger('test_error_monitor') - test_logger.setLevel(logging.ERROR) - - # Test-Fehler generieren - try: - raise ValueError("Test-Fehler für Error-Monitoring") - except Exception as e: - test_logger.error("Test Exception aufgetreten", exc_info=True) - - try: - raise ConnectionError("Test-Verbindungsfehler") - except Exception as e: - test_logger.error("Verbindungsfehler beim Test", exc_info=True) - - # Gleichen Fehler nochmal (sollte gruppiert werden) - try: - raise ValueError("Test-Fehler für Error-Monitoring") - except Exception as e: - test_logger.error("Wiederholter Test Exception", exc_info=True) - - # Stats anzeigen - stats = monitor.get_stats() - print(f"📊 Current stats: {stats}") - - # Force Report - monitor.force_report() - - print("✅ Error-Monitoring-Test abgeschlossen") - -if __name__ == "__main__": - test_error_monitoring() \ No newline at end of file diff --git a/backend/app/utils/logging_config.py b/backend/app/utils/logging_config.py index 85f3957e..d8cda21b 100644 --- a/backend/app/utils/logging_config.py +++ b/backend/app/utils/logging_config.py @@ -464,30 +464,4 @@ def measure_execution_time(func=None, logger=None, task_name=None): if func: return decorator(func) - return decorator - -# Error-Monitoring Integration -def setup_error_monitoring(): - """ - Initialisiert das Error-Monitoring-System für automatische E-Mail-Berichte. - """ - try: - from utils.error_monitor import install_error_monitoring - - # Error-Monitoring für wichtige Logger aktivieren - critical_loggers = [ - 'app', # Hauptanwendung - 'auth', # Authentifizierung - 'jobs', # Job-Management - 'printers', # Drucker-System - 'scheduler', # Job-Scheduler - 'database', # Datenbank-Operationen - 'security' # Sicherheit - ] - - install_error_monitoring(critical_loggers) - - print("🔍 Error-Monitoring für kritische Systeme aktiviert") - - except Exception as e: - print(f"❌ Fehler beim Initialisieren des Error-Monitoring: {str(e)}") \ No newline at end of file + return decorator \ No newline at end of file