Source code for serena.util.logging
import queue
import threading
from collections.abc import Callable
from dataclasses import dataclass
from typing import Optional
from sensai.util import logging
from serena.constants import LOG_MESSAGES_BUFFER_SIZE, SERENA_LOG_FORMAT
lg = logging
[docs]
@dataclass
class LogMessages:
messages: list[str]
"""
the list of log messages, ordered from oldest to newest
"""
max_idx: int
"""
the 0-based index of the last message in `messages` (in the full log history)
"""
[docs]
class MemoryLogHandler(logging.Handler):
def __init__(self, level: int = logging.NOTSET, max_messages: int | None = LOG_MESSAGES_BUFFER_SIZE) -> None:
super().__init__(level=level)
self.setFormatter(logging.Formatter(SERENA_LOG_FORMAT))
self._log_buffer = LogBuffer(max_messages=max_messages)
self._log_queue: queue.Queue[str] = queue.Queue()
self._stop_event = threading.Event()
self._emit_callbacks: list[Callable[[str], None]] = []
# start background thread to process logs
self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
self.worker_thread.start()
[docs]
def add_emit_callback(self, callback: Callable[[str], None]) -> None:
"""
Adds a callback that will be called with each log message.
The callback should accept a single string argument (the log message).
"""
self._emit_callbacks.append(callback)
[docs]
def emit(self, record: logging.LogRecord) -> None:
msg = self.format(record)
self._log_queue.put_nowait(msg)
def _process_queue(self) -> None:
while not self._stop_event.is_set():
try:
msg = self._log_queue.get(timeout=1)
self._log_buffer.append(msg)
for callback in self._emit_callbacks:
try:
callback(msg)
except:
pass
self._log_queue.task_done()
except queue.Empty:
continue
def get_log_messages(self, from_idx: int = 0) -> LogMessages:
return self._log_buffer.get_log_messages(from_idx=from_idx)
def clear_log_messages(self) -> None:
self._log_buffer.clear()
[docs]
class LogBuffer:
"""
A thread-safe buffer for storing (an optionally limited number of) log messages.
"""
def __init__(self, max_messages: int | None = None) -> None:
self._max_messages = max_messages
self._log_messages: list[str] = []
self._lock = threading.Lock()
self._max_idx = -1
"""
the 0-based index of the most recently added log message
"""
def append(self, msg: str) -> None:
with self._lock:
self._log_messages.append(msg)
self._max_idx += 1
if self._max_messages is not None and len(self._log_messages) > self._max_messages:
excess = len(self._log_messages) - self._max_messages
self._log_messages = self._log_messages[excess:]
def clear(self) -> None:
with self._lock:
self._log_messages = []
self._max_idx = -1
[docs]
def get_log_messages(self, from_idx: int = 0) -> LogMessages:
"""
:param from_idx: the 0-based index of the first log message to return.
If from_idx is less than or equal to the index of the oldest message in the buffer,
then all messages in the buffer will be returned.
:return: the list of messages
"""
from_idx = max(from_idx, 0)
with self._lock:
first_stored_idx = self._max_idx - len(self._log_messages) + 1
if from_idx <= first_stored_idx:
messages = self._log_messages.copy()
else:
start_idx = from_idx - first_stored_idx
messages = self._log_messages[start_idx:].copy()
return LogMessages(messages=messages, max_idx=self._max_idx)
[docs]
class SuspendedLoggersContext:
"""A context manager that provides an isolated logging environment.
Temporarily removes all root log handlers upon entry, providing a clean slate
for defining new log handlers within the context. Upon exit, restores the original
logging configuration. This is useful when you need to temporarily configure
an isolated logging setup with well-defined log handlers.
The context manager:
- Removes all existing (root) log handlers on entry
- Allows defining new temporary handlers within the context
- Restores the original configuration (handlers and root log level) on exit
Example:
>>> with SuspendedLoggersContext():
... # No handlers are active here (configure your own and set desired log level)
... pass
>>> # Original log handlers are restored here
"""
def __init__(self) -> None:
self.saved_root_handlers: list = []
self.saved_root_level: Optional[int] = None
def __enter__(self) -> "SuspendedLoggersContext":
root_logger = lg.getLogger()
self.saved_root_handlers = root_logger.handlers.copy()
self.saved_root_level = root_logger.level
root_logger.handlers.clear()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
root_logger = lg.getLogger()
root_logger.handlers = self.saved_root_handlers
if self.saved_root_level is not None:
root_logger.setLevel(self.saved_root_level)