Projektarbeit-MYP/backend/utils/logging_config.py
2025-05-31 22:40:29 +02:00

467 lines
16 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import logging.handlers
import os
import sys
import time
import platform
import socket
from typing import Dict, Optional, Any
from datetime import datetime
from config.settings import (
LOG_DIR, LOG_SUBDIRS, LOG_LEVEL, LOG_FORMAT, LOG_DATE_FORMAT,
get_log_file, ensure_log_directories
)
# Dictionary zur Speicherung der konfigurierten Logger
_loggers: Dict[str, logging.Logger] = {}
# ANSI-Farbcodes für Log-Level
ANSI_COLORS = {
'RESET': '\033[0m',
'BOLD': '\033[1m',
'BLACK': '\033[30m',
'RED': '\033[31m',
'GREEN': '\033[32m',
'YELLOW': '\033[33m',
'BLUE': '\033[34m',
'MAGENTA': '\033[35m',
'CYAN': '\033[36m',
'WHITE': '\033[37m',
'BG_RED': '\033[41m',
'BG_GREEN': '\033[42m',
'BG_YELLOW': '\033[43m',
'BG_BLUE': '\033[44m'
}
# Emojis für verschiedene Log-Level und Kategorien
LOG_EMOJIS = {
'DEBUG': '🔍',
'INFO': '',
'WARNING': '⚠️',
'ERROR': '',
'CRITICAL': '🔥',
'app': '🖥️',
'scheduler': '⏱️',
'auth': '🔐',
'jobs': '🖨️',
'printers': '🔧',
'errors': '💥',
'user': '👤',
'kiosk': '📺'
}
# ASCII-Fallback für Emojis bei Encoding-Problemen
EMOJI_FALLBACK = {
'🔍': '[DEBUG]',
'': '[INFO]',
'⚠️': '[WARN]',
'': '[ERROR]',
'🔥': '[CRIT]',
'🖥️': '[APP]',
'⏱️': '[SCHED]',
'🔐': '[AUTH]',
'🖨️': '[JOBS]',
'🔧': '[PRINT]',
'💥': '[ERR]',
'👤': '[USER]',
'📺': '[KIOSK]',
'🐞': '[BUG]',
'🚀': '[START]',
'📂': '[FOLDER]',
'📊': '[CHART]',
'💻': '[PC]',
'🌐': '[WEB]',
'📅': '[TIME]',
'📡': '[SIGNAL]',
'🧩': '[CONTENT]',
'📋': '[HEADER]',
'': '[OK]',
'📦': '[SIZE]'
}
def safe_emoji(emoji: str) -> str:
"""Gibt ein Emoji zurück oder einen ASCII-Fallback bei Encoding-Problemen."""
try:
# Erste Priorität: Teste, ob das Emoji dargestellt werden kann
test_encoding = sys.stdout.encoding or 'utf-8'
emoji.encode(test_encoding)
# Zweite Prüfung: Windows-spezifische cp1252-Codierung
if os.name == 'nt':
try:
emoji.encode('cp1252')
except UnicodeEncodeError:
# Wenn cp1252 fehlschlägt, verwende Fallback
return EMOJI_FALLBACK.get(emoji, '[?]')
return emoji
except (UnicodeEncodeError, LookupError, AttributeError):
return EMOJI_FALLBACK.get(emoji, '[?]')
# Prüfen, ob das Terminal ANSI-Farben unterstützt
def supports_color() -> bool:
"""Prüft, ob das Terminal ANSI-Farben unterstützt."""
if os.name == 'nt':
try:
import ctypes
kernel32 = ctypes.windll.kernel32
# Aktiviere VT100-Unterstützung unter Windows
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
# Setze Console-Output auf UTF-8 für bessere Emoji-Unterstützung
try:
kernel32.SetConsoleOutputCP(65001) # UTF-8
except:
pass
# Versuche UTF-8-Encoding für Emojis zu setzen
try:
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except:
try:
# Fallback für deutsche Lokalisierung
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
except:
pass
return True
except:
return False
else:
return sys.stdout.isatty()
USE_COLORS = supports_color()
class ColoredFormatter(logging.Formatter):
"""Formatter, der Farben und Emojis für Logs hinzufügt."""
level_colors = {
'DEBUG': ANSI_COLORS['CYAN'],
'INFO': ANSI_COLORS['GREEN'],
'WARNING': ANSI_COLORS['YELLOW'],
'ERROR': ANSI_COLORS['RED'],
'CRITICAL': ANSI_COLORS['BG_RED'] + ANSI_COLORS['WHITE'] + ANSI_COLORS['BOLD']
}
def format(self, record):
try:
# Basis-Format erstellen
log_fmt = LOG_FORMAT
date_fmt = LOG_DATE_FORMAT
# Emoji dem Level und der Kategorie hinzufügen
level_name = record.levelname
category_name = record.name.split('.')[-1] if '.' in record.name else record.name
level_emoji = safe_emoji(LOG_EMOJIS.get(level_name, ''))
category_emoji = safe_emoji(LOG_EMOJIS.get(category_name, ''))
# Record-Objekt modifizieren (aber temporär)
original_levelname = record.levelname
original_name = record.name
# Emojis hinzufügen
record.levelname = f"{level_emoji} {level_name}"
record.name = f"{category_emoji} {category_name}"
# Farbe hinzufügen wenn unterstützt
if USE_COLORS:
level_color = self.level_colors.get(original_levelname, ANSI_COLORS['RESET'])
record.levelname = f"{level_color}{record.levelname}{ANSI_COLORS['RESET']}"
record.name = f"{ANSI_COLORS['BOLD']}{record.name}{ANSI_COLORS['RESET']}"
# Formatieren
result = super().format(record)
# Originale Werte wiederherstellen
record.levelname = original_levelname
record.name = original_name
return result
except (UnicodeEncodeError, UnicodeDecodeError, AttributeError) as e:
# Fallback bei Unicode-Problemen: Verwende nur ASCII-Text
original_levelname = record.levelname
original_name = record.name
# Emojis durch ASCII-Fallbacks ersetzen
level_fallback = EMOJI_FALLBACK.get(LOG_EMOJIS.get(original_levelname, ''), '[LOG]')
category_name = record.name.split('.')[-1] if '.' in record.name else record.name
category_fallback = EMOJI_FALLBACK.get(LOG_EMOJIS.get(category_name, ''), '[CAT]')
record.levelname = f"{level_fallback} {original_levelname}"
record.name = f"{category_fallback} {category_name}"
# Basis-Formatierung ohne Farben
result = super().format(record)
# Originale Werte wiederherstellen
record.levelname = original_levelname
record.name = original_name
return result
class DebugInfoFilter(logging.Filter):
"""Filter, der Debug-Informationen zu jedem Log-Eintrag hinzufügt."""
def __init__(self, add_hostname=True, add_process_info=True):
super().__init__()
self.add_hostname = add_hostname
self.add_process_info = add_process_info
self.hostname = socket.gethostname() if add_hostname else None
self.pid = os.getpid() if add_process_info else None
def filter(self, record):
# Debug-Informationen hinzufügen
if self.add_hostname and not hasattr(record, 'hostname'):
record.hostname = self.hostname
if self.add_process_info and not hasattr(record, 'pid'):
record.pid = self.pid
# Zusätzliche Infos für DEBUG-Level
if record.levelno == logging.DEBUG:
# Funktionsname und Zeilennummer hervorheben
if USE_COLORS:
record.funcName = f"{ANSI_COLORS['CYAN']}{record.funcName}{ANSI_COLORS['RESET']}"
record.lineno = f"{ANSI_COLORS['CYAN']}{record.lineno}{ANSI_COLORS['RESET']}"
return True
def setup_logging(debug_mode: bool = False):
"""
Initialisiert das Logging-System und erstellt alle erforderlichen Verzeichnisse.
Args:
debug_mode: Wenn True, wird das Log-Level auf DEBUG gesetzt
"""
ensure_log_directories()
# Log-Level festlegen
log_level = logging.DEBUG if debug_mode else getattr(logging, LOG_LEVEL)
# Root-Logger konfigurieren
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# Alle Handler entfernen
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Formatter erstellen (mit und ohne Farben)
colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT)
file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Filter für zusätzliche Debug-Informationen
debug_filter = DebugInfoFilter()
# Console Handler für alle Logs
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(colored_formatter)
console_handler.addFilter(debug_filter)
# Windows PowerShell UTF-8 Encoding-Unterstützung
if os.name == 'nt' and hasattr(console_handler.stream, 'reconfigure'):
try:
console_handler.stream.reconfigure(encoding='utf-8')
except:
pass
root_logger.addHandler(console_handler)
# File Handler für allgemeine App-Logs
app_log_file = get_log_file("app")
app_handler = logging.handlers.RotatingFileHandler(
app_log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
)
app_handler.setLevel(log_level)
app_handler.setFormatter(file_formatter)
root_logger.addHandler(app_handler)
# Wenn Debug-Modus aktiv, Konfiguration loggen
if debug_mode:
bug_emoji = safe_emoji("🐞")
root_logger.debug(f"{bug_emoji} Debug-Modus aktiviert - Ausführliche Logs werden generiert")
def get_logger(category: str) -> logging.Logger:
"""
Gibt einen konfigurierten Logger für eine bestimmte Kategorie zurück.
Args:
category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors)
Returns:
logging.Logger: Konfigurierter Logger
"""
if category in _loggers:
return _loggers[category]
# Logger erstellen
logger = logging.getLogger(f"myp.{category}")
logger.setLevel(getattr(logging, LOG_LEVEL))
# Verhindere doppelte Logs durch Parent-Logger
logger.propagate = False
# Formatter erstellen (mit und ohne Farben)
colored_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT)
file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
# Filter für zusätzliche Debug-Informationen
debug_filter = DebugInfoFilter()
# Console Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, LOG_LEVEL))
console_handler.setFormatter(colored_formatter)
console_handler.addFilter(debug_filter)
# Windows PowerShell UTF-8 Encoding-Unterstützung
if os.name == 'nt' and hasattr(console_handler.stream, 'reconfigure'):
try:
console_handler.stream.reconfigure(encoding='utf-8')
except:
pass
logger.addHandler(console_handler)
# File Handler für spezifische Kategorie
log_file = get_log_file(category)
file_handler = logging.handlers.RotatingFileHandler(
log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
)
file_handler.setLevel(getattr(logging, LOG_LEVEL))
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Error-Logs zusätzlich in errors.log schreiben
if category != "errors":
error_log_file = get_log_file("errors")
error_handler = logging.handlers.RotatingFileHandler(
error_log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(file_formatter)
logger.addHandler(error_handler)
_loggers[category] = logger
return logger
def log_startup_info():
"""Loggt Startup-Informationen."""
app_logger = get_logger("app")
rocket_emoji = safe_emoji("🚀")
folder_emoji = safe_emoji("📂")
chart_emoji = safe_emoji("📊")
computer_emoji = safe_emoji("💻")
globe_emoji = safe_emoji("🌐")
calendar_emoji = safe_emoji("📅")
app_logger.info("=" * 50)
app_logger.info(f"{rocket_emoji} MYP (Manage Your Printers) wird gestartet...")
app_logger.info(f"{folder_emoji} Log-Verzeichnis: {LOG_DIR}")
app_logger.info(f"{chart_emoji} Log-Level: {LOG_LEVEL}")
app_logger.info(f"{computer_emoji} Betriebssystem: {platform.system()} {platform.release()}")
app_logger.info(f"{globe_emoji} Hostname: {socket.gethostname()}")
app_logger.info(f"{calendar_emoji} Startzeit: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
app_logger.info("=" * 50)
# Hilfsfunktionen für das Debugging
def debug_request(logger: logging.Logger, request):
"""
Loggt detaillierte Informationen über eine HTTP-Anfrage.
Args:
logger: Logger-Instanz
request: Flask-Request-Objekt
"""
if logger.level > logging.DEBUG:
return
web_emoji = safe_emoji("🌐")
signal_emoji = safe_emoji("📡")
puzzle_emoji = safe_emoji("🧩")
clipboard_emoji = safe_emoji("📋")
search_emoji = safe_emoji("🔍")
logger.debug(f"{web_emoji} HTTP-Anfrage: {request.method} {request.path}")
logger.debug(f"{signal_emoji} Remote-Adresse: {request.remote_addr}")
logger.debug(f"{puzzle_emoji} Inhaltstyp: {request.content_type}")
# Nur relevante Headers ausgeben
important_headers = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Authorization']
headers = {k: v for k, v in request.headers.items() if k in important_headers}
if headers:
logger.debug(f"{clipboard_emoji} Wichtige Headers: {headers}")
# Request-Parameter (max. 1000 Zeichen)
if request.args:
args_str = str(request.args)
if len(args_str) > 1000:
args_str = args_str[:997] + "..."
logger.debug(f"{search_emoji} URL-Parameter: {args_str}")
def debug_response(logger: logging.Logger, response, duration_ms: float = None):
"""
Loggt detaillierte Informationen über eine HTTP-Antwort.
Args:
logger: Logger-Instanz
response: Flask-Response-Objekt
duration_ms: Verarbeitungsdauer in Millisekunden (optional)
"""
if logger.level > logging.DEBUG:
return
status_emoji = safe_emoji("") if response.status_code < 400 else safe_emoji("")
logger.debug(f"{status_emoji} HTTP-Antwort: {response.status_code}")
if duration_ms is not None:
timer_emoji = safe_emoji("⏱️")
logger.debug(f"{timer_emoji} Verarbeitungsdauer: {duration_ms:.2f} ms")
content_length = response.content_length or 0
if content_length > 0:
size_str = f"{content_length / 1024:.1f} KB" if content_length > 1024 else f"{content_length} Bytes"
package_emoji = safe_emoji("📦")
logger.debug(f"{package_emoji} Antwortgröße: {size_str}")
def measure_execution_time(func=None, logger=None, task_name=None):
"""
Dekorator, der die Ausführungszeit einer Funktion misst und loggt.
Args:
func: Die zu dekorierende Funktion
logger: Logger-Instanz (optional)
task_name: Name der Aufgabe für das Logging (optional)
Returns:
Dekorierte Funktion
"""
from functools import wraps
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
start_time = time.time()
result = f(*args, **kwargs)
end_time = time.time()
duration_ms = (end_time - start_time) * 1000
name = task_name or f.__name__
if logger:
timer_emoji = safe_emoji('⏱️')
if duration_ms > 1000: # Länger als 1 Sekunde
logger.warning(f"{timer_emoji} Langsame Ausführung: {name} - {duration_ms:.2f} ms")
else:
logger.debug(f"{timer_emoji} Ausführungszeit: {name} - {duration_ms:.2f} ms")
return result
return wrapper
if func:
return decorator(func)
return decorator