context.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # -*- coding: utf-8 -*-
  2. """
  3. Logging-Kontext für Korrelations-IDs und kontextuelle Metadaten.
  4. Ermöglicht die Propagation von Kontext-Informationen durch die gesamte
  5. Anwendung für besseres Tracing und Debugging.
  6. """
  7. from __future__ import annotations
  8. import contextvars
  9. import uuid
  10. from contextlib import contextmanager
  11. from dataclasses import dataclass, field
  12. from datetime import datetime
  13. from typing import Any, Generator, Iterator
  14. # Kontextvariablen für Thread-/Task-lokale Speicherung
  15. _correlation_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
  16. "correlation_id", default=None
  17. )
  18. _log_context: contextvars.ContextVar[dict[str, Any]] = contextvars.ContextVar(
  19. "log_context", default={}
  20. )
  21. _context_stack: contextvars.ContextVar[list[dict[str, Any]]] = contextvars.ContextVar(
  22. "context_stack", default=[]
  23. )
  24. def get_correlation_id() -> str:
  25. """
  26. Gibt die aktuelle Korrelations-ID zurück.
  27. Generiert automatisch eine neue ID, falls keine existiert.
  28. Returns:
  29. Die aktuelle Korrelations-ID.
  30. """
  31. cid = _correlation_id.get()
  32. if cid is None:
  33. cid = str(uuid.uuid4())
  34. _correlation_id.set(cid)
  35. return cid
  36. def set_correlation_id(correlation_id: str | None) -> str | None:
  37. """
  38. Setzt die Korrelations-ID für den aktuellen Kontext.
  39. Args:
  40. correlation_id: Die zu setzende ID oder None zum Zurücksetzen.
  41. Returns:
  42. Die vorherige Korrelations-ID.
  43. """
  44. previous = _correlation_id.get()
  45. _correlation_id.set(correlation_id)
  46. return previous
  47. def generate_correlation_id() -> str:
  48. """
  49. Generiert eine neue Korrelations-ID und setzt sie im Kontext.
  50. Returns:
  51. Die neu generierte Korrelations-ID.
  52. """
  53. new_id = str(uuid.uuid4())
  54. _correlation_id.set(new_id)
  55. return new_id
  56. def get_context() -> dict[str, Any]:
  57. """
  58. Gibt den aktuellen Log-Kontext zurück.
  59. Returns:
  60. Dictionary mit kontextuellen Metadaten.
  61. """
  62. return dict(_log_context.get())
  63. def set_context(**kwargs: Any) -> None:
  64. """
  65. Setzt Werte im aktuellen Log-Kontext.
  66. Args:
  67. **kwargs: Schlüssel-Wert-Paare für den Kontext.
  68. """
  69. current = dict(_log_context.get())
  70. current.update(kwargs)
  71. _log_context.set(current)
  72. def clear_context() -> None:
  73. """Löscht den gesamten Log-Kontext."""
  74. _log_context.set({})
  75. _correlation_id.set(None)
  76. @contextmanager
  77. def log_context(**kwargs: Any) -> Generator[dict[str, Any], None, None]:
  78. """
  79. Context-Manager für temporäre Log-Kontext-Erweiterung.
  80. Fügt die angegebenen Werte zum Kontext hinzu und entfernt sie
  81. beim Verlassen des Blocks automatisch.
  82. Args:
  83. **kwargs: Temporäre Kontext-Werte.
  84. Yields:
  85. Der aktuelle kombinierte Kontext.
  86. Example:
  87. with log_context(user_id="123", action="login"):
  88. logger.info("Benutzer meldet sich an")
  89. """
  90. previous = dict(_log_context.get())
  91. new_context = {**previous, **kwargs}
  92. _log_context.set(new_context)
  93. try:
  94. yield new_context
  95. finally:
  96. _log_context.set(previous)
  97. @contextmanager
  98. def correlation_scope(correlation_id: str | None = None) -> Generator[str, None, None]:
  99. """
  100. Context-Manager für Korrelations-ID-Scope.
  101. Args:
  102. correlation_id: Optionale ID. Falls None, wird eine neue generiert.
  103. Yields:
  104. Die aktive Korrelations-ID.
  105. Example:
  106. with correlation_scope() as cid:
  107. logger.info("Request gestartet")
  108. # Alle Logs haben dieselbe correlation_id
  109. """
  110. previous = _correlation_id.get()
  111. new_id = correlation_id if correlation_id else str(uuid.uuid4())
  112. _correlation_id.set(new_id)
  113. try:
  114. yield new_id
  115. finally:
  116. _correlation_id.set(previous)
  117. @dataclass
  118. class LogContext:
  119. """
  120. Strukturierter Log-Kontext mit allen relevanten Metadaten.
  121. Sammelt automatisch Korrelations-ID, Zeitstempel und benutzerdefinierte
  122. Attribute für strukturiertes Logging.
  123. Attributes:
  124. correlation_id: Eindeutige ID für Request-Tracing.
  125. timestamp: Zeitpunkt der Kontext-Erstellung.
  126. service_name: Name des aktuellen Services.
  127. environment: Umgebung (production, staging, development).
  128. extra: Zusätzliche benutzerdefinierte Felder.
  129. """
  130. correlation_id: str = field(default_factory=get_correlation_id)
  131. timestamp: datetime = field(default_factory=datetime.now)
  132. service_name: str = ""
  133. environment: str = ""
  134. extra: dict[str, Any] = field(default_factory=dict)
  135. def __post_init__(self) -> None:
  136. """Initialisiert den Kontext mit gespeicherten Werten."""
  137. # Übernimm Werte aus dem globalen Kontext
  138. ctx = get_context()
  139. if not self.service_name and "service_name" in ctx:
  140. self.service_name = ctx["service_name"]
  141. if not self.environment and "environment" in ctx:
  142. self.environment = ctx["environment"]
  143. def to_dict(self) -> dict[str, Any]:
  144. """
  145. Konvertiert den Kontext in ein Dictionary.
  146. Returns:
  147. Dictionary mit allen Kontext-Feldern.
  148. """
  149. result: dict[str, Any] = {
  150. "correlation_id": self.correlation_id,
  151. "timestamp": self.timestamp.isoformat(),
  152. }
  153. if self.service_name:
  154. result["service_name"] = self.service_name
  155. if self.environment:
  156. result["environment"] = self.environment
  157. if self.extra:
  158. result.update(self.extra)
  159. return result
  160. def with_extra(self, **kwargs: Any) -> "LogContext":
  161. """
  162. Erstellt eine Kopie mit zusätzlichen Feldern.
  163. Args:
  164. **kwargs: Zusätzliche Felder.
  165. Returns:
  166. Neue LogContext-Instanz mit erweiterten Feldern.
  167. """
  168. new_extra = {**self.extra, **kwargs}
  169. return LogContext(
  170. correlation_id=self.correlation_id,
  171. timestamp=self.timestamp,
  172. service_name=self.service_name,
  173. environment=self.environment,
  174. extra=new_extra,
  175. )
  176. @classmethod
  177. def from_headers(cls, headers: dict[str, str]) -> "LogContext":
  178. """
  179. Erstellt einen Kontext aus HTTP-Headers.
  180. Nützlich für die Propagation von Kontext-Informationen
  181. zwischen Services.
  182. Args:
  183. headers: HTTP-Headers mit Kontext-Informationen.
  184. Returns:
  185. LogContext mit Werten aus den Headers.
  186. """
  187. correlation_id = headers.get(
  188. "X-Correlation-ID",
  189. headers.get("X-Request-ID", str(uuid.uuid4()))
  190. )
  191. return cls(
  192. correlation_id=correlation_id,
  193. service_name=headers.get("X-Service-Name", ""),
  194. environment=headers.get("X-Environment", ""),
  195. )
  196. def to_headers(self) -> dict[str, str]:
  197. """
  198. Konvertiert den Kontext in HTTP-Headers.
  199. Returns:
  200. Dictionary mit Header-Namen und -Werten.
  201. """
  202. headers = {
  203. "X-Correlation-ID": self.correlation_id,
  204. }
  205. if self.service_name:
  206. headers["X-Service-Name"] = self.service_name
  207. if self.environment:
  208. headers["X-Environment"] = self.environment
  209. return headers
  210. class ContextStack:
  211. """
  212. Stack-basierte Kontext-Verwaltung für verschachtelte Operationen.
  213. Ermöglicht das Pushen und Poppen von Kontext-Layern für
  214. hierarchische Logging-Szenarien.
  215. """
  216. def __init__(self) -> None:
  217. """Initialisiert einen leeren Kontext-Stack."""
  218. self._stack: list[dict[str, Any]] = []
  219. def push(self, **kwargs: Any) -> None:
  220. """
  221. Fügt einen neuen Kontext-Layer zum Stack hinzu.
  222. Args:
  223. **kwargs: Kontext-Werte für den neuen Layer.
  224. """
  225. current = dict(_log_context.get())
  226. self._stack.append(current)
  227. new_context = {**current, **kwargs}
  228. _log_context.set(new_context)
  229. def pop(self) -> dict[str, Any] | None:
  230. """
  231. Entfernt den obersten Kontext-Layer vom Stack.
  232. Returns:
  233. Der entfernte Kontext oder None, falls Stack leer.
  234. """
  235. if self._stack:
  236. previous = self._stack.pop()
  237. _log_context.set(previous)
  238. return previous
  239. return None
  240. def peek(self) -> dict[str, Any]:
  241. """
  242. Gibt den aktuellen Kontext zurück, ohne ihn zu entfernen.
  243. Returns:
  244. Der aktuelle kombinierte Kontext.
  245. """
  246. return dict(_log_context.get())
  247. @property
  248. def depth(self) -> int:
  249. """Gibt die aktuelle Stack-Tiefe zurück."""
  250. return len(self._stack)
  251. @contextmanager
  252. def scope(self, **kwargs: Any) -> Iterator[dict[str, Any]]:
  253. """
  254. Context-Manager für automatisches Push/Pop.
  255. Args:
  256. **kwargs: Kontext-Werte für den Scope.
  257. Yields:
  258. Der aktuelle Kontext innerhalb des Scopes.
  259. """
  260. self.push(**kwargs)
  261. try:
  262. yield self.peek()
  263. finally:
  264. self.pop()
  265. # Globale Stack-Instanz für einfache Verwendung
  266. _global_stack = ContextStack()
  267. def push_context(**kwargs: Any) -> None:
  268. """Fügt Werte zum globalen Kontext-Stack hinzu."""
  269. _global_stack.push(**kwargs)
  270. def pop_context() -> dict[str, Any] | None:
  271. """Entfernt den obersten Layer vom globalen Kontext-Stack."""
  272. return _global_stack.pop()
  273. def context_scope(**kwargs: Any) -> Iterator[dict[str, Any]]:
  274. """Context-Manager für den globalen Kontext-Stack."""
  275. return _global_stack.scope(**kwargs)