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
« 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
5logger = getLogger(__name__)
7# seconds between logs of a hook which keeps failing
8_ERROR_LOG_INTERVAL: Final[float] = 60.0
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 """
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
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] = {}
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)
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
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