| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- # -*- coding: utf-8 -*-
- """
- File-Log-Router mit ContextVar-basiertem Routing.
- Routet Log-Nachrichten automatisch in separate Dateien:
- - core.log fuer alle Core-Komponenten
- - plugins/<name>.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.<name>"
- 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.<name>"
- 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
|