📚 Improved error monitoring configuration & utility files 🖥️
This commit is contained in:
@@ -222,6 +222,13 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
|
|||||||
setup_logging()
|
setup_logging()
|
||||||
log_startup_info()
|
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
|
# Logger für verschiedene Komponenten
|
||||||
app_logger = get_logger("app")
|
app_logger = get_logger("app")
|
||||||
auth_logger = get_logger("auth")
|
auth_logger = get_logger("auth")
|
||||||
@@ -4860,3 +4867,53 @@ def get_printers():
|
|||||||
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
"error": f"Fehler beim Laden der Drucker: {str(e)}",
|
||||||
"printers": []
|
"printers": []
|
||||||
}), 500
|
}), 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
|
106
backend/app/config/error_monitoring.env.example
Normal file
106
backend/app/config/error_monitoring.env.example
Normal file
@@ -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()"
|
593
backend/app/utils/error_monitor.py
Normal file
593
backend/app/utils/error_monitor.py
Normal file
@@ -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()
|
@@ -465,3 +465,29 @@ def measure_execution_time(func=None, logger=None, task_name=None):
|
|||||||
if func:
|
if func:
|
||||||
return decorator(func)
|
return decorator(func)
|
||||||
return decorator
|
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)}")
|
Reference in New Issue
Block a user