# -*- coding: utf-8 -*- """ File-Log-Router mit ContextVar-basiertem Routing. Routet Log-Nachrichten automatisch in separate Dateien: - core.log fuer alle Core-Komponenten - plugins/.log fuer jedes Plugin """ from __future__ import annotations import logging from contextvars import ContextVar from contextlib import contextmanager from pathlib import Path from typing import Generator from trixy_core.utils.logging.handlers import RotatingFileHandler from trixy_core.utils.logging.formatters import TextFormatter # ContextVar bestimmt die aktuelle Log-Komponente (Default: "core") _log_component: ContextVar[str] = ContextVar("_log_component", default="core") # Globale Instanz _file_router: FileLogRouter | None = None class FileLogRouter: """ Routet Log-Nachrichten basierend auf ContextVar in separate Dateien. Erstellt automatisch Logger fuer Core und Plugins mit RotatingFileHandler. """ def __init__( self, log_directory: str = "logs", file_log_level: str = "INFO", max_file_size_mb: int = 10, backup_count: int = 5, ) -> None: """ Initialisiert den FileLogRouter. Args: log_directory: Basis-Verzeichnis fuer Log-Dateien file_log_level: Minimales Log-Level fuer Datei-Ausgabe max_file_size_mb: Maximale Dateigroesse vor Rotation backup_count: Anzahl der Backup-Dateien """ self._log_directory = Path(log_directory) self._file_log_level = getattr(logging, file_log_level.upper(), logging.INFO) self._max_bytes = max_file_size_mb * 1024 * 1024 self._backup_count = backup_count self._loggers: dict[str, logging.Logger] = {} self._handlers: dict[str, RotatingFileHandler] = {} def initialize(self) -> None: """Erstellt Verzeichnisse und initialisiert den Core-Logger.""" self._log_directory.mkdir(parents=True, exist_ok=True) (self._log_directory / "plugins").mkdir(parents=True, exist_ok=True) # Core-Logger sofort erstellen self._get_or_create_logger("core") def log(self, level: int, message: str) -> None: """ Routet eine Log-Nachricht zum passenden Logger. Args: level: logging-Level (logging.INFO, logging.ERROR, etc.) message: Formatierte Nachricht """ if level < self._file_log_level: return component = _log_component.get() logger = self._get_or_create_logger(component) logger.log(level, message) def _get_or_create_logger(self, component: str) -> logging.Logger: """ Gibt einen Logger fuer die Komponente zurueck (lazy creation). Args: component: "core" oder "plugin." Returns: Konfigurierter Logger """ if component in self._loggers: return self._loggers[component] # Log-Datei-Pfad bestimmen if component == "core": log_file = self._log_directory / "core.log" elif component.startswith("plugin."): plugin_name = component[len("plugin."):] log_file = self._log_directory / "plugins" / f"{plugin_name}.log" else: log_file = self._log_directory / f"{component}.log" # Logger erstellen (eigener Namespace, kein Propagation) logger_name = f"trixy.file.{component}" logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) # Handler filtert nach Level logger.propagate = False # RotatingFileHandler mit TextFormatter (ohne Farben) handler = RotatingFileHandler( filename=log_file, max_bytes=self._max_bytes, backup_count=self._backup_count, compress=True, ) formatter = TextFormatter( use_colors=False, show_source=False, include_context=False, include_correlation_id=False, ) handler.setFormatter(formatter) logger.addHandler(handler) self._loggers[component] = logger self._handlers[component] = handler return logger def shutdown(self) -> None: """Schliesst alle Handler und gibt Ressourcen frei.""" for handler in self._handlers.values(): handler.close() self._handlers.clear() self._loggers.clear() @contextmanager def log_as(component: str) -> Generator[None, None, None]: """ Context-Manager: Setzt die Log-Komponente temporaer. Alle pinfo/pdebug/perror/pwarn Aufrufe innerhalb dieses Kontexts werden in die Datei der angegebenen Komponente geroutet. Args: component: "core" oder "plugin." Beispiel: with log_as("plugin.timer"): pinfo("Timer gestartet") # -> logs/plugins/timer.log """ token = _log_component.set(component) try: yield finally: _log_component.reset(token) def get_file_router() -> FileLogRouter | None: """Gibt die globale FileLogRouter-Instanz zurueck.""" return _file_router def init_file_router( log_directory: str = "logs", file_log_level: str = "INFO", max_file_size_mb: int = 10, backup_count: int = 5, ) -> FileLogRouter: """ Erstellt und initialisiert die globale FileLogRouter-Instanz. Args: log_directory: Basis-Verzeichnis fuer Log-Dateien file_log_level: Minimales Log-Level fuer Datei-Ausgabe max_file_size_mb: Maximale Dateigroesse vor Rotation backup_count: Anzahl der Backup-Dateien Returns: Initialisierte FileLogRouter-Instanz """ global _file_router router = FileLogRouter( log_directory=log_directory, file_log_level=file_log_level, max_file_size_mb=max_file_size_mb, backup_count=backup_count, ) router.initialize() _file_router = router return router def shutdown_file_router() -> None: """Faehrt die globale FileLogRouter-Instanz herunter.""" global _file_router if _file_router is not None: _file_router.shutdown() _file_router = None