|
1 | | -import datetime |
2 | 1 | import logging |
3 | 2 | import os |
4 | | -import queue |
5 | 3 | import threading |
6 | 4 | from abc import ABC, abstractmethod |
7 | 5 | from typing import Optional, Dict, Any |
8 | 6 |
|
9 | | -# Configuration du logging de base pour voir les erreurs et avertissements de la librairie |
10 | | -logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s') |
11 | | - |
| 7 | +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') |
12 | 8 |
|
13 | 9 | class BaseBusinessLogger(ABC): |
14 | | - """ |
15 | | - Classe de base abstraite pour les loggers métier. |
16 | | - Gère toute la logique commune : singleton, initialisation paresseuse, |
17 | | - file d'attente, thread de travail, et gestion de contexte 'with'. |
18 | | - """ |
19 | | - _instance = None |
| 10 | + _instances = {} |
20 | 11 | _lock = threading.Lock() |
21 | | - |
22 | 12 | _GREEN = "\033[92m" |
23 | 13 | _RESET = "\033[0m" |
24 | 14 |
|
25 | 15 | def __new__(cls, *args, **kwargs): |
26 | | - if not cls._instance: |
27 | | - with cls._lock: |
28 | | - if not cls._instance: |
29 | | - cls._instance = super().__new__(cls) |
30 | | - return cls._instance |
| 16 | + with cls._lock: |
| 17 | + if cls not in cls._instances: |
| 18 | + cls._instances[cls] = super().__new__(cls) |
| 19 | + return cls._instances[cls] |
31 | 20 |
|
32 | 21 | def __init__(self): |
33 | | - if not hasattr(self, '_initialized_flag'): |
34 | | - self._initialized_flag = False |
35 | | - self.is_enabled = False |
36 | | - self._init_lock = threading.Lock() |
| 22 | + if hasattr(self, '_initialized') and self._initialized: |
| 23 | + return |
| 24 | + self._initialized = False |
| 25 | + self.is_enabled = False |
| 26 | + self._init_lock = threading.Lock() |
| 27 | + self.backend = None |
37 | 28 |
|
38 | 29 | @property |
39 | 30 | @abstractmethod |
40 | 31 | def logger_name(self) -> str: |
41 | | - """Nom du logger pour les messages console.""" |
42 | 32 | pass |
43 | 33 |
|
44 | 34 | @property |
45 | 35 | @abstractmethod |
46 | 36 | def enabled_env_var(self) -> str: |
47 | | - """Clé de la variable d'environnement pour activer le logger.""" |
48 | 37 | pass |
49 | 38 |
|
50 | 39 | @property |
51 | 40 | @abstractmethod |
52 | 41 | def db_file_env_var(self) -> str: |
53 | | - """Clé de la variable d'environnement pour le fichier de BDD.""" |
54 | | - pass |
55 | | - |
56 | | - @abstractmethod |
57 | | - def _setup_backend(self, db_file: str) -> bool: |
58 | | - """Prépare le backend de stockage (crée le fichier, la table, etc.). Retourne True si succès.""" |
59 | 42 | pass |
60 | 43 |
|
| 44 | + @staticmethod |
61 | 45 | @abstractmethod |
62 | | - def _initialize_backend_for_worker(self): |
63 | | - """Initialise les ressources (ex: connexion BDD) pour le thread de travail.""" |
| 46 | + def _create_backend(file_path: str): |
64 | 47 | pass |
65 | 48 |
|
66 | 49 | @abstractmethod |
67 | | - def _write_log_to_backend(self, log_item: Any): |
68 | | - """Écrit un seul item de log dans le backend.""" |
| 50 | + def _on_backend_ready(self): |
69 | 51 | pass |
70 | 52 |
|
71 | 53 | @abstractmethod |
72 | | - def _shutdown_backend(self): |
73 | | - """Ferme proprement les connexions au backend (ex: db.close()).""" |
| 54 | + def log(self, event_type: str, details: Optional[Dict[str, Any]] = None): |
74 | 55 | pass |
75 | 56 |
|
76 | 57 | def _lazy_initialize(self): |
77 | 58 | with self._init_lock: |
78 | | - if self._initialized_flag: |
79 | | - return |
80 | | - |
| 59 | + if self._initialized: return |
81 | 60 | enabled = os.getenv(self.enabled_env_var, "false").lower() in ("true", "1", "yes") |
82 | 61 | db_file = os.getenv(self.db_file_env_var) |
83 | | - |
84 | 62 | if enabled and db_file: |
85 | | - if self._setup_backend(db_file): |
| 63 | + self.backend = self.__class__._create_backend(db_file) |
| 64 | + self.backend.start() |
| 65 | + if self.backend.wait_for_ready(): |
86 | 66 | self.is_enabled = True |
87 | | - # Utilisation d'une file de taille limitée pour éviter une surconsommation de mémoire |
88 | | - self.log_queue = queue.Queue(maxsize=1000) |
89 | | - self.worker_thread = threading.Thread(target=self._process_queue, daemon=True) |
90 | | - self.worker_thread.start() |
91 | | - print(f"✅ {self.logger_name} auto-configuré. Logs dans '{db_file}'.") |
92 | | - |
93 | | - self._initialized_flag = True |
94 | | - |
95 | | - def _process_queue(self): |
96 | | - # Initialisation spécifique au thread (ex: connexion BDD) |
97 | | - self._initialize_backend_for_worker() |
98 | | - |
99 | | - while True: |
100 | | - try: |
101 | | - log_item = self.log_queue.get() |
102 | | - if log_item is None: break |
103 | | - self._write_log_to_backend(log_item) |
104 | | - self.log_queue.task_done() |
105 | | - except Exception as e: |
106 | | - # Utiliser le logging standard pour les erreurs internes |
107 | | - logging.error(f"Erreur dans le worker {self.logger_name} : {e}") |
| 67 | + self._on_backend_ready() |
| 68 | + print(f"✅ {self.logger_name} configured. Logs will be in '{db_file}'.") |
| 69 | + else: |
| 70 | + logging.error(f"The backend for {self.logger_name} failed to start.") |
| 71 | + self._initialized = True |
108 | 72 |
|
109 | | - def log(self, event_type: str, details: Optional[Dict[str, Any]] = None): |
110 | | - if not self._initialized_flag: self._lazy_initialize() |
111 | | - if not self.is_enabled: return |
112 | | - |
113 | | - timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() |
114 | | - details_str = f"- {details}" if details else "" |
115 | | - console_output = f"[{self.logger_name}] {event_type} {details_str}" |
116 | | - print(f"{self._GREEN}{console_output}{self._RESET}") |
117 | | - |
118 | | - log_data = { |
119 | | - 'timestamp': timestamp, |
120 | | - 'event_type': event_type, |
121 | | - 'details': details |
122 | | - } |
123 | | - |
124 | | - try: |
125 | | - # Ne pas bloquer l'application si la file est pleine |
126 | | - self.log_queue.put(log_data, block=False) |
127 | | - except queue.Full: |
128 | | - logging.warning(f"File d'attente du logger '{self.logger_name}' pleine. Le log a été ignoré.") |
129 | | - |
130 | | - def shutdown(self, wait=True): |
131 | | - if not self._initialized_flag: self._lazy_initialize() |
132 | | - if not self.is_enabled or not hasattr(self, 'log_queue'): return |
133 | | - if wait: self.log_queue.join() |
134 | | - self.log_queue.put(None) |
135 | | - if hasattr(self, 'worker_thread'): self.worker_thread.join(timeout=5) |
136 | | - self._shutdown_backend() |
137 | | - print(f"✅ {self.logger_name} arrêté proprement.") |
| 73 | + def _ensure_initialized(self): |
| 74 | + if not self._initialized: self._lazy_initialize() |
| 75 | + |
| 76 | + def shutdown(self): |
| 77 | + self._ensure_initialized() |
| 78 | + if self.is_enabled and self.backend: |
| 79 | + self.backend.stop() |
| 80 | + print(f"✅ {self.logger_name} shut down cleanly.") |
138 | 81 |
|
139 | 82 | def __enter__(self): |
140 | | - self._lazy_initialize() |
| 83 | + self._ensure_initialized() |
141 | 84 | return self |
142 | 85 |
|
143 | | - def __exit__(self, exc_type, exc_value, traceback): |
| 86 | + def __exit__(self, exc_type, exc_val, exc_tb): |
144 | 87 | self.shutdown() |
145 | 88 |
|
| 89 | + # ================================================================= |
| 90 | + # MÉTHODE CORRIGÉE / AJOUTÉE |
| 91 | + # ================================================================= |
146 | 92 | def _reset_for_testing(self): |
147 | | - """Réinitialise l'état interne du logger. POUR LES TESTS UNIQUEMENT.""" |
148 | | - # S'assure que le thread est bien arrêté s'il est en cours |
149 | | - if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): |
150 | | - # CORRIGÉ : On attend la fin du traitement de la file. |
151 | | - self.shutdown(wait=True) |
152 | | - |
153 | | - # Réinitialise les flags d'état |
154 | | - self._initialized_flag = False |
| 93 | + """ |
| 94 | + Réinitialise l'état interne du logger pour les tests. |
| 95 | + Cette méthode arrête le backend, réinitialise les flags et supprime |
| 96 | + l'instance singleton pour garantir l'isolation des tests. |
| 97 | + """ |
| 98 | + # 1. Arrête le backend s'il est actif |
| 99 | + if self.is_enabled and self.backend: |
| 100 | + self.shutdown() |
| 101 | + |
| 102 | + # 2. Réinitialise les attributs d'état de l'instance |
| 103 | + self._initialized = False |
155 | 104 | self.is_enabled = False |
| 105 | + self.backend = None |
156 | 106 |
|
157 | | - # Vide la queue au cas où un test précédent aurait échoué en laissant des items |
158 | | - if hasattr(self, 'log_queue'): |
159 | | - while not self.log_queue.empty(): |
160 | | - try: |
161 | | - self.log_queue.get_nowait() |
162 | | - except queue.Empty: |
163 | | - continue |
| 107 | + # 3. Supprime l'instance du cache singleton de la classe |
| 108 | + with self.__class__._lock: |
| 109 | + if self.__class__ in self.__class__._instances: |
| 110 | + del self.__class__._instances[self.__class__] |
0 commit comments