Spaces:
Sleeping
Sleeping
| """ | |
| Centralized logger for Job Automation Agent. | |
| Writes to data/logs/run_YYYY-MM-DD_HH-MM-SS.log | |
| Also keeps last N lines in memory for the UI. | |
| """ | |
| import logging | |
| import os | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| from collections import deque | |
| LOG_DIR = Path("data/logs") | |
| _memory_buffer: deque = deque(maxlen=500) # last 500 lines in memory | |
| _current_log_file: str = "" | |
| def setup(run_id: str = "") -> str: | |
| """ | |
| Set up file + console logging. Returns path to log file. | |
| Call once at the start of each pipeline run. | |
| """ | |
| global _current_log_file, _memory_buffer | |
| _memory_buffer.clear() | |
| LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| tag = run_id or datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | |
| log_path = str(LOG_DIR / f"run_{tag}.log") | |
| _current_log_file = log_path | |
| # Root logger | |
| root = logging.getLogger() | |
| root.setLevel(logging.DEBUG) | |
| # Remove old handlers | |
| for h in root.handlers[:]: | |
| root.removeHandler(h) | |
| fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s — %(message)s", | |
| datefmt="%H:%M:%S") | |
| # File handler | |
| fh = logging.FileHandler(log_path, encoding="utf-8") | |
| fh.setLevel(logging.DEBUG) | |
| fh.setFormatter(fmt) | |
| root.addHandler(fh) | |
| # Memory handler (for UI display) | |
| mh = _MemoryHandler() | |
| mh.setLevel(logging.DEBUG) | |
| mh.setFormatter(fmt) | |
| root.addHandler(mh) | |
| # Force UTF-8 on stdout/stderr so unicode chars (✓ → etc.) don't crash on Windows cp1252 | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| except AttributeError: | |
| pass # already replaced by Streamlit or not a TextIOWrapper | |
| # Redirect stdout/stderr so print() and Playwright output are captured in log file | |
| sys.stdout = _TeeStream(sys.stdout, log_path) | |
| sys.stderr = _TeeStream(sys.stderr, log_path) | |
| return log_path | |
| def get_log_file() -> str: | |
| return _current_log_file | |
| def get_memory_lines() -> list[str]: | |
| return list(_memory_buffer) | |
| def read_log_file(tail: int = 200) -> str: | |
| """Read last N lines of current log file.""" | |
| if not _current_log_file or not os.path.exists(_current_log_file): | |
| return "" | |
| try: | |
| with open(_current_log_file, "r", encoding="utf-8", errors="replace") as f: | |
| lines = f.readlines() | |
| return "".join(lines[-tail:]) | |
| except Exception: | |
| return "" | |
| def list_log_files() -> list[str]: | |
| """Return all log files, newest first.""" | |
| if not LOG_DIR.exists(): | |
| return [] | |
| return sorted( | |
| [str(p) for p in LOG_DIR.glob("run_*.log")], | |
| reverse=True, | |
| ) | |
| class _MemoryHandler(logging.Handler): | |
| def emit(self, record): | |
| try: | |
| _memory_buffer.append(self.format(record)) | |
| except Exception: | |
| pass | |
| class _TeeStream: | |
| """Writes to both original stream and log file.""" | |
| def __init__(self, original, log_path: str): | |
| self._orig = original | |
| self._log = log_path | |
| def write(self, text): | |
| try: | |
| self._orig.write(text) | |
| except (UnicodeEncodeError, UnicodeDecodeError): | |
| # Windows cp1252 console can't handle unicode — write safe version | |
| try: | |
| self._orig.write(text.encode("utf-8", errors="replace").decode("ascii", errors="replace")) | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| if text.strip(): | |
| try: | |
| with open(self._log, "a", encoding="utf-8", errors="replace") as f: | |
| f.write(text) | |
| except Exception: | |
| pass | |
| def flush(self): | |
| try: | |
| self._orig.flush() | |
| except Exception: | |
| pass | |
| def fileno(self): | |
| return self._orig.fileno() | |
| def isatty(self): | |
| return False | |