# -*- coding: utf-8 -*- """ Strukturierter Logger für enterprise-grade Logging. Bietet JSON-fähiges Logging mit automatischer Kontext-Propagation, strukturierten Feldern und Integration mit dem Trixy-Debug-System. """ from __future__ import annotations import logging import sys from contextlib import contextmanager from dataclasses import dataclass, field from datetime import datetime from typing import Any, Callable, Iterator from trixy_core.utils.logging.context import ( LogContext, get_context, get_correlation_id, log_context, ) from trixy_core.utils.logging.formatters import JSONFormatter, TextFormatter # Logger-Registry für zentrale Verwaltung _loggers: dict[str, "StructuredLogger"] = {} @dataclass class LogEntry: """ Strukturierter Log-Eintrag mit allen Metadaten. Repräsentiert einen einzelnen Log-Eintrag mit Timestamp, Level, Nachricht und kontextuellen Daten. """ level: str message: str timestamp: datetime = field(default_factory=datetime.now) logger_name: str = "" correlation_id: str = "" context: dict[str, Any] = field(default_factory=dict) extra: dict[str, Any] = field(default_factory=dict) exception: str | None = None source: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: """Konvertiert den Eintrag in ein Dictionary.""" result = { "timestamp": self.timestamp.isoformat(), "level": self.level, "message": self.message, "logger": self.logger_name, } if self.correlation_id: result["correlation_id"] = self.correlation_id if self.context: result["context"] = self.context if self.extra: result["extra"] = self.extra if self.exception: result["exception"] = self.exception if self.source: result["source"] = self.source return result class StructuredLogger: """ Strukturierter Logger mit JSON-Output und Kontext-Unterstützung. Erweitert den Standard-Python-Logger um strukturierte Felder, automatische Korrelations-IDs und flexible Ausgabeformate. Example: logger = StructuredLogger("my_service") logger.info("User logged in", user_id="123", action="login") with logger.context(request_id="abc"): logger.debug("Processing request") """ # Log-Level-Mapping LEVELS = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, } def __init__( self, name: str, level: int | str = logging.INFO, use_json: bool = False, propagate: bool = True, handlers: list[logging.Handler] | None = None, ) -> None: """ Initialisiert den strukturierten Logger. Args: name: Name des Loggers (typischerweise Modul-/Servicename). level: Minimales Log-Level (int oder String). use_json: Ob JSON-Format für die Ausgabe verwendet werden soll. propagate: Ob Logs an Parent-Logger weitergegeben werden. handlers: Optionale Liste von Handlern. """ self.name = name self._logger = logging.getLogger(name) # Level setzen if isinstance(level, str): level = self.LEVELS.get(level.upper(), logging.INFO) self._logger.setLevel(level) self._logger.propagate = propagate self._use_json = use_json self._default_extra: dict[str, Any] = {} self._hooks: list[Callable[[LogEntry], LogEntry | None]] = [] # Handler konfigurieren if handlers: for handler in handlers: self._logger.addHandler(handler) elif not self._logger.handlers: # Standard-Handler hinzufügen handler = logging.StreamHandler(sys.stdout) if use_json: handler.setFormatter(JSONFormatter()) else: handler.setFormatter(TextFormatter()) self._logger.addHandler(handler) @property def level(self) -> int: """Gibt das aktuelle Log-Level zurück.""" return self._logger.level @level.setter def level(self, value: int | str) -> None: """Setzt das Log-Level.""" if isinstance(value, str): value = self.LEVELS.get(value.upper(), logging.INFO) self._logger.setLevel(value) def set_default_extra(self, **kwargs: Any) -> None: """ Setzt Standard-Extra-Felder für alle Log-Nachrichten. Args: **kwargs: Schlüssel-Wert-Paare für Extra-Felder. """ self._default_extra.update(kwargs) def clear_default_extra(self) -> None: """Löscht alle Standard-Extra-Felder.""" self._default_extra.clear() def add_hook( self, hook: Callable[[LogEntry], LogEntry | None] ) -> None: """ Fügt einen Hook hinzu, der vor jedem Log aufgerufen wird. Der Hook kann den LogEntry modifizieren oder None zurückgeben, um das Logging zu unterdrücken. Args: hook: Funktion, die LogEntry verarbeitet. """ self._hooks.append(hook) def remove_hook( self, hook: Callable[[LogEntry], LogEntry | None] ) -> None: """Entfernt einen Hook.""" self._hooks.remove(hook) def _create_entry( self, level: str, message: str, exc_info: bool = False, **extra: Any, ) -> LogEntry: """Erstellt einen LogEntry mit allen Metadaten.""" # Extra-Felder kombinieren combined_extra = {**self._default_extra, **extra} entry = LogEntry( level=level, message=message, logger_name=self.name, correlation_id=get_correlation_id(), context=get_context(), extra=combined_extra, ) return entry def _apply_hooks(self, entry: LogEntry) -> LogEntry | None: """Wendet alle registrierten Hooks an.""" for hook in self._hooks: result = hook(entry) if result is None: return None entry = result return entry def _log( self, level: int, level_name: str, message: str, exc_info: bool = False, **extra: Any, ) -> None: """Interne Log-Methode.""" if not self._logger.isEnabledFor(level): return # LogEntry erstellen entry = self._create_entry(level_name, message, exc_info, **extra) # Hooks anwenden entry = self._apply_hooks(entry) if entry is None: return # Python-Logger aufrufen self._logger.log( level, message, exc_info=exc_info, extra={"structured_extra": entry.extra}, ) def debug(self, message: str, **extra: Any) -> None: """ Loggt eine Debug-Nachricht. Args: message: Die Log-Nachricht. **extra: Zusätzliche strukturierte Felder. """ self._log(logging.DEBUG, "DEBUG", message, **extra) def info(self, message: str, **extra: Any) -> None: """ Loggt eine Info-Nachricht. Args: message: Die Log-Nachricht. **extra: Zusätzliche strukturierte Felder. """ self._log(logging.INFO, "INFO", message, **extra) def warning(self, message: str, **extra: Any) -> None: """ Loggt eine Warnung. Args: message: Die Log-Nachricht. **extra: Zusätzliche strukturierte Felder. """ self._log(logging.WARNING, "WARNING", message, **extra) def warn(self, message: str, **extra: Any) -> None: """Alias für warning().""" self.warning(message, **extra) def error(self, message: str, exc_info: bool = False, **extra: Any) -> None: """ Loggt eine Fehlermeldung. Args: message: Die Log-Nachricht. exc_info: Ob Exception-Info inkludiert werden soll. **extra: Zusätzliche strukturierte Felder. """ self._log(logging.ERROR, "ERROR", message, exc_info=exc_info, **extra) def critical(self, message: str, exc_info: bool = False, **extra: Any) -> None: """ Loggt eine kritische Meldung. Args: message: Die Log-Nachricht. exc_info: Ob Exception-Info inkludiert werden soll. **extra: Zusätzliche strukturierte Felder. """ self._log( logging.CRITICAL, "CRITICAL", message, exc_info=exc_info, **extra ) def exception(self, message: str, **extra: Any) -> None: """ Loggt eine Exception (ERROR-Level mit Traceback). Args: message: Die Log-Nachricht. **extra: Zusätzliche strukturierte Felder. """ self._log(logging.ERROR, "ERROR", message, exc_info=True, **extra) def log(self, level: int | str, message: str, **extra: Any) -> None: """ Loggt mit einem benutzerdefinierten Level. Args: level: Log-Level (int oder String). message: Die Log-Nachricht. **extra: Zusätzliche strukturierte Felder. """ if isinstance(level, str): level_int = self.LEVELS.get(level.upper(), logging.INFO) level_name = level.upper() else: level_int = level level_name = logging.getLevelName(level) self._log(level_int, level_name, message, **extra) @contextmanager def context(self, **kwargs: Any) -> Iterator[None]: """ Context-Manager für temporäre Extra-Felder. Args: **kwargs: Temporäre Extra-Felder. Example: with logger.context(request_id="123"): logger.info("Processing") # Enthält request_id """ with log_context(**kwargs): yield @contextmanager def timed( self, operation: str, level: int | str = logging.DEBUG, **extra: Any, ) -> Iterator[None]: """ Context-Manager für Zeitmessung. Loggt die Dauer der Operation beim Verlassen des Blocks. Args: operation: Name der Operation. level: Log-Level für die Ausgabe. **extra: Zusätzliche strukturierte Felder. Example: with logger.timed("database_query"): cursor.execute(query) # Loggt: "database_query completed in 0.123s" """ start = datetime.now() try: yield finally: duration = (datetime.now() - start).total_seconds() self.log( level, f"{operation} completed", duration_seconds=duration, operation=operation, **extra, ) def bind(self, **kwargs: Any) -> "BoundLogger": """ Erstellt einen gebundenen Logger mit festen Extra-Feldern. Args: **kwargs: Felder, die an alle Logs angehängt werden. Returns: BoundLogger mit den angegebenen Feldern. Example: user_logger = logger.bind(user_id="123") user_logger.info("Action performed") # Enthält user_id """ return BoundLogger(self, kwargs) def add_handler(self, handler: logging.Handler) -> None: """Fügt einen Handler hinzu.""" self._logger.addHandler(handler) def remove_handler(self, handler: logging.Handler) -> None: """Entfernt einen Handler.""" self._logger.removeHandler(handler) def get_child(self, suffix: str) -> "StructuredLogger": """ Erstellt einen Kind-Logger. Args: suffix: Suffix für den Logger-Namen. Returns: Neuer StructuredLogger mit erweitertem Namen. """ child_name = f"{self.name}.{suffix}" return get_logger(child_name, use_json=self._use_json) class BoundLogger: """ Logger mit vordefinierten Extra-Feldern. Wrapper um StructuredLogger, der automatisch bestimmte Felder an alle Log-Nachrichten anhängt. """ def __init__( self, logger: StructuredLogger, bound_fields: dict[str, Any], ) -> None: """ Initialisiert den gebundenen Logger. Args: logger: Der zugrunde liegende Logger. bound_fields: Felder, die an alle Logs angehängt werden. """ self._logger = logger self._bound_fields = bound_fields def _merge_extra(self, extra: dict[str, Any]) -> dict[str, Any]: """Kombiniert gebundene und übergebene Felder.""" return {**self._bound_fields, **extra} def debug(self, message: str, **extra: Any) -> None: """Loggt eine Debug-Nachricht mit gebundenen Feldern.""" self._logger.debug(message, **self._merge_extra(extra)) def info(self, message: str, **extra: Any) -> None: """Loggt eine Info-Nachricht mit gebundenen Feldern.""" self._logger.info(message, **self._merge_extra(extra)) def warning(self, message: str, **extra: Any) -> None: """Loggt eine Warnung mit gebundenen Feldern.""" self._logger.warning(message, **self._merge_extra(extra)) def warn(self, message: str, **extra: Any) -> None: """Alias für warning().""" self.warning(message, **extra) def error(self, message: str, exc_info: bool = False, **extra: Any) -> None: """Loggt einen Fehler mit gebundenen Feldern.""" self._logger.error(message, exc_info=exc_info, **self._merge_extra(extra)) def critical(self, message: str, exc_info: bool = False, **extra: Any) -> None: """Loggt eine kritische Meldung mit gebundenen Feldern.""" self._logger.critical( message, exc_info=exc_info, **self._merge_extra(extra) ) def exception(self, message: str, **extra: Any) -> None: """Loggt eine Exception mit gebundenen Feldern.""" self._logger.exception(message, **self._merge_extra(extra)) def bind(self, **kwargs: Any) -> "BoundLogger": """ Erstellt einen neuen gebundenen Logger mit zusätzlichen Feldern. Args: **kwargs: Zusätzliche Felder. Returns: Neuer BoundLogger mit kombinierten Feldern. """ new_fields = {**self._bound_fields, **kwargs} return BoundLogger(self._logger, new_fields) @contextmanager def context(self, **kwargs: Any) -> Iterator[None]: """Context-Manager für temporäre Extra-Felder.""" with self._logger.context(**kwargs): yield @contextmanager def timed( self, operation: str, level: int | str = logging.DEBUG, **extra: Any, ) -> Iterator[None]: """Context-Manager für Zeitmessung.""" with self._logger.timed( operation, level, **self._merge_extra(extra) ): yield def get_logger( name: str, level: int | str = logging.INFO, use_json: bool = False, ) -> StructuredLogger: """ Factory-Funktion für strukturierte Logger. Gibt gecachte Logger zurück, um Duplikate zu vermeiden. Args: name: Name des Loggers. level: Log-Level. use_json: Ob JSON-Format verwendet werden soll. Returns: StructuredLogger-Instanz. """ if name in _loggers: return _loggers[name] logger = StructuredLogger(name, level=level, use_json=use_json) _loggers[name] = logger return logger def configure_root_logger( level: int | str = logging.INFO, use_json: bool = False, handlers: list[logging.Handler] | None = None, ) -> None: """ Konfiguriert den Root-Logger. Args: level: Log-Level für den Root-Logger. use_json: Ob JSON-Format verwendet werden soll. handlers: Optionale Handler-Liste. """ root = logging.getLogger() if isinstance(level, str): level = StructuredLogger.LEVELS.get(level.upper(), logging.INFO) root.setLevel(level) # Alte Handler entfernen for handler in root.handlers[:]: root.removeHandler(handler) # Neue Handler hinzufügen if handlers: for handler in handlers: root.addHandler(handler) else: handler = logging.StreamHandler(sys.stdout) if use_json: handler.setFormatter(JSONFormatter()) else: handler.setFormatter(TextFormatter()) root.addHandler(handler)