valhalla / backend /src /core /log_relay.py
Dontcryx_07
terminal
98f0a17
Raw History Blame Contribute Delete
2.7 kB
"""log_relay — mirror of every project log line for the admin log terminal.
Attaches a second handler to the ROOT logger: each emitted record is
formatted with the project's standard format and appended to a bounded
in-memory ring buffer (polled by the admin-only /api/logs endpoints) and
to a live file backend/output/logs/live.log, so the same output also
survives restarts and stays viewable without the Space console.
Architecture: installed once by src/core/log.py setup_logging(); consumed
by Odin.py's admin-gated endpoints and the frontend LogTerminal panel.
Design: stdlib-only, a one-way mirror — never changes existing handlers,
levels, or the file rotation policy.
"""
from __future__ import annotations
import logging
import threading
from collections import deque
from itertools import count
from src.config import LOG_DIR
from src.core.log import DATE_FORMAT, LOG_FORMAT
MAX_LINES = 2000
LIVE_LOG_PATH = LOG_DIR / "live.log"
_seq = count(1)
_buffer: deque = deque(maxlen=MAX_LINES)
_latest_seq = 0
_lock = threading.Lock()
_installed = False
class LogRelayHandler(logging.Handler):
"""Appends each formatted record to the ring buffer and the live file."""
def __init__(self, level: int = logging.NOTSET) -> None:
super().__init__(level)
self._formatter = logging.Formatter(LOG_FORMAT, DATE_FORMAT)
def emit(self, record: logging.LogRecord) -> None:
global _latest_seq
try:
text = self._formatter.format(record)
except Exception:
text = f"{record.name} | {record.getMessage()}"
with _lock:
_latest_seq = next(_seq)
_buffer.append({"seq": _latest_seq, "level": record.levelname, "text": text})
try:
LOG_DIR.mkdir(parents=True, exist_ok=True)
with open(LIVE_LOG_PATH, "a", encoding="utf-8") as fh:
fh.write(text + "\n")
except OSError:
pass
def install_relay() -> None:
"""Attach the relay handler to the root logger exactly once."""
global _installed
if _installed:
return
logging.getLogger().addHandler(LogRelayHandler())
_installed = True
def relay_lines(since: int = 0) -> dict:
"""Return relayed entries with seq > since, plus the latest seq as cursor.
The cursor tracks the last emitted sequence, so polling stays
incremental even after the buffer is cleared.
"""
with _lock:
lines = [entry for entry in _buffer if entry["seq"] > since]
return {"lines": lines, "next": _latest_seq}
def clear_relay() -> None:
"""Drop the in-memory buffer (the live file is intentionally kept)."""
with _lock:
_buffer.clear()