| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- # -*- 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)
|