"""키별 레이트리밋 경고 로깅 헬퍼 — 순수 모듈(settings·DB·네트워크 참조 없음). 고빈도 루프(종목별 스캔·시세 캐시 미스 등)에서 반복되는 예외를 ``logger.debug`` 로 숨기지 않으면서(CLAUDE.md §4: 예외 로그는 최소 warning) 로그 범람도 막기 위한 것이다. 같은 ``key`` 는 ``interval`` 초에 1회만 warning 으로 기록하고, 그 사이 억제된 횟수는 다음 출력에 ``(+N건 억제)`` 로 덧붙인다. 키는 정적 태그(종목코드 등 가변값 금지)여야 메모리가 불어나지 않는다. """ from __future__ import annotations import logging import threading import time DEFAULT_INTERVAL_SEC = 300.0 _lock = threading.Lock() # key -> (마지막 출력 monotonic 시각, 그 이후 억제 횟수) _state: dict[str, tuple[float, int]] = {} def should_log(key: str, interval: float = DEFAULT_INTERVAL_SEC) -> tuple[bool, int]: """(출력 여부, 직전 출력 이후 억제된 횟수). 출력하기로 하면 상태를 갱신한다.""" now = time.monotonic() with _lock: last, suppressed = _state.get(key, (None, 0)) if last is None or now - last >= interval: _state[key] = (now, 0) return True, suppressed _state[key] = (last, suppressed + 1) return False, suppressed + 1 def warn_throttled( logger: logging.Logger, key: str, msg: str, *args: object, interval: float = DEFAULT_INTERVAL_SEC, ) -> bool: """같은 key 당 interval 초에 1회만 ``logger.warning(msg, *args)``. 출력했으면 True.""" ok, suppressed = should_log(key, interval) if not ok: return False if suppressed: msg = f"{msg} (+{suppressed}건 억제, {interval:.0f}초 내 동일 키)" logger.warning(msg, *args, stacklevel=2) return True def reset_for_tests() -> None: with _lock: _state.clear()