Source code for logeverything.transport.buffer

"""
Shared batching / retry buffer for LogEverything transports.

Provides a thread-safe queue with a background flush thread, batching,
exponential-backoff retry, and configurable back-pressure policy.
Follows the same daemon-thread pattern as ``AsyncQueueHandler``.
"""

import logging
import threading
import time
from collections import deque
from typing import Any, Callable, Dict, List, Literal, Optional

logger = logging.getLogger(__name__)


[docs] class LogBuffer: """ Thread-safe log buffer with background batched flushing. Args: send_batch: Callable that accepts a ``List[Dict]`` and ships them. Must raise on failure so the buffer can retry. max_buffer_size: Maximum number of records to hold in memory. batch_size: Number of records per batch sent to ``send_batch``. flush_interval: Seconds between automatic flush attempts. max_retries: How many times to retry a failed batch (with backoff). backpressure: What to do when the buffer is full — ``"drop"`` silently discards the oldest records; ``"block"`` waits for space. """ def __init__( self, send_batch: Callable[[List[Dict[str, Any]]], None], max_buffer_size: int = 10000, batch_size: int = 100, flush_interval: float = 5.0, max_retries: int = 3, backpressure: Literal["drop", "block"] = "drop", ): self._send_batch = send_batch self._max_buffer_size = max_buffer_size self._batch_size = batch_size self._flush_interval = flush_interval self._max_retries = max_retries self._backpressure = backpressure self._queue: deque = deque(maxlen=max_buffer_size if backpressure == "drop" else None) self._lock = threading.Lock() self._not_full = threading.Condition(self._lock) self._not_empty = threading.Condition(self._lock) self._closed = False self._flush_thread: Optional[threading.Thread] = None self._start_flush_thread() # --- Public API ---
[docs] def put(self, record: Dict[str, Any]) -> None: """Add a serialised log record to the buffer.""" if self._closed: return with self._lock: if self._backpressure == "block": while len(self._queue) >= self._max_buffer_size and not self._closed: self._not_full.wait(timeout=1.0) self._queue.append(record) self._not_empty.notify()
[docs] def flush(self) -> None: """Immediately drain the buffer and send all batches.""" self._drain()
[docs] def close(self) -> None: """Flush remaining records and stop the background thread.""" self._closed = True # Wake up the flush thread so it can exit with self._lock: self._not_empty.notify_all() self._not_full.notify_all() if self._flush_thread and self._flush_thread.is_alive(): self._flush_thread.join(timeout=10.0) # Final drain self._drain()
# --- Internals --- def _start_flush_thread(self) -> None: self._flush_thread = threading.Thread( target=self._flush_loop, name="logeverything-buffer-flush", daemon=True ) self._flush_thread.start() def _flush_loop(self) -> None: while not self._closed: with self._lock: self._not_empty.wait(timeout=self._flush_interval) # Check for shutdown signal (self._closed can change from another thread) should_stop = self._closed and not self._queue if should_stop: break self._drain() def _drain(self) -> None: """Pull batches from the queue and send them.""" while True: batch = self._take_batch() if not batch: break self._send_with_retry(batch) def _take_batch(self) -> List[Dict[str, Any]]: with self._lock: batch: List[Dict[str, Any]] = [] for _ in range(min(self._batch_size, len(self._queue))): batch.append(self._queue.popleft()) if batch and self._backpressure == "block": self._not_full.notify_all() return batch def _send_with_retry(self, batch: List[Dict[str, Any]]) -> None: delay = 0.5 for attempt in range(self._max_retries + 1): try: self._send_batch(batch) return except Exception: if attempt == self._max_retries: logger.warning( "LogBuffer: dropping %d records after %d retries", len(batch), self._max_retries, ) return time.sleep(delay) delay = min(delay * 2, 30.0)