structured.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. # -*- coding: utf-8 -*-
  2. """
  3. Strukturierter Logger für enterprise-grade Logging.
  4. Bietet JSON-fähiges Logging mit automatischer Kontext-Propagation,
  5. strukturierten Feldern und Integration mit dem Trixy-Debug-System.
  6. """
  7. from __future__ import annotations
  8. import logging
  9. import sys
  10. from contextlib import contextmanager
  11. from dataclasses import dataclass, field
  12. from datetime import datetime
  13. from typing import Any, Callable, Iterator
  14. from trixy_core.utils.logging.context import (
  15. LogContext,
  16. get_context,
  17. get_correlation_id,
  18. log_context,
  19. )
  20. from trixy_core.utils.logging.formatters import JSONFormatter, TextFormatter
  21. # Logger-Registry für zentrale Verwaltung
  22. _loggers: dict[str, "StructuredLogger"] = {}
  23. @dataclass
  24. class LogEntry:
  25. """
  26. Strukturierter Log-Eintrag mit allen Metadaten.
  27. Repräsentiert einen einzelnen Log-Eintrag mit Timestamp,
  28. Level, Nachricht und kontextuellen Daten.
  29. """
  30. level: str
  31. message: str
  32. timestamp: datetime = field(default_factory=datetime.now)
  33. logger_name: str = ""
  34. correlation_id: str = ""
  35. context: dict[str, Any] = field(default_factory=dict)
  36. extra: dict[str, Any] = field(default_factory=dict)
  37. exception: str | None = None
  38. source: dict[str, Any] = field(default_factory=dict)
  39. def to_dict(self) -> dict[str, Any]:
  40. """Konvertiert den Eintrag in ein Dictionary."""
  41. result = {
  42. "timestamp": self.timestamp.isoformat(),
  43. "level": self.level,
  44. "message": self.message,
  45. "logger": self.logger_name,
  46. }
  47. if self.correlation_id:
  48. result["correlation_id"] = self.correlation_id
  49. if self.context:
  50. result["context"] = self.context
  51. if self.extra:
  52. result["extra"] = self.extra
  53. if self.exception:
  54. result["exception"] = self.exception
  55. if self.source:
  56. result["source"] = self.source
  57. return result
  58. class StructuredLogger:
  59. """
  60. Strukturierter Logger mit JSON-Output und Kontext-Unterstützung.
  61. Erweitert den Standard-Python-Logger um strukturierte Felder,
  62. automatische Korrelations-IDs und flexible Ausgabeformate.
  63. Example:
  64. logger = StructuredLogger("my_service")
  65. logger.info("User logged in", user_id="123", action="login")
  66. with logger.context(request_id="abc"):
  67. logger.debug("Processing request")
  68. """
  69. # Log-Level-Mapping
  70. LEVELS = {
  71. "DEBUG": logging.DEBUG,
  72. "INFO": logging.INFO,
  73. "WARNING": logging.WARNING,
  74. "ERROR": logging.ERROR,
  75. "CRITICAL": logging.CRITICAL,
  76. }
  77. def __init__(
  78. self,
  79. name: str,
  80. level: int | str = logging.INFO,
  81. use_json: bool = False,
  82. propagate: bool = True,
  83. handlers: list[logging.Handler] | None = None,
  84. ) -> None:
  85. """
  86. Initialisiert den strukturierten Logger.
  87. Args:
  88. name: Name des Loggers (typischerweise Modul-/Servicename).
  89. level: Minimales Log-Level (int oder String).
  90. use_json: Ob JSON-Format für die Ausgabe verwendet werden soll.
  91. propagate: Ob Logs an Parent-Logger weitergegeben werden.
  92. handlers: Optionale Liste von Handlern.
  93. """
  94. self.name = name
  95. self._logger = logging.getLogger(name)
  96. # Level setzen
  97. if isinstance(level, str):
  98. level = self.LEVELS.get(level.upper(), logging.INFO)
  99. self._logger.setLevel(level)
  100. self._logger.propagate = propagate
  101. self._use_json = use_json
  102. self._default_extra: dict[str, Any] = {}
  103. self._hooks: list[Callable[[LogEntry], LogEntry | None]] = []
  104. # Handler konfigurieren
  105. if handlers:
  106. for handler in handlers:
  107. self._logger.addHandler(handler)
  108. elif not self._logger.handlers:
  109. # Standard-Handler hinzufügen
  110. handler = logging.StreamHandler(sys.stdout)
  111. if use_json:
  112. handler.setFormatter(JSONFormatter())
  113. else:
  114. handler.setFormatter(TextFormatter())
  115. self._logger.addHandler(handler)
  116. @property
  117. def level(self) -> int:
  118. """Gibt das aktuelle Log-Level zurück."""
  119. return self._logger.level
  120. @level.setter
  121. def level(self, value: int | str) -> None:
  122. """Setzt das Log-Level."""
  123. if isinstance(value, str):
  124. value = self.LEVELS.get(value.upper(), logging.INFO)
  125. self._logger.setLevel(value)
  126. def set_default_extra(self, **kwargs: Any) -> None:
  127. """
  128. Setzt Standard-Extra-Felder für alle Log-Nachrichten.
  129. Args:
  130. **kwargs: Schlüssel-Wert-Paare für Extra-Felder.
  131. """
  132. self._default_extra.update(kwargs)
  133. def clear_default_extra(self) -> None:
  134. """Löscht alle Standard-Extra-Felder."""
  135. self._default_extra.clear()
  136. def add_hook(
  137. self, hook: Callable[[LogEntry], LogEntry | None]
  138. ) -> None:
  139. """
  140. Fügt einen Hook hinzu, der vor jedem Log aufgerufen wird.
  141. Der Hook kann den LogEntry modifizieren oder None zurückgeben,
  142. um das Logging zu unterdrücken.
  143. Args:
  144. hook: Funktion, die LogEntry verarbeitet.
  145. """
  146. self._hooks.append(hook)
  147. def remove_hook(
  148. self, hook: Callable[[LogEntry], LogEntry | None]
  149. ) -> None:
  150. """Entfernt einen Hook."""
  151. self._hooks.remove(hook)
  152. def _create_entry(
  153. self,
  154. level: str,
  155. message: str,
  156. exc_info: bool = False,
  157. **extra: Any,
  158. ) -> LogEntry:
  159. """Erstellt einen LogEntry mit allen Metadaten."""
  160. # Extra-Felder kombinieren
  161. combined_extra = {**self._default_extra, **extra}
  162. entry = LogEntry(
  163. level=level,
  164. message=message,
  165. logger_name=self.name,
  166. correlation_id=get_correlation_id(),
  167. context=get_context(),
  168. extra=combined_extra,
  169. )
  170. return entry
  171. def _apply_hooks(self, entry: LogEntry) -> LogEntry | None:
  172. """Wendet alle registrierten Hooks an."""
  173. for hook in self._hooks:
  174. result = hook(entry)
  175. if result is None:
  176. return None
  177. entry = result
  178. return entry
  179. def _log(
  180. self,
  181. level: int,
  182. level_name: str,
  183. message: str,
  184. exc_info: bool = False,
  185. **extra: Any,
  186. ) -> None:
  187. """Interne Log-Methode."""
  188. if not self._logger.isEnabledFor(level):
  189. return
  190. # LogEntry erstellen
  191. entry = self._create_entry(level_name, message, exc_info, **extra)
  192. # Hooks anwenden
  193. entry = self._apply_hooks(entry)
  194. if entry is None:
  195. return
  196. # Python-Logger aufrufen
  197. self._logger.log(
  198. level,
  199. message,
  200. exc_info=exc_info,
  201. extra={"structured_extra": entry.extra},
  202. )
  203. def debug(self, message: str, **extra: Any) -> None:
  204. """
  205. Loggt eine Debug-Nachricht.
  206. Args:
  207. message: Die Log-Nachricht.
  208. **extra: Zusätzliche strukturierte Felder.
  209. """
  210. self._log(logging.DEBUG, "DEBUG", message, **extra)
  211. def info(self, message: str, **extra: Any) -> None:
  212. """
  213. Loggt eine Info-Nachricht.
  214. Args:
  215. message: Die Log-Nachricht.
  216. **extra: Zusätzliche strukturierte Felder.
  217. """
  218. self._log(logging.INFO, "INFO", message, **extra)
  219. def warning(self, message: str, **extra: Any) -> None:
  220. """
  221. Loggt eine Warnung.
  222. Args:
  223. message: Die Log-Nachricht.
  224. **extra: Zusätzliche strukturierte Felder.
  225. """
  226. self._log(logging.WARNING, "WARNING", message, **extra)
  227. def warn(self, message: str, **extra: Any) -> None:
  228. """Alias für warning()."""
  229. self.warning(message, **extra)
  230. def error(self, message: str, exc_info: bool = False, **extra: Any) -> None:
  231. """
  232. Loggt eine Fehlermeldung.
  233. Args:
  234. message: Die Log-Nachricht.
  235. exc_info: Ob Exception-Info inkludiert werden soll.
  236. **extra: Zusätzliche strukturierte Felder.
  237. """
  238. self._log(logging.ERROR, "ERROR", message, exc_info=exc_info, **extra)
  239. def critical(self, message: str, exc_info: bool = False, **extra: Any) -> None:
  240. """
  241. Loggt eine kritische Meldung.
  242. Args:
  243. message: Die Log-Nachricht.
  244. exc_info: Ob Exception-Info inkludiert werden soll.
  245. **extra: Zusätzliche strukturierte Felder.
  246. """
  247. self._log(
  248. logging.CRITICAL, "CRITICAL", message, exc_info=exc_info, **extra
  249. )
  250. def exception(self, message: str, **extra: Any) -> None:
  251. """
  252. Loggt eine Exception (ERROR-Level mit Traceback).
  253. Args:
  254. message: Die Log-Nachricht.
  255. **extra: Zusätzliche strukturierte Felder.
  256. """
  257. self._log(logging.ERROR, "ERROR", message, exc_info=True, **extra)
  258. def log(self, level: int | str, message: str, **extra: Any) -> None:
  259. """
  260. Loggt mit einem benutzerdefinierten Level.
  261. Args:
  262. level: Log-Level (int oder String).
  263. message: Die Log-Nachricht.
  264. **extra: Zusätzliche strukturierte Felder.
  265. """
  266. if isinstance(level, str):
  267. level_int = self.LEVELS.get(level.upper(), logging.INFO)
  268. level_name = level.upper()
  269. else:
  270. level_int = level
  271. level_name = logging.getLevelName(level)
  272. self._log(level_int, level_name, message, **extra)
  273. @contextmanager
  274. def context(self, **kwargs: Any) -> Iterator[None]:
  275. """
  276. Context-Manager für temporäre Extra-Felder.
  277. Args:
  278. **kwargs: Temporäre Extra-Felder.
  279. Example:
  280. with logger.context(request_id="123"):
  281. logger.info("Processing") # Enthält request_id
  282. """
  283. with log_context(**kwargs):
  284. yield
  285. @contextmanager
  286. def timed(
  287. self,
  288. operation: str,
  289. level: int | str = logging.DEBUG,
  290. **extra: Any,
  291. ) -> Iterator[None]:
  292. """
  293. Context-Manager für Zeitmessung.
  294. Loggt die Dauer der Operation beim Verlassen des Blocks.
  295. Args:
  296. operation: Name der Operation.
  297. level: Log-Level für die Ausgabe.
  298. **extra: Zusätzliche strukturierte Felder.
  299. Example:
  300. with logger.timed("database_query"):
  301. cursor.execute(query)
  302. # Loggt: "database_query completed in 0.123s"
  303. """
  304. start = datetime.now()
  305. try:
  306. yield
  307. finally:
  308. duration = (datetime.now() - start).total_seconds()
  309. self.log(
  310. level,
  311. f"{operation} completed",
  312. duration_seconds=duration,
  313. operation=operation,
  314. **extra,
  315. )
  316. def bind(self, **kwargs: Any) -> "BoundLogger":
  317. """
  318. Erstellt einen gebundenen Logger mit festen Extra-Feldern.
  319. Args:
  320. **kwargs: Felder, die an alle Logs angehängt werden.
  321. Returns:
  322. BoundLogger mit den angegebenen Feldern.
  323. Example:
  324. user_logger = logger.bind(user_id="123")
  325. user_logger.info("Action performed") # Enthält user_id
  326. """
  327. return BoundLogger(self, kwargs)
  328. def add_handler(self, handler: logging.Handler) -> None:
  329. """Fügt einen Handler hinzu."""
  330. self._logger.addHandler(handler)
  331. def remove_handler(self, handler: logging.Handler) -> None:
  332. """Entfernt einen Handler."""
  333. self._logger.removeHandler(handler)
  334. def get_child(self, suffix: str) -> "StructuredLogger":
  335. """
  336. Erstellt einen Kind-Logger.
  337. Args:
  338. suffix: Suffix für den Logger-Namen.
  339. Returns:
  340. Neuer StructuredLogger mit erweitertem Namen.
  341. """
  342. child_name = f"{self.name}.{suffix}"
  343. return get_logger(child_name, use_json=self._use_json)
  344. class BoundLogger:
  345. """
  346. Logger mit vordefinierten Extra-Feldern.
  347. Wrapper um StructuredLogger, der automatisch bestimmte
  348. Felder an alle Log-Nachrichten anhängt.
  349. """
  350. def __init__(
  351. self,
  352. logger: StructuredLogger,
  353. bound_fields: dict[str, Any],
  354. ) -> None:
  355. """
  356. Initialisiert den gebundenen Logger.
  357. Args:
  358. logger: Der zugrunde liegende Logger.
  359. bound_fields: Felder, die an alle Logs angehängt werden.
  360. """
  361. self._logger = logger
  362. self._bound_fields = bound_fields
  363. def _merge_extra(self, extra: dict[str, Any]) -> dict[str, Any]:
  364. """Kombiniert gebundene und übergebene Felder."""
  365. return {**self._bound_fields, **extra}
  366. def debug(self, message: str, **extra: Any) -> None:
  367. """Loggt eine Debug-Nachricht mit gebundenen Feldern."""
  368. self._logger.debug(message, **self._merge_extra(extra))
  369. def info(self, message: str, **extra: Any) -> None:
  370. """Loggt eine Info-Nachricht mit gebundenen Feldern."""
  371. self._logger.info(message, **self._merge_extra(extra))
  372. def warning(self, message: str, **extra: Any) -> None:
  373. """Loggt eine Warnung mit gebundenen Feldern."""
  374. self._logger.warning(message, **self._merge_extra(extra))
  375. def warn(self, message: str, **extra: Any) -> None:
  376. """Alias für warning()."""
  377. self.warning(message, **extra)
  378. def error(self, message: str, exc_info: bool = False, **extra: Any) -> None:
  379. """Loggt einen Fehler mit gebundenen Feldern."""
  380. self._logger.error(message, exc_info=exc_info, **self._merge_extra(extra))
  381. def critical(self, message: str, exc_info: bool = False, **extra: Any) -> None:
  382. """Loggt eine kritische Meldung mit gebundenen Feldern."""
  383. self._logger.critical(
  384. message, exc_info=exc_info, **self._merge_extra(extra)
  385. )
  386. def exception(self, message: str, **extra: Any) -> None:
  387. """Loggt eine Exception mit gebundenen Feldern."""
  388. self._logger.exception(message, **self._merge_extra(extra))
  389. def bind(self, **kwargs: Any) -> "BoundLogger":
  390. """
  391. Erstellt einen neuen gebundenen Logger mit zusätzlichen Feldern.
  392. Args:
  393. **kwargs: Zusätzliche Felder.
  394. Returns:
  395. Neuer BoundLogger mit kombinierten Feldern.
  396. """
  397. new_fields = {**self._bound_fields, **kwargs}
  398. return BoundLogger(self._logger, new_fields)
  399. @contextmanager
  400. def context(self, **kwargs: Any) -> Iterator[None]:
  401. """Context-Manager für temporäre Extra-Felder."""
  402. with self._logger.context(**kwargs):
  403. yield
  404. @contextmanager
  405. def timed(
  406. self,
  407. operation: str,
  408. level: int | str = logging.DEBUG,
  409. **extra: Any,
  410. ) -> Iterator[None]:
  411. """Context-Manager für Zeitmessung."""
  412. with self._logger.timed(
  413. operation, level, **self._merge_extra(extra)
  414. ):
  415. yield
  416. def get_logger(
  417. name: str,
  418. level: int | str = logging.INFO,
  419. use_json: bool = False,
  420. ) -> StructuredLogger:
  421. """
  422. Factory-Funktion für strukturierte Logger.
  423. Gibt gecachte Logger zurück, um Duplikate zu vermeiden.
  424. Args:
  425. name: Name des Loggers.
  426. level: Log-Level.
  427. use_json: Ob JSON-Format verwendet werden soll.
  428. Returns:
  429. StructuredLogger-Instanz.
  430. """
  431. if name in _loggers:
  432. return _loggers[name]
  433. logger = StructuredLogger(name, level=level, use_json=use_json)
  434. _loggers[name] = logger
  435. return logger
  436. def configure_root_logger(
  437. level: int | str = logging.INFO,
  438. use_json: bool = False,
  439. handlers: list[logging.Handler] | None = None,
  440. ) -> None:
  441. """
  442. Konfiguriert den Root-Logger.
  443. Args:
  444. level: Log-Level für den Root-Logger.
  445. use_json: Ob JSON-Format verwendet werden soll.
  446. handlers: Optionale Handler-Liste.
  447. """
  448. root = logging.getLogger()
  449. if isinstance(level, str):
  450. level = StructuredLogger.LEVELS.get(level.upper(), logging.INFO)
  451. root.setLevel(level)
  452. # Alte Handler entfernen
  453. for handler in root.handlers[:]:
  454. root.removeHandler(handler)
  455. # Neue Handler hinzufügen
  456. if handlers:
  457. for handler in handlers:
  458. root.addHandler(handler)
  459. else:
  460. handler = logging.StreamHandler(sys.stdout)
  461. if use_json:
  462. handler.setFormatter(JSONFormatter())
  463. else:
  464. handler.setFormatter(TextFormatter())
  465. root.addHandler(handler)