from __future__ import annotations import json import sys from datetime import datetime from pathlib import Path from typing import Any, Dict, Optional def utc_iso() -> str: return datetime.utcnow().replace(microsecond=0).isoformat() + "Z" class Logger: def __init__(self, log_path: Optional[str] = None): self.log_path = Path(log_path) if log_path else None if self.log_path: self.log_path.parent.mkdir(parents=True, exist_ok=True) def _write(self, level: str, msg: str, extra: Optional[Dict[str, Any]] = None) -> None: line = f"{utc_iso()} [{level}] {msg}" print(line, file=sys.stdout, flush=True) if self.log_path: payload = {"ts": utc_iso(), "level": level, "msg": msg, "extra": extra or {}} with self.log_path.open("a", encoding="utf-8") as f: f.write(json.dumps(payload, ensure_ascii=False) + "\n") def info(self, msg: str, extra: Optional[Dict[str, Any]] = None) -> None: self._write("INFO", msg, extra) def warn(self, msg: str, extra: Optional[Dict[str, Any]] = None) -> None: self._write("WARN", msg, extra) def error(self, msg: str, extra: Optional[Dict[str, Any]] = None) -> None: self._write("ERROR", msg, extra)