file_router.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # -*- coding: utf-8 -*-
  2. """
  3. File-Log-Router mit ContextVar-basiertem Routing.
  4. Routet Log-Nachrichten automatisch in separate Dateien:
  5. - core.log fuer alle Core-Komponenten
  6. - plugins/<name>.log fuer jedes Plugin
  7. """
  8. from __future__ import annotations
  9. import logging
  10. from contextvars import ContextVar
  11. from contextlib import contextmanager
  12. from pathlib import Path
  13. from typing import Generator
  14. from trixy_core.utils.logging.handlers import RotatingFileHandler
  15. from trixy_core.utils.logging.formatters import TextFormatter
  16. # ContextVar bestimmt die aktuelle Log-Komponente (Default: "core")
  17. _log_component: ContextVar[str] = ContextVar("_log_component", default="core")
  18. # Globale Instanz
  19. _file_router: FileLogRouter | None = None
  20. class FileLogRouter:
  21. """
  22. Routet Log-Nachrichten basierend auf ContextVar in separate Dateien.
  23. Erstellt automatisch Logger fuer Core und Plugins mit RotatingFileHandler.
  24. """
  25. def __init__(
  26. self,
  27. log_directory: str = "logs",
  28. file_log_level: str = "INFO",
  29. max_file_size_mb: int = 10,
  30. backup_count: int = 5,
  31. ) -> None:
  32. """
  33. Initialisiert den FileLogRouter.
  34. Args:
  35. log_directory: Basis-Verzeichnis fuer Log-Dateien
  36. file_log_level: Minimales Log-Level fuer Datei-Ausgabe
  37. max_file_size_mb: Maximale Dateigroesse vor Rotation
  38. backup_count: Anzahl der Backup-Dateien
  39. """
  40. self._log_directory = Path(log_directory)
  41. self._file_log_level = getattr(logging, file_log_level.upper(), logging.INFO)
  42. self._max_bytes = max_file_size_mb * 1024 * 1024
  43. self._backup_count = backup_count
  44. self._loggers: dict[str, logging.Logger] = {}
  45. self._handlers: dict[str, RotatingFileHandler] = {}
  46. def initialize(self) -> None:
  47. """Erstellt Verzeichnisse und initialisiert den Core-Logger."""
  48. self._log_directory.mkdir(parents=True, exist_ok=True)
  49. (self._log_directory / "plugins").mkdir(parents=True, exist_ok=True)
  50. # Core-Logger sofort erstellen
  51. self._get_or_create_logger("core")
  52. def log(self, level: int, message: str) -> None:
  53. """
  54. Routet eine Log-Nachricht zum passenden Logger.
  55. Args:
  56. level: logging-Level (logging.INFO, logging.ERROR, etc.)
  57. message: Formatierte Nachricht
  58. """
  59. if level < self._file_log_level:
  60. return
  61. component = _log_component.get()
  62. logger = self._get_or_create_logger(component)
  63. logger.log(level, message)
  64. def _get_or_create_logger(self, component: str) -> logging.Logger:
  65. """
  66. Gibt einen Logger fuer die Komponente zurueck (lazy creation).
  67. Args:
  68. component: "core" oder "plugin.<name>"
  69. Returns:
  70. Konfigurierter Logger
  71. """
  72. if component in self._loggers:
  73. return self._loggers[component]
  74. # Log-Datei-Pfad bestimmen
  75. if component == "core":
  76. log_file = self._log_directory / "core.log"
  77. elif component.startswith("plugin."):
  78. plugin_name = component[len("plugin."):]
  79. log_file = self._log_directory / "plugins" / f"{plugin_name}.log"
  80. else:
  81. log_file = self._log_directory / f"{component}.log"
  82. # Logger erstellen (eigener Namespace, kein Propagation)
  83. logger_name = f"trixy.file.{component}"
  84. logger = logging.getLogger(logger_name)
  85. logger.setLevel(logging.DEBUG) # Handler filtert nach Level
  86. logger.propagate = False
  87. # RotatingFileHandler mit TextFormatter (ohne Farben)
  88. handler = RotatingFileHandler(
  89. filename=log_file,
  90. max_bytes=self._max_bytes,
  91. backup_count=self._backup_count,
  92. compress=True,
  93. )
  94. formatter = TextFormatter(
  95. use_colors=False,
  96. show_source=False,
  97. include_context=False,
  98. include_correlation_id=False,
  99. )
  100. handler.setFormatter(formatter)
  101. logger.addHandler(handler)
  102. self._loggers[component] = logger
  103. self._handlers[component] = handler
  104. return logger
  105. def shutdown(self) -> None:
  106. """Schliesst alle Handler und gibt Ressourcen frei."""
  107. for handler in self._handlers.values():
  108. handler.close()
  109. self._handlers.clear()
  110. self._loggers.clear()
  111. @contextmanager
  112. def log_as(component: str) -> Generator[None, None, None]:
  113. """
  114. Context-Manager: Setzt die Log-Komponente temporaer.
  115. Alle pinfo/pdebug/perror/pwarn Aufrufe innerhalb dieses
  116. Kontexts werden in die Datei der angegebenen Komponente geroutet.
  117. Args:
  118. component: "core" oder "plugin.<name>"
  119. Beispiel:
  120. with log_as("plugin.timer"):
  121. pinfo("Timer gestartet") # -> logs/plugins/timer.log
  122. """
  123. token = _log_component.set(component)
  124. try:
  125. yield
  126. finally:
  127. _log_component.reset(token)
  128. def get_file_router() -> FileLogRouter | None:
  129. """Gibt die globale FileLogRouter-Instanz zurueck."""
  130. return _file_router
  131. def init_file_router(
  132. log_directory: str = "logs",
  133. file_log_level: str = "INFO",
  134. max_file_size_mb: int = 10,
  135. backup_count: int = 5,
  136. ) -> FileLogRouter:
  137. """
  138. Erstellt und initialisiert die globale FileLogRouter-Instanz.
  139. Args:
  140. log_directory: Basis-Verzeichnis fuer Log-Dateien
  141. file_log_level: Minimales Log-Level fuer Datei-Ausgabe
  142. max_file_size_mb: Maximale Dateigroesse vor Rotation
  143. backup_count: Anzahl der Backup-Dateien
  144. Returns:
  145. Initialisierte FileLogRouter-Instanz
  146. """
  147. global _file_router
  148. router = FileLogRouter(
  149. log_directory=log_directory,
  150. file_log_level=file_log_level,
  151. max_file_size_mb=max_file_size_mb,
  152. backup_count=backup_count,
  153. )
  154. router.initialize()
  155. _file_router = router
  156. return router
  157. def shutdown_file_router() -> None:
  158. """Faehrt die globale FileLogRouter-Instanz herunter."""
  159. global _file_router
  160. if _file_router is not None:
  161. _file_router.shutdown()
  162. _file_router = None