78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
class LogWatcher:
|
|
def __init__(self, log_path: str, poll_interval: float = 0.1) -> None:
|
|
self.log_path = Path(log_path)
|
|
self.poll_interval = poll_interval
|
|
self._lines: list[str] = []
|
|
self._markers: dict[str, int] = {}
|
|
self._lock = threading.Lock()
|
|
self._stop = threading.Event()
|
|
self._thread: Optional[threading.Thread] = None
|
|
|
|
def start(self) -> None:
|
|
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.log_path.touch(exist_ok=True)
|
|
self._stop.clear()
|
|
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
self._thread.start()
|
|
|
|
def stop(self) -> None:
|
|
self._stop.set()
|
|
if self._thread and self._thread.is_alive():
|
|
self._thread.join(timeout=2)
|
|
|
|
def _run(self) -> None:
|
|
f = self.log_path.open("r", encoding="utf-8", errors="replace")
|
|
f.seek(0, 2)
|
|
try:
|
|
while not self._stop.is_set():
|
|
pos = f.tell()
|
|
line = f.readline()
|
|
if not line:
|
|
if self.log_path.exists() and self.log_path.stat().st_size < pos:
|
|
f.close()
|
|
f = self.log_path.open("r", encoding="utf-8", errors="replace")
|
|
time.sleep(self.poll_interval)
|
|
continue
|
|
with self._lock:
|
|
self._lines.append(line.rstrip("\n"))
|
|
finally:
|
|
f.close()
|
|
|
|
def set_marker(self, name: str) -> None:
|
|
with self._lock:
|
|
self._markers[name] = len(self._lines)
|
|
|
|
def get_lines_since(self, marker: str) -> list[str]:
|
|
with self._lock:
|
|
idx = self._markers.get(marker, 0)
|
|
return list(self._lines[idx:])
|
|
|
|
def get_all_lines(self) -> list[str]:
|
|
with self._lock:
|
|
return list(self._lines)
|
|
|
|
def search(self, pattern: str, since_marker: Optional[str] = None) -> list[str]:
|
|
regex = re.compile(pattern)
|
|
lines = self.get_all_lines() if since_marker is None else self.get_lines_since(since_marker)
|
|
return [line for line in lines if regex.search(line)]
|
|
|
|
def error_lines(self, since_marker: Optional[str] = None) -> list[str]:
|
|
lines = self.get_all_lines() if since_marker is None else self.get_lines_since(since_marker)
|
|
return [line for line in lines if "[ERROR]" in line]
|
|
|
|
def warning_lines(self, since_marker: Optional[str] = None) -> list[str]:
|
|
lines = self.get_all_lines() if since_marker is None else self.get_lines_since(since_marker)
|
|
return [line for line in lines if "[WARN" in line]
|
|
|
|
def has_errors(self, since_marker: Optional[str] = None) -> bool:
|
|
return len(self.error_lines(since_marker)) > 0
|