structlog-throttling 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of structlog-throttling might be problematic. Click here for more details.

@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: structlog-throttling
3
+ Version: 1.1.0
4
+ Summary: Log throttling utilities for structlog.
5
+ Keywords: structlog,throttling,throttle
6
+ License-Expression: MIT OR Apache-2.0
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: System :: Logging
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: structlog>=25.5.0
19
+ Requires-Python: >=3.9
20
+ Project-URL: Codeberg, https://codeberg.org/tomasfarias/structlog-throttling
21
+ Description-Content-Type: text/markdown
22
+
23
+ # *structlog-throttling*: Throttling for *[structlog](https://www.structlog.org/)* loggers
24
+
25
+ <a href="https://pypi.org/project/structlog-throttling/"><img src="https://img.shields.io/pypi/pyversions/structlog-throttling.svg" alt="Supported Python versions from PyPI." /></a>
26
+
27
+ Logging offers a trade-off between visibility and performance. A particularly high performance cost can be incurred when logging in each iteration of a loop, common [hot spots](https://en.wikipedia.org/wiki/Hot_spot_%28computer_programming%29) in most programs. A solution to this problem is to space out the log calls such that they only happen every some time instead of on every iteration of the loop. By tweaking the time in between log calls we can move within the visibility-performance trade-off.
28
+
29
+ *structlog-throttling* brings this solution to *[structlog](https://www.structlog.org/)* in the form of processors to throttle log calls based on time, or call count.
30
+
31
+ ## Getting started
32
+
33
+ ### Installation
34
+
35
+ Install *structlog-throttling* from PyPI:
36
+
37
+ ```sh
38
+ pip install structlog-throttling
39
+ ```
40
+
41
+ ### Configure
42
+
43
+ When configuring *structlog*, use one of the processors offered by *structlog-throttling*. I recommend putting the processor close to the beginning of your processor chain, to avoid processing logs that will ultimately be dropped:
44
+
45
+ ```python
46
+ import structlog
47
+ from structlog_throttling.processors import LogTimeThrottler
48
+
49
+
50
+ structlog.configure(
51
+ processors=[
52
+ # Logs with the same 'event' will only be allowed through every 5 seconds.
53
+ LogTimeThrottler("event", every_seconds=5),
54
+ ...
55
+ ]
56
+ )
57
+ ```
58
+
59
+ ## Examples
60
+
61
+ Throttle logs based on log level every 5 seconds:
62
+
63
+ ```python
64
+ import structlog
65
+ from structlog_throttling.processors import LogTimeThrottler
66
+
67
+
68
+ structlog.configure(
69
+ processors=[
70
+ structlog.processors.add_log_level,
71
+ LogTimeThrottler("level", every_seconds=5),
72
+ ...
73
+ ],
74
+ )
75
+ ```
@@ -0,0 +1,53 @@
1
+ # *structlog-throttling*: Throttling for *[structlog](https://www.structlog.org/)* loggers
2
+
3
+ <a href="https://pypi.org/project/structlog-throttling/"><img src="https://img.shields.io/pypi/pyversions/structlog-throttling.svg" alt="Supported Python versions from PyPI." /></a>
4
+
5
+ Logging offers a trade-off between visibility and performance. A particularly high performance cost can be incurred when logging in each iteration of a loop, common [hot spots](https://en.wikipedia.org/wiki/Hot_spot_%28computer_programming%29) in most programs. A solution to this problem is to space out the log calls such that they only happen every some time instead of on every iteration of the loop. By tweaking the time in between log calls we can move within the visibility-performance trade-off.
6
+
7
+ *structlog-throttling* brings this solution to *[structlog](https://www.structlog.org/)* in the form of processors to throttle log calls based on time, or call count.
8
+
9
+ ## Getting started
10
+
11
+ ### Installation
12
+
13
+ Install *structlog-throttling* from PyPI:
14
+
15
+ ```sh
16
+ pip install structlog-throttling
17
+ ```
18
+
19
+ ### Configure
20
+
21
+ When configuring *structlog*, use one of the processors offered by *structlog-throttling*. I recommend putting the processor close to the beginning of your processor chain, to avoid processing logs that will ultimately be dropped:
22
+
23
+ ```python
24
+ import structlog
25
+ from structlog_throttling.processors import LogTimeThrottler
26
+
27
+
28
+ structlog.configure(
29
+ processors=[
30
+ # Logs with the same 'event' will only be allowed through every 5 seconds.
31
+ LogTimeThrottler("event", every_seconds=5),
32
+ ...
33
+ ]
34
+ )
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ Throttle logs based on log level every 5 seconds:
40
+
41
+ ```python
42
+ import structlog
43
+ from structlog_throttling.processors import LogTimeThrottler
44
+
45
+
46
+ structlog.configure(
47
+ processors=[
48
+ structlog.processors.add_log_level,
49
+ LogTimeThrottler("level", every_seconds=5),
50
+ ...
51
+ ],
52
+ )
53
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.9.8,<0.10.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "structlog-throttling"
7
+ version = "1.1.0"
8
+ description = "Log throttling utilities for structlog."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT OR Apache-2.0"
12
+ keywords = ["structlog", "throttling", "throttle"]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: System :: Logging",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "structlog>=25.5.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Codeberg = "https://codeberg.org/tomasfarias/structlog-throttling"
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = "tests"
35
+
36
+ [tool.ruff]
37
+ src = ["src", "tests"]
38
+ line-length = 88
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8.4.2",
43
+ ]
@@ -0,0 +1,3 @@
1
+ __title__ = "structlog-throttling"
2
+ __author__ = "Tomás Farías Santana"
3
+ __copyright__ = "Copyright (c) 2025 Tomás Farías Santana"
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from structlog import DropEvent
6
+ from structlog.typing import EventDict
7
+
8
+ from .throttlers import CountThrottler, ThrottlerProtocol, TimeThrottler
9
+
10
+ __all__ = [
11
+ "LogThrottler",
12
+ "LogTimeThrottler",
13
+ "CountThrottler",
14
+ ]
15
+
16
+
17
+ class LogThrottler:
18
+ """Drop logs when throttled based on *throttler*.
19
+
20
+ This should generally be close to the top of your processor chain so that a log that
21
+ will ultimately be throttled is not processed further.
22
+
23
+ Args:
24
+ key: Unique key in the *event_dict* to determine if log should be throttled.
25
+ throttler:
26
+ A ``ThrottlerProtocol`` implementation to decide if *key should be
27
+ throttled.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ key: str,
33
+ throttler: ThrottlerProtocol,
34
+ ):
35
+ self.key = key
36
+ self.throttler = throttler
37
+
38
+ def __call__(self, _: logging.Logger, __: str, event_dict: EventDict) -> EventDict:
39
+ if self.throttler.is_throttled(event_dict[self.key]):
40
+ raise DropEvent
41
+ return event_dict
42
+
43
+
44
+ class LogTimeThrottler:
45
+ """Drop logs when throttled based on time in between calls.
46
+
47
+ This is a convinience class to initialize a ``LogThrottler`` with a
48
+ ``TimeThrottler``.
49
+
50
+ Args:
51
+ key: Unique key in the *event_dict* to determine if log should be throttled.
52
+ every_seconds: How long to throttle logs for, in seconds.
53
+ """
54
+
55
+ def __init__(self, key: str, every_seconds: int | float) -> None:
56
+ self.key = key
57
+ self.throttler = TimeThrottler(every_seconds)
58
+
59
+ def __call__(self, _: logging.Logger, __: str, event_dict: EventDict) -> EventDict:
60
+ if self.throttler.is_throttled(event_dict[self.key]):
61
+ raise DropEvent
62
+ return event_dict
63
+
64
+
65
+ class LogCountThrottler:
66
+ """Drop logs when throttled based on the number of times *key* was in a log call.
67
+
68
+ This is a convinience class to initialize a ``LogThrottler`` with a
69
+ ``CountThrottler``.
70
+
71
+ Args:
72
+ key: Unique key in the *event_dict* to determine if log should be throttled.
73
+ every: How frequently to throttle logs.
74
+ """
75
+
76
+ def __init__(self, key: str, every: int) -> None:
77
+ self.key = key
78
+ self.throttler = CountThrottler(every)
79
+
80
+ def __call__(self, _: logging.Logger, __: str, event_dict: EventDict) -> EventDict:
81
+ if self.throttler.is_throttled(event_dict[self.key]):
82
+ raise DropEvent
83
+ return event_dict
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import typing
5
+ import weakref
6
+
7
+
8
+ class ThrottlerProtocol(typing.Protocol):
9
+ def is_throttled(self, key: str) -> bool: ...
10
+
11
+
12
+ class _Hashable(typing.Protocol):
13
+ def __hash__(self) -> int: ...
14
+
15
+
16
+ class _Link:
17
+ """A link in a doubly-linked list"""
18
+
19
+ __slots__ = "at", "previous", "next", "__weakref__"
20
+ previous: "_Link" | None
21
+ next: "_Link" | None
22
+ at: float | None
23
+
24
+
25
+ class TimeThrottler:
26
+ """A throttler for time-based throttling.
27
+
28
+ The intuition is that if we determine that a particular key is no longer throttled,
29
+ then it follows that any key that came before it (i.e. any key that was throttled
30
+ with an earlier timestamp) is also no longer throttled. Thus it is possible to save
31
+ memory by clearing multiple keys simultaneously.
32
+
33
+ This is achieved by maintaining two data structures:
34
+ * A doubly-linked list of ``_Link``.
35
+ * A ``weakref.WeakValueDictionary`` mapping keys to links in the list.
36
+
37
+ In each ``_Link`` of the doubly-linked list we store:
38
+ * The timestamp observed when a ``key`` is throttled.
39
+ * A weak reference to the ``next`` ``_Link``, which will contain the registered
40
+ timestamp for the next ``key`` that was throttled.
41
+ * A strong reference to the ``previous`` ``_Link``, which will contain the
42
+ registered timestamp for the ``key`` that was just previously throttled.
43
+
44
+ The idea is that there is only one strong reference to each link: In the link that
45
+ comes next in the list. This allows preserving memory by dropping multiple keys from
46
+ the list simultaneously by dropping a single link in the list. By dropping a single
47
+ link, we will drop the only strong reference to the previous link, thus it will also
48
+ be garbage collected, and this will subsequently drop the only reference to the
49
+ previous link of the previous link, which will also be garbage collected, and so on.
50
+
51
+ This design was inspired by ``collections.OrderedDict``.
52
+ """
53
+
54
+ def __init__(self, every_seconds: int | float) -> None:
55
+ self.every = every_seconds
56
+
57
+ self._last: _Link | None = None
58
+ self._indexes = weakref.WeakValueDictionary()
59
+
60
+ def is_throttled(self, key: _Hashable) -> bool:
61
+ """Determine whether *key* is throttled.
62
+
63
+ Examples:
64
+ >>> tt = TimeThrottler(every_seconds=1)
65
+ >>> tt.is_throttled("event")
66
+ False
67
+ >>> tt.is_throttled("event")
68
+ True
69
+ >>> tt.is_throttled("another-event")
70
+ False
71
+ >>> tt.is_throttled("another-event")
72
+ True
73
+ >>> time.sleep(1)
74
+ >>> tt.is_throttled("event")
75
+ False
76
+ >>> tt.is_throttled("another-event")
77
+ False
78
+ """
79
+ now = time.monotonic()
80
+
81
+ if key not in self._indexes:
82
+ new = _Link()
83
+ new.at = now
84
+ # Stores a weak reference
85
+ self._indexes[key] = new
86
+
87
+ if self._last:
88
+ # 'next' is a weak reference
89
+ self._last.next = self._indexes[key]
90
+ # 'previous' is not
91
+ new.previous = self._last
92
+
93
+ self._last = new
94
+
95
+ return False
96
+
97
+ link = self._indexes[key]
98
+ if (now - link.at) >= self.every:
99
+ if self._last == link:
100
+ # Unset '_last' otherwise we will still be holding a strong reference to
101
+ # 'link' and it won't be GC'd.
102
+ self._last = None
103
+
104
+ del link
105
+
106
+ return False
107
+
108
+ return True
109
+
110
+
111
+ class CountThrottler:
112
+ """A throttler based on the count of times a key was seen."""
113
+
114
+ def __init__(self, every: int) -> None:
115
+ self.every = every
116
+
117
+ self._counts = {}
118
+
119
+ def is_throttled(self, key: _Hashable) -> bool:
120
+ """Determine whether *key* is throttled.
121
+
122
+ Examples:
123
+ >>> ct = CountThrottler(every=2)
124
+ >>> ct.is_throttled("event")
125
+ False
126
+ >>> ct.is_throttled("event")
127
+ True
128
+ >>> ct.is_throttled("event")
129
+ False
130
+ """
131
+ if key not in self._counts:
132
+ self._counts[key] = self.every
133
+
134
+ current = self._counts[key]
135
+ if current % self.every == 0:
136
+ should_throttle = False
137
+ else:
138
+ should_throttle = True
139
+
140
+ if current - 1 == 0:
141
+ del self._counts[key]
142
+ else:
143
+ self._counts[key] = current - 1
144
+
145
+ return should_throttle