Async logger with synchronous interface. Uses background thread for file I/O to avoid blocking event loop, while maintaining API compatibility.
| 26 | DEBUG = logging.DEBUG |
| 27 | |
| 28 | class Logger(logging.Logger, metaclass=Singleton): |
| 29 | """ |
| 30 | Async logger with synchronous interface. |
| 31 | Uses background thread for file I/O to avoid blocking event loop, |
| 32 | while maintaining API compatibility. |
| 33 | """ |
| 34 | def __init__(self, name="logger", level=logging.INFO): |
| 35 | # Initialize the parent class |
| 36 | super().__init__(name, level) |
| 37 | |
| 38 | # Define a formatter for log messages |
| 39 | self.formatter = logging.Formatter( |
| 40 | fmt="%(asctime)s - %(name)s:%(levelname)s - %(filename)s:%(lineno)s - %(message)s", |
| 41 | datefmt="%Y-%m-%d %H:%M:%S", |
| 42 | ) |
| 43 | |
| 44 | # Async log writing related |
| 45 | self._log_queue: Optional[Queue] = None |
| 46 | self._log_thread: Optional[threading.Thread] = None |
| 47 | self._stop_event = threading.Event() |
| 48 | self._log_path: Optional[str] = None |
| 49 | self._initialized = False |
| 50 | |
| 51 | def _log_writer_thread(self, log_path: str): |
| 52 | """Background thread: reads logs from queue and writes to file.""" |
| 53 | with open(log_path, "a", encoding="utf-8") as log_file: |
| 54 | while not self._stop_event.is_set(): |
| 55 | try: |
| 56 | # Get log entry from queue (with timeout to avoid blocking forever) |
| 57 | log_entry = self._log_queue.get(timeout=0.1) |
| 58 | if log_entry is None: # Stop signal |
| 59 | break |
| 60 | |
| 61 | # Write to file |
| 62 | log_file.write(log_entry) |
| 63 | log_file.flush() # Ensure immediate write |
| 64 | self._log_queue.task_done() |
| 65 | |
| 66 | except Empty: |
| 67 | continue |
| 68 | except Exception as e: |
| 69 | # If write fails, at least output to stderr |
| 70 | import sys |
| 71 | print(f"Logger write error: {e}", file=sys.stderr) |
| 72 | |
| 73 | def _enqueue_log(self, level: str, msg: str, *args, **kwargs): |
| 74 | """Enqueue log message to background thread (non-blocking).""" |
| 75 | if not self._initialized or self._log_queue is None: |
| 76 | # Fallback to synchronous logging if not initialized |
| 77 | return |
| 78 | |
| 79 | try: |
| 80 | # Format log message |
| 81 | record = self.makeRecord( |
| 82 | self.name, |
| 83 | getattr(logging, level.upper()), |
| 84 | "", 0, msg, args, None |
| 85 | ) |