🐛 Backend Optimization: Removed error monitoring configuration files and utilities, refactored logging configuration for improved performance. 🎉

This commit is contained in:
Till Tomczak 2025-05-30 20:37:02 +02:00
parent 5543e9ba5e
commit d1a6281577
4 changed files with 56 additions and 838 deletions

View File

@ -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

View File

@ -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()"

View File

@ -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()

View File

@ -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)}")
return decorator