logging.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # -*- coding: utf-8 -*-
  2. """
  3. Logging-Middleware für Events.
  4. Protokolliert Event-Verarbeitung für Debugging und Audit.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import logging
  9. from datetime import datetime
  10. from typing import TYPE_CHECKING, Any, Callable
  11. from trixy_core.events.middleware.base import (
  12. EventMiddleware,
  13. MiddlewareContext,
  14. MiddlewareResult,
  15. )
  16. if TYPE_CHECKING:
  17. from trixy_core.events.event_data.base import EventData
  18. class LoggingMiddleware(EventMiddleware):
  19. """
  20. Middleware für Event-Logging.
  21. Protokolliert Event-Namen, Daten und Verarbeitungszeiten.
  22. Example:
  23. middleware = LoggingMiddleware(
  24. logger=logging.getLogger("events"),
  25. log_data=True,
  26. log_metadata=True,
  27. )
  28. pipeline.add(middleware)
  29. """
  30. def __init__(
  31. self,
  32. name: str = "LoggingMiddleware",
  33. enabled: bool = True,
  34. logger: logging.Logger | None = None,
  35. log_level: int = logging.DEBUG,
  36. log_data: bool = False,
  37. log_metadata: bool = False,
  38. include_events: list[str] | None = None,
  39. exclude_events: list[str] | None = None,
  40. data_formatter: Callable[["EventData"], dict[str, Any]] | None = None,
  41. ) -> None:
  42. """
  43. Initialisiert die Logging-Middleware.
  44. Args:
  45. name: Name der Middleware.
  46. enabled: Ob aktiv.
  47. logger: Logger-Instanz (Standard: neuer Logger).
  48. log_level: Log-Level für Events.
  49. log_data: Ob Event-Daten geloggt werden.
  50. log_metadata: Ob Metadaten geloggt werden.
  51. include_events: Events die geloggt werden (oder alle).
  52. exclude_events: Events die nicht geloggt werden.
  53. data_formatter: Optionaler Formatter für Event-Daten.
  54. """
  55. super().__init__(name, enabled)
  56. self._logger = logger or logging.getLogger("trixy.events")
  57. self._log_level = log_level
  58. self._log_data = log_data
  59. self._log_metadata = log_metadata
  60. self._include_events = set(include_events) if include_events else None
  61. self._exclude_events = set(exclude_events) if exclude_events else set()
  62. self._data_formatter = data_formatter
  63. def should_process(self, event_name: str) -> bool:
  64. """Prüft, ob das Event geloggt werden soll."""
  65. if event_name in self._exclude_events:
  66. return False
  67. if self._include_events is not None:
  68. return event_name in self._include_events
  69. return True
  70. def _format_event_data(self, event_data: "EventData") -> dict[str, Any]:
  71. """
  72. Formatiert Event-Daten für das Logging.
  73. Args:
  74. event_data: Die Event-Daten.
  75. Returns:
  76. Dictionary-Repräsentation.
  77. """
  78. if self._data_formatter:
  79. return self._data_formatter(event_data)
  80. # Standard-Formatierung
  81. result: dict[str, Any] = {
  82. "event_id": event_data.event_id,
  83. "source": event_data.source,
  84. "cancelled": event_data.cancelled,
  85. }
  86. if self._log_metadata:
  87. result["metadata"] = event_data.metadata
  88. return result
  89. async def before(
  90. self,
  91. context: MiddlewareContext,
  92. ) -> MiddlewareResult:
  93. """
  94. Loggt den Event-Start.
  95. Args:
  96. context: Der Middleware-Kontext.
  97. Returns:
  98. Ergebnis (immer CONTINUE).
  99. """
  100. log_entry: dict[str, Any] = {
  101. "phase": "before",
  102. "event": context.event_name,
  103. "timestamp": context.started_at.isoformat(),
  104. }
  105. if self._log_data:
  106. log_entry["data"] = self._format_event_data(context.event_data)
  107. self._logger.log(
  108. self._log_level,
  109. f"Event START: {context.event_name}",
  110. extra={"event_log": log_entry},
  111. )
  112. return MiddlewareResult()
  113. async def after(
  114. self,
  115. context: MiddlewareContext,
  116. ) -> None:
  117. """
  118. Loggt das Event-Ende.
  119. Args:
  120. context: Der Middleware-Kontext.
  121. """
  122. log_entry: dict[str, Any] = {
  123. "phase": "after",
  124. "event": context.event_name,
  125. "duration_ms": context.elapsed_ms,
  126. "cancelled": context.event_data.cancelled,
  127. "skipped": context.skipped,
  128. "aborted": context.aborted,
  129. "error_count": len(context.errors),
  130. }
  131. if self._log_data:
  132. log_entry["data"] = self._format_event_data(context.event_data)
  133. level = self._log_level
  134. if context.errors:
  135. level = logging.WARNING
  136. self._logger.log(
  137. level,
  138. f"Event END: {context.event_name} ({context.elapsed_ms:.2f}ms)",
  139. extra={"event_log": log_entry},
  140. )
  141. async def on_error(
  142. self,
  143. context: MiddlewareContext,
  144. error: Exception,
  145. ) -> None:
  146. """
  147. Loggt Event-Fehler.
  148. Args:
  149. context: Der Middleware-Kontext.
  150. error: Der aufgetretene Fehler.
  151. """
  152. log_entry: dict[str, Any] = {
  153. "phase": "error",
  154. "event": context.event_name,
  155. "error_type": type(error).__name__,
  156. "error_message": str(error),
  157. "duration_ms": context.elapsed_ms,
  158. }
  159. self._logger.error(
  160. f"Event ERROR: {context.event_name} - {error}",
  161. extra={"event_log": log_entry},
  162. exc_info=True,
  163. )
  164. class StructuredLoggingMiddleware(EventMiddleware):
  165. """
  166. Middleware für strukturiertes JSON-Logging.
  167. Gibt Event-Informationen als JSON aus für Log-Aggregation.
  168. Example:
  169. middleware = StructuredLoggingMiddleware(
  170. output=sys.stdout.write,
  171. include_stack_trace=True,
  172. )
  173. """
  174. def __init__(
  175. self,
  176. name: str = "StructuredLoggingMiddleware",
  177. enabled: bool = True,
  178. output: Callable[[str], Any] | None = None,
  179. include_data: bool = True,
  180. include_stack_trace: bool = False,
  181. pretty_print: bool = False,
  182. ) -> None:
  183. """
  184. Initialisiert die Middleware.
  185. Args:
  186. name: Name der Middleware.
  187. enabled: Ob aktiv.
  188. output: Output-Funktion (Standard: print).
  189. include_data: Ob Event-Daten inkludiert werden.
  190. include_stack_trace: Ob Stack-Traces bei Fehlern inkludiert werden.
  191. pretty_print: Ob JSON formatiert ausgegeben wird.
  192. """
  193. super().__init__(name, enabled)
  194. self._output = output or print
  195. self._include_data = include_data
  196. self._include_stack_trace = include_stack_trace
  197. self._pretty_print = pretty_print
  198. def _emit(self, log_entry: dict[str, Any]) -> None:
  199. """
  200. Gibt einen Log-Eintrag aus.
  201. Args:
  202. log_entry: Der Log-Eintrag.
  203. """
  204. if self._pretty_print:
  205. json_str = json.dumps(log_entry, indent=2, default=str)
  206. else:
  207. json_str = json.dumps(log_entry, default=str)
  208. self._output(json_str)
  209. async def before(
  210. self,
  211. context: MiddlewareContext,
  212. ) -> MiddlewareResult:
  213. """Loggt Event-Start als JSON."""
  214. log_entry = {
  215. "@timestamp": datetime.now().isoformat(),
  216. "level": "INFO",
  217. "type": "event",
  218. "phase": "start",
  219. "event_name": context.event_name,
  220. "event_id": context.event_data.event_id,
  221. "source": context.event_data.source,
  222. }
  223. if self._include_data:
  224. log_entry["data"] = {
  225. k: v for k, v in context.event_data.__dict__.items()
  226. if not k.startswith("_") and k not in ("event_id", "source", "cancelled")
  227. }
  228. self._emit(log_entry)
  229. return MiddlewareResult()
  230. async def after(
  231. self,
  232. context: MiddlewareContext,
  233. ) -> None:
  234. """Loggt Event-Ende als JSON."""
  235. log_entry = {
  236. "@timestamp": datetime.now().isoformat(),
  237. "level": "INFO" if not context.errors else "WARNING",
  238. "type": "event",
  239. "phase": "end",
  240. "event_name": context.event_name,
  241. "event_id": context.event_data.event_id,
  242. "duration_ms": context.elapsed_ms,
  243. "cancelled": context.event_data.cancelled,
  244. "skipped": context.skipped,
  245. "aborted": context.aborted,
  246. }
  247. if context.errors:
  248. log_entry["errors"] = [str(e) for e in context.errors]
  249. self._emit(log_entry)
  250. async def on_error(
  251. self,
  252. context: MiddlewareContext,
  253. error: Exception,
  254. ) -> None:
  255. """Loggt Fehler als JSON."""
  256. import traceback
  257. log_entry = {
  258. "@timestamp": datetime.now().isoformat(),
  259. "level": "ERROR",
  260. "type": "event_error",
  261. "event_name": context.event_name,
  262. "event_id": context.event_data.event_id,
  263. "error_type": type(error).__name__,
  264. "error_message": str(error),
  265. "duration_ms": context.elapsed_ms,
  266. }
  267. if self._include_stack_trace:
  268. log_entry["stack_trace"] = traceback.format_exc()
  269. self._emit(log_entry)