Coverage for zenmqtt/instrumentation/guard.py: 100%

21 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 21:55 +0000

1import time 

2from logging import getLogger 

3from typing import Final 

4 

5logger = getLogger(__name__) 

6 

7# seconds between logs of a hook which keeps failing 

8_ERROR_LOG_INTERVAL: Final[float] = 60.0 

9 

10 

11class HookErrors: 

12 """ 

13 Exceptions of instrumentation hooks: they are counted and logged, never 

14 raised into the client. The first error of a hook is logged, then one log 

15 per interval, so a hook which fails on every packet doesn't flood the logs 

16 but stays visible. 

17 """ 

18 

19 def __init__(self, owner: str, event: str) -> None: 

20 """ 

21 :param owner: the class name of the instrumentation, used in the log 

22 :param event: the name of the log event, e.g. "mqtt_metrics.hook_error" 

23 """ 

24 self._owner = owner 

25 self._event = event 

26 

27 # number of exceptions per hook 

28 self.errors: dict[str, int] = {} 

29 self._logged_at: dict[str, float] = {} 

30 self._suppressed: dict[str, int] = {} 

31 

32 def report(self, name: str, exc: Exception) -> None: 

33 self.errors[name] = self.errors.get(name, 0) + 1 

34 now = time.monotonic() 

35 logged_at = self._logged_at.get(name) 

36 

37 if logged_at is not None and now - logged_at < _ERROR_LOG_INTERVAL: 

38 self._suppressed[name] = self._suppressed.get(name, 0) + 1 

39 return 

40 

41 logger.error( 

42 "%s: %s.%s raised, %s errors since the last log; instrumentation " 

43 "doesn't affect the client", 

44 self._event, 

45 self._owner, 

46 name, 

47 self._suppressed.pop(name, 0) + 1, 

48 exc_info=exc, 

49 ) 

50 self._logged_at[name] = now