from __future__ import annotations import contextvars import logging import os import threading import time from contextlib import contextmanager from typing import Any, Iterator _current_timing: contextvars.ContextVar["TimingCollector | None"] = contextvars.ContextVar( "surya_current_timing", default=None, ) def timing_enabled() -> bool: return os.getenv("SUYA_TIMING_ENABLED", "true").strip().lower() not in { "0", "false", "no", "off", } class TimingCollector: def __init__(self, *, request_id: str = "-", batch_size: int = 0) -> None: self.request_id = request_id self.batch_size = batch_size self._lock = threading.Lock() self._events: list[dict[str, Any]] = [] def record(self, name: str, duration_ms: float, **metadata: Any) -> None: if not timing_enabled(): return event = { "name": name, "duration_ms": round(duration_ms, 2), } if metadata: event["metadata"] = { key: value for key, value in metadata.items() if value is not None } with self._lock: self._events.append(event) def summary(self) -> list[dict[str, Any]]: with self._lock: return list(self._events) def get_current_timing() -> TimingCollector | None: return _current_timing.get() def set_current_timing(collector: TimingCollector | None): return _current_timing.set(collector) def reset_current_timing(token) -> None: _current_timing.reset(token) @contextmanager def timing_span(name: str, **metadata: Any) -> Iterator[None]: collector = get_current_timing() if collector is None or not timing_enabled(): yield return start = time.perf_counter() try: yield finally: collector.record(name, (time.perf_counter() - start) * 1000, **metadata) def log_timing_summary( logger: logging.Logger, collector: TimingCollector, *, message: str = "surya_timing_summary", ) -> None: if not timing_enabled(): return logger.info( "%s request_id=%s batch_size=%s events=%s", message, collector.request_id, collector.batch_size, collector.summary(), )