From 5543e9ba5e630a2b9a9cabfef1bb41b2ca9a1d57 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Fri, 30 May 2025 20:25:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9A=20Improved=20error=20monitoring=20?= =?UTF-8?q?configuration=20&=20utility=20files=20=F0=9F=96=A5=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/app.py | 57 ++ .../app/config/error_monitoring.env.example | 106 ++++ backend/app/utils/error_monitor.py | 593 ++++++++++++++++++ backend/app/utils/logging_config.py | 28 +- 4 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 backend/app/config/error_monitoring.env.example create mode 100644 backend/app/utils/error_monitor.py diff --git a/backend/app/app.py b/backend/app/app.py index c5cc2bb19..9b84e72e6 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -222,6 +222,13 @@ 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") @@ -4859,4 +4866,54 @@ 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 new file mode 100644 index 000000000..352bff7a6 --- /dev/null +++ b/backend/app/config/error_monitoring.env.example @@ -0,0 +1,106 @@ +# 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 new file mode 100644 index 000000000..b6a300f15 --- /dev/null +++ b/backend/app/utils/error_monitor.py @@ -0,0 +1,593 @@ +""" +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 d8cda21b6..85f3957e7 100644 --- a/backend/app/utils/logging_config.py +++ b/backend/app/utils/logging_config.py @@ -464,4 +464,30 @@ def measure_execution_time(func=None, logger=None, task_name=None): if func: return decorator(func) - return decorator \ No newline at end of file + 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