JAA-ATS-Tool / src /app_logger.py
saitejatirunagari's picture
fix: resolve UnicodeEncodeError crashing pipeline on Windows cp1252
b90c16e
Raw
History Blame
3.94 kB
"""
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