Ctrl K
ComunidadGuías…

app/logbuffer.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""The last few hundred log lines, kept in memory for the Settings page.

When something goes wrong in a self-hosted app the answer is in the container's
stdout, and reaching that means knowing `kubectl logs` and having host access —
reasonable to ask of whoever deployed it, unreasonable at the moment they are
working out why an import failed.

WARNING and above, in memory only and bounded: decisions.md #90.
"""

from __future__ import annotations

import datetime as dt
import logging
from collections import deque
from dataclasses import dataclass

# A few hundred lines is minutes of a busy install and days of a quiet one, and
# costs a handful of kilobytes.
CAPACITY = 400


@dataclass(frozen=True)
class Line:
    # A monotonic id, so a page that is tailing can ask for "everything after
    # what I already have" rather than re-fetching the window and working out
    # what is new. Survives eviction: ids keep counting up even as old lines
    # fall off the front.
    seq: int
    when: str
    level: str
    logger: str
    message: str


_lines: deque[Line] = deque(maxlen=CAPACITY)
_next_seq = 0


class BufferHandler(logging.Handler):
    """Formats into the ring buffer. Never raises into the caller.

    A logging handler that throws takes down whatever was being logged, which
    in this app is usually the error path of something already going wrong.
    """

    def emit(self, record: logging.LogRecord) -> None:
        global _next_seq
        try:
            _next_seq += 1
            _lines.append(Line(
                seq=_next_seq,
                when=dt.datetime.fromtimestamp(
                    record.created, dt.timezone.utc).isoformat(timespec="seconds"),
                level=record.levelname,
                logger=record.name,
                message=record.getMessage(),
            ))
        except Exception:                                    # noqa: BLE001
            pass


def install(level: int = logging.WARNING) -> None:
    """Attach to the root logger. Idempotent, so a reload cannot double up."""
    root = logging.getLogger()
    if any(isinstance(h, BufferHandler) for h in root.handlers):
        return
    handler = BufferHandler()
    handler.setLevel(level)
    root.addHandler(handler)


def recent(limit: int = CAPACITY) -> list[Line]:
    """Newest last, which is the order a console is read in."""
    return list(_lines)[-limit:]


def since(seq: int, limit: int = CAPACITY) -> list[Line]:
    """Everything newer than `seq` — what a tailing page asks for.

    If the buffer has wrapped past `seq` (a burst longer than the window while
    the page sat idle), this returns the whole window rather than nothing: the
    reader has missed lines either way, and showing the recent ones is more
    use than showing none.
    """
    fresh = [line for line in _lines if line.seq > seq]
    return fresh[-limit:]


def clear() -> None:
    global _next_seq
    _lines.clear()
    _next_seq = 0