Skip to content

jmwalletd.log_buffer

jmwalletd.log_buffer

In-memory ring buffer sink for jmwalletd logs.

jam's Logs page expects to fetch the daemon's recent log output as plain text. The reference joinmarket-clientserver writes a jmwalletd_stdout.log file on disk, but jm-ng currently logs only to stderr. Instead of forcing a file dependency, we attach a loguru sink that keeps the most recent N formatted log lines in a thread-safe deque; a small router serves that snapshot back to jam.

The buffer is bounded so long-running daemons cannot grow the heap unboundedly, and a secondary soft cap on total bytes prevents pathological spam from blowing past reasonable memory even within the line limit.

Attributes

__all__ = ['LogRingBuffer', 'get_log_buffer', 'install_log_sink'] module-attribute

Classes

LogRingBuffer

Thread-safe bounded in-memory log buffer.

Stores formatted log lines (one per loguru record) up to max_lines entries or max_bytes total bytes, whichever fills first.

Source code in jmwalletd/src/jmwalletd/log_buffer.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class LogRingBuffer:
    """Thread-safe bounded in-memory log buffer.

    Stores formatted log lines (one per loguru record) up to ``max_lines``
    entries or ``max_bytes`` total bytes, whichever fills first.
    """

    def __init__(self, max_lines: int = 2000, max_bytes: int = 1_000_000) -> None:
        self._max_lines = max_lines
        self._max_bytes = max_bytes
        self._lines: deque[str] = deque(maxlen=max_lines)
        self._size_bytes = 0
        self._lock = threading.Lock()

    def append(self, message: str) -> None:
        """Append a formatted log line. ``message`` already ends in ``\\n``."""
        with self._lock:
            # Enforce the byte cap before adding by dropping oldest entries.
            while self._lines and self._size_bytes + len(message) > self._max_bytes:
                oldest = self._lines.popleft()
                self._size_bytes -= len(oldest)

            if len(self._lines) == self._max_lines:
                # deque will drop the oldest on append; update accounting first.
                self._size_bytes -= len(self._lines[0])

            self._lines.append(message)
            self._size_bytes += len(message)

    def text(self) -> str:
        """Return a snapshot of the buffer joined as plain text."""
        with self._lock:
            return "".join(self._lines)

    def clear(self) -> None:
        with self._lock:
            self._lines.clear()
            self._size_bytes = 0
Methods:
__init__(max_lines: int = 2000, max_bytes: int = 1000000) -> None
Source code in jmwalletd/src/jmwalletd/log_buffer.py
32
33
34
35
36
37
def __init__(self, max_lines: int = 2000, max_bytes: int = 1_000_000) -> None:
    self._max_lines = max_lines
    self._max_bytes = max_bytes
    self._lines: deque[str] = deque(maxlen=max_lines)
    self._size_bytes = 0
    self._lock = threading.Lock()
append(message: str) -> None

Append a formatted log line. message already ends in \n.

Source code in jmwalletd/src/jmwalletd/log_buffer.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def append(self, message: str) -> None:
    """Append a formatted log line. ``message`` already ends in ``\\n``."""
    with self._lock:
        # Enforce the byte cap before adding by dropping oldest entries.
        while self._lines and self._size_bytes + len(message) > self._max_bytes:
            oldest = self._lines.popleft()
            self._size_bytes -= len(oldest)

        if len(self._lines) == self._max_lines:
            # deque will drop the oldest on append; update accounting first.
            self._size_bytes -= len(self._lines[0])

        self._lines.append(message)
        self._size_bytes += len(message)
clear() -> None
Source code in jmwalletd/src/jmwalletd/log_buffer.py
59
60
61
62
def clear(self) -> None:
    with self._lock:
        self._lines.clear()
        self._size_bytes = 0
text() -> str

Return a snapshot of the buffer joined as plain text.

Source code in jmwalletd/src/jmwalletd/log_buffer.py
54
55
56
57
def text(self) -> str:
    """Return a snapshot of the buffer joined as plain text."""
    with self._lock:
        return "".join(self._lines)

Functions:

get_log_buffer() -> LogRingBuffer

Return the process-wide log ring buffer, creating it on first use.

Source code in jmwalletd/src/jmwalletd/log_buffer.py
69
70
71
72
73
74
def get_log_buffer() -> LogRingBuffer:
    """Return the process-wide log ring buffer, creating it on first use."""
    global _buffer
    if _buffer is None:
        _buffer = LogRingBuffer()
    return _buffer

install_log_sink(level: str = 'DEBUG') -> None

Attach the ring buffer as a loguru sink.

Safe to call multiple times; the existing sink is removed first so the level can be updated at runtime without duplicating records.

Source code in jmwalletd/src/jmwalletd/log_buffer.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def install_log_sink(level: str = "DEBUG") -> None:
    """Attach the ring buffer as a loguru sink.

    Safe to call multiple times; the existing sink is removed first so the
    level can be updated at runtime without duplicating records.
    """
    global _sink_id
    buffer = get_log_buffer()
    if _sink_id is not None:
        with contextlib.suppress(ValueError):
            # Already removed elsewhere (e.g. logger.remove()).
            logger.remove(_sink_id)
        _sink_id = None

    _sink_id = logger.add(
        buffer.append,
        format=(
            "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}"
        ),
        level=level.upper(),
        colorize=False,
        enqueue=False,
    )