Skip to content

Commit 63e0b89

Browse files
committed
Add a RateLimitedLogger implementation
It is a wrapper around a `Logger` instance and limits logs when there's an ongoing outage. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 374665f commit 63e0b89

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Logging utilities for the SDK."""
5+
6+
import logging
7+
from collections.abc import Mapping
8+
from datetime import datetime, timedelta
9+
from types import TracebackType
10+
11+
_ExcInfoType = (
12+
bool
13+
| BaseException
14+
| tuple[None, None, None]
15+
| tuple[type[BaseException], BaseException, TracebackType | None]
16+
| None
17+
)
18+
19+
DEFAULT_RATE_LIMIT = timedelta(minutes=15)
20+
21+
# The standard logging.py file uses variadic arguments in the logging methods, but the
22+
# type hints file has more specific parameters. pylint is not able to handle this, so
23+
# we need to suppress the warning.
24+
#
25+
# pylint: disable=arguments-differ
26+
27+
28+
class RateLimitedLogger:
29+
"""Logger that limits the rate of logging messages.
30+
31+
The first message is logged immediately. Subsequent messages are ignored until the
32+
rate limit interval has elapsed. After that the next request goes through, and so on.
33+
34+
This allows a new outage to be reported immediately and subsequent logs to be
35+
rate-limited.
36+
37+
When an outage has been resolved, the `reset()` method may be used to reset the
38+
logger and the next message will get logged immediately.
39+
"""
40+
41+
def __init__(
42+
self,
43+
logger: logging.Logger,
44+
rate_limit: timedelta = DEFAULT_RATE_LIMIT,
45+
) -> None:
46+
"""Initialize the logger.
47+
48+
Args:
49+
logger: Logger to rate-limit.
50+
rate_limit: Time interval between two log messages.
51+
"""
52+
self._logger = logger
53+
self._started: bool = False
54+
self._last_log_time: datetime | None = None
55+
self._rate_limit: timedelta = rate_limit
56+
57+
def set_rate_limit(self, rate_limit: timedelta) -> None:
58+
"""Set the rate limit for the logger.
59+
60+
Args:
61+
rate_limit: Time interval between two log messages.
62+
"""
63+
self._rate_limit = rate_limit
64+
65+
def is_limiting(self) -> bool:
66+
"""Return whether rate limiting is active.
67+
68+
This is true when a previous message has already been logged, and can be reset
69+
by calling the `reset()` method.
70+
"""
71+
return self._started
72+
73+
def reset(self) -> None:
74+
"""Reset the logger to healthy state."""
75+
self._started = False
76+
self._last_log_time = None
77+
78+
def log( # pylint: disable=too-many-arguments
79+
self,
80+
level: int,
81+
msg: object,
82+
*args: object,
83+
exc_info: _ExcInfoType = None,
84+
stack_info: bool = False,
85+
stacklevel: int = 1,
86+
extra: Mapping[str, object] | None = None,
87+
) -> None:
88+
"""Log a message.
89+
90+
Args:
91+
level: Log level.
92+
msg: Log message.
93+
*args: Arguments for the log message.
94+
exc_info: Exception information.
95+
stack_info: Stack information.
96+
stacklevel: Stack level.
97+
extra: Extra information.
98+
"""
99+
if self._rate_limit is None:
100+
self._logger.log(
101+
level,
102+
msg,
103+
*args,
104+
exc_info=exc_info,
105+
stack_info=stack_info,
106+
stacklevel=stacklevel,
107+
extra=extra,
108+
)
109+
return
110+
111+
current_time = datetime.now()
112+
if (
113+
not self._started
114+
or self._last_log_time is None
115+
or (current_time - self._last_log_time) >= self._rate_limit
116+
):
117+
self._logger.log(
118+
level,
119+
msg,
120+
*args,
121+
exc_info=exc_info,
122+
stack_info=stack_info,
123+
stacklevel=stacklevel,
124+
extra=extra,
125+
)
126+
self._last_log_time = current_time
127+
self._started = True
128+
129+
def info( # pylint: disable=too-many-arguments
130+
self,
131+
msg: object,
132+
*args: object,
133+
exc_info: _ExcInfoType = None,
134+
stack_info: bool = False,
135+
stacklevel: int = 1,
136+
extra: Mapping[str, object] | None = None,
137+
) -> None:
138+
"""Log an info message.
139+
140+
Args:
141+
msg: Log message.
142+
*args: Arguments for the log message.
143+
exc_info: Exception information.
144+
stack_info: Stack information.
145+
stacklevel: Stack level.
146+
extra: Extra information.
147+
"""
148+
self.log(
149+
logging.INFO,
150+
msg,
151+
*args,
152+
exc_info=exc_info,
153+
stack_info=stack_info,
154+
stacklevel=stacklevel,
155+
extra=extra,
156+
)
157+
158+
def debug( # pylint: disable=too-many-arguments
159+
self,
160+
msg: object,
161+
*args: object,
162+
exc_info: _ExcInfoType = None,
163+
stack_info: bool = False,
164+
stacklevel: int = 1,
165+
extra: Mapping[str, object] | None = None,
166+
) -> None:
167+
"""Log a debug message.
168+
169+
Args:
170+
msg: Log message.
171+
*args: Arguments for the log message.
172+
exc_info: Exception information.
173+
stack_info: Stack information.
174+
stacklevel: Stack level.
175+
extra: Extra information.
176+
"""
177+
self.log(
178+
logging.DEBUG,
179+
msg,
180+
*args,
181+
exc_info=exc_info,
182+
stack_info=stack_info,
183+
stacklevel=stacklevel,
184+
extra=extra,
185+
)
186+
187+
def warning( # pylint: disable=too-many-arguments
188+
self,
189+
msg: object,
190+
*args: object,
191+
exc_info: _ExcInfoType = None,
192+
stack_info: bool = False,
193+
stacklevel: int = 1,
194+
extra: Mapping[str, object] | None = None,
195+
) -> None:
196+
"""Log a warning message.
197+
198+
Args:
199+
msg: Log message.
200+
*args: Arguments for the log message.
201+
exc_info: Exception information.
202+
stack_info: Stack information.
203+
stacklevel: Stack level.
204+
extra: Extra information.
205+
"""
206+
self.log(
207+
logging.WARNING,
208+
msg,
209+
*args,
210+
exc_info=exc_info,
211+
stack_info=stack_info,
212+
stacklevel=stacklevel,
213+
extra=extra,
214+
)
215+
216+
def critical( # pylint: disable=too-many-arguments
217+
self,
218+
msg: object,
219+
*args: object,
220+
exc_info: _ExcInfoType = None,
221+
stack_info: bool = False,
222+
stacklevel: int = 1,
223+
extra: Mapping[str, object] | None = None,
224+
) -> None:
225+
"""Log a critical message.
226+
227+
Args:
228+
msg: Log message.
229+
*args: Arguments for the log message.
230+
exc_info: Exception information.
231+
stack_info: Stack information.
232+
stacklevel: Stack level.
233+
extra: Extra information.
234+
"""
235+
self.log(
236+
logging.CRITICAL,
237+
msg,
238+
*args,
239+
exc_info=exc_info,
240+
stack_info=stack_info,
241+
stacklevel=stacklevel,
242+
extra=extra,
243+
)
244+
245+
def error( # pylint: disable=too-many-arguments
246+
self,
247+
msg: object,
248+
*args: object,
249+
exc_info: _ExcInfoType = None,
250+
stack_info: bool = False,
251+
stacklevel: int = 1,
252+
extra: Mapping[str, object] | None = None,
253+
) -> None:
254+
"""Log an error message.
255+
256+
Args:
257+
msg: Log message.
258+
*args: Arguments for the log message.
259+
exc_info: Exception information.
260+
stack_info: Stack information.
261+
stacklevel: Stack level.
262+
extra: Extra information.
263+
"""
264+
self.log(
265+
logging.ERROR,
266+
msg,
267+
*args,
268+
exc_info=exc_info,
269+
stack_info=stack_info,
270+
stacklevel=stacklevel,
271+
extra=extra,
272+
)
273+
274+
def exception( # pylint: disable=too-many-arguments
275+
self,
276+
msg: object,
277+
*args: object,
278+
exc_info: _ExcInfoType = True,
279+
stack_info: bool = False,
280+
stacklevel: int = 1,
281+
extra: Mapping[str, object] | None = None,
282+
) -> None:
283+
"""Log an exception message.
284+
285+
Args:
286+
msg: Log message.
287+
*args: Arguments for the log message.
288+
exc_info: Exception information.
289+
stack_info: Stack information.
290+
stacklevel: Stack level.
291+
extra: Extra information.
292+
"""
293+
self.error(
294+
msg,
295+
*args,
296+
exc_info=exc_info,
297+
stack_info=stack_info,
298+
stacklevel=stacklevel,
299+
extra=extra,
300+
)

0 commit comments

Comments
 (0)