feat: events journal read path with filters and backward pagination

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:57:32 -04:00
co-authored by Claude Fable 5
parent 0343eb4a24
commit 16c2922bea
2 changed files with 191 additions and 1 deletions
+81 -1
View File
@@ -11,7 +11,7 @@ import os
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Optional, Iterable
from typing import Dict, Any, Optional, Callable, Iterable, List, Tuple, Union
logger = logging.getLogger(__name__)
@@ -334,6 +334,86 @@ class MessageJournal:
}
def _iter_journal_events(journal_dir: Union[str, Path], journal_file: str) -> Iterable[Dict[str, Any]]:
"""Yield event dicts from the journal files, newest event first.
Reads the current file, then rotated backups newest-to-oldest (backup
names embed rotation timestamps, so filename sort is chronological).
"""
dirp = Path(journal_dir)
files = [dirp / journal_file]
files.extend(sorted(dirp.glob(journal_file + '.*'), reverse=True))
for f in files:
if not f.is_file():
continue
try:
lines = f.read_text(encoding='utf-8', errors='replace').splitlines()
except OSError as e:
logger.warning(f"Cannot read journal file {f}: {e}")
continue
for line in reversed(lines):
try:
ev = json.loads(line)
except ValueError:
continue
if isinstance(ev, dict):
yield ev
def filter_events(
events: Iterable[Dict[str, Any]],
limit: int = 100,
before: Optional[float] = None,
host: Optional[str] = None,
level: Optional[str] = None,
q: Optional[str] = None,
predicate: Optional[Callable[[Dict[str, Any]], bool]] = None,
) -> Tuple[List[Dict[str, Any]], bool]:
"""Filter an iterable of event dicts already ordered newest-first.
Returns (events, more): up to *limit* matching events, and whether at
least one further matching event exists beyond the limit.
"""
host_l = host.lower() if host else None
level_l = level.lower() if level else None
q_l = q.lower() if q else None
out: List[Dict[str, Any]] = []
for ev in events:
ts = ev.get('ts')
if before is not None and (not isinstance(ts, (int, float)) or ts >= before):
continue
if host_l and host_l not in str(ev.get('host') or '').lower():
continue
if level_l and str(ev.get('level') or '').lower() != level_l:
continue
if q_l and q_l not in str(ev.get('message') or '').lower():
continue
if predicate is not None and not predicate(ev):
continue
if len(out) >= limit:
return out, True
out.append(ev)
return out, False
def read_events(
journal_dir: Union[str, Path],
journal_file: str = 'events.journal',
*,
limit: int = 100,
before: Optional[float] = None,
host: Optional[str] = None,
level: Optional[str] = None,
q: Optional[str] = None,
predicate: Optional[Callable[[Dict[str, Any]], bool]] = None,
) -> Tuple[List[Dict[str, Any]], bool]:
"""Read filtered events newest-first from the events journal files."""
return filter_events(
_iter_journal_events(journal_dir, journal_file),
limit=limit, before=before, host=host, level=level, q=q, predicate=predicate,
)
# Global journal instance
_journal_instance: Optional[MessageJournal] = None