"""Session telemetry — lightweight event/metric log for post-run analysis. Writes a JSON-lines file (one event per line) to the session directory. Each event has a timestamp, type, and payload. Designed to be grep-friendly. Usage: tel = Telemetry(session_dir) tel.event("scene_threshold_changed", {"from": 0.10, "to": 0.15}) tel.metric("scene_detection", {"start": 120.0, "end": 135.0, "frames_found": 3, "elapsed_ms": 1200}) tel.close() """ import json import logging import time from pathlib import Path log = logging.getLogger(__name__) class Telemetry: def __init__(self, session_dir: Path): self._path = session_dir / "telemetry.jsonl" self._start = time.monotonic() self._wall_start = time.time() self._file = None self._log_handler = None try: session_dir.mkdir(parents=True, exist_ok=True) self._file = open(self._path, "a") except Exception as e: log.warning("Telemetry init failed: %s", e) # Also save full logs to session directory try: log_path = session_dir / "session.log" handler = logging.FileHandler(str(log_path), mode="a") handler.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter( "%(asctime)s %(levelname)-7s %(name)s: %(message)s", datefmt="%H:%M:%S", )) logging.getLogger().addHandler(handler) self._log_handler = handler except Exception as e: log.warning("Log file handler failed: %s", e) self.event("session_start", {"session_dir": str(session_dir)}) def event(self, name: str, data: dict | None = None) -> None: """Log a discrete event (setting change, mode switch, user action).""" self._write("event", name, data or {}) def metric(self, name: str, data: dict) -> None: """Log a measurement (processing time, frame count, etc).""" self._write("metric", name, data) def _write(self, kind: str, name: str, data: dict) -> None: if not self._file: return entry = { "t": round(time.monotonic() - self._start, 3), "wall": round(time.time(), 3), "kind": kind, "name": name, **data, } try: self._file.write(json.dumps(entry) + "\n") self._file.flush() except Exception: pass def close(self) -> None: self.event("session_end", {}) if self._log_handler: logging.getLogger().removeHandler(self._log_handler) try: self._log_handler.close() except Exception: pass self._log_handler = None if self._file: try: self._file.close() except Exception: pass self._file = None