"""Structured logging: console + per-run JSONL file under logs/.""" from __future__ import annotations import json import logging import sys import time from pathlib import Path class JsonlHandler(logging.Handler): def __init__(self, path: Path): super().__init__() path.parent.mkdir(parents=True, exist_ok=True) self._f = open(path, "a") def emit(self, record: logging.LogRecord) -> None: entry = { "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "level": record.levelname, "name": record.name, "msg": record.getMessage(), } if hasattr(record, "extra_fields"): entry.update(record.extra_fields) self._f.write(json.dumps(entry) + "\n") self._f.flush() def get_logger(name: str, logfile: Path | None = None) -> logging.Logger: logger = logging.getLogger(name) if logger.handlers: return logger logger.setLevel(logging.INFO) sh = logging.StreamHandler(sys.stderr) sh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) logger.addHandler(sh) if logfile is not None: logger.addHandler(JsonlHandler(logfile)) return logger