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.
- structlog_throttling-1.1.0/PKG-INFO +75 -0
- structlog_throttling-1.1.0/README.md +53 -0
- structlog_throttling-1.1.0/pyproject.toml +43 -0
- structlog_throttling-1.1.0/src/structlog_throttling/__init__.py +3 -0
- structlog_throttling-1.1.0/src/structlog_throttling/processors.py +83 -0
- structlog_throttling-1.1.0/src/structlog_throttling/throttlers.py +145 -0
|
@@ -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,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
|