Skip to content

Commit 76b4d8f

Browse files
feat(logger): add logger buffer feature (#6060)
* First commit - Adding Buffer and Config classes * First commit - Adding Buffer and Config classes * First commit - Adding Buffer and Config classes * First commit - Adding nox tests * Adding new parameter to the constructor * Starting tests * Adding initial logic + test * Adding initial logic + test * Adding initial logic + test * Unit tests for log comparasion * Fixing logic to handle log flush * Start flushing logs * Adding more logic * Adding more logic * Adding more logic * Refactoring buffer class * Refactoring buffer class * Refactoring buffer class * More refactor * Adding more coverage + documentation * Adding decorator * Refactoring variables name * Adding more tests + propagating buffer configuration * Adding exception fields * Adding e2e tests * sys.getframe is more performatic * Chaging the max size * Removing unecessary files * Fix tests * Flushing logs when log line is bigger than buffer size * Flushing logs when log line is bigger than buffer size * Adding documentation * Adding documentation * Adding documentation * Addressing Andrea's feedback * Addressing Andrea's feedback * Addressing Andrea's feedback * Refactoring parameters name + child logger + tests * Improve documentation * Improve docstring
1 parent 4b84e93 commit 76b4d8f

24 files changed

+1898
-26
lines changed

Diff for: aws_lambda_powertools/logging/buffer/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from aws_lambda_powertools.logging.buffer.config import LoggerBufferConfig
2+
3+
__all__ = ["LoggerBufferConfig"]

Diff for: aws_lambda_powertools/logging/buffer/cache.py

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from __future__ import annotations
2+
3+
from collections import deque
4+
from typing import Any
5+
6+
7+
class KeyBufferCache:
8+
"""
9+
A cache implementation for a single key with size tracking and eviction support.
10+
11+
This class manages a buffer for a specific key, keeping track of the current size
12+
and providing methods to add, remove, and manage cached items. It supports automatic
13+
eviction tracking and size management.
14+
15+
Attributes
16+
----------
17+
cache : deque
18+
A double-ended queue storing the cached items.
19+
current_size : int
20+
The total size of all items currently in the cache.
21+
has_evicted : bool
22+
A flag indicating whether any items have been evicted from the cache.
23+
"""
24+
25+
def __init__(self):
26+
"""
27+
Initialize a buffer cache for a specific key.
28+
"""
29+
self.cache: deque = deque()
30+
self.current_size: int = 0
31+
self.has_evicted: bool = False
32+
33+
def add(self, item: Any) -> None:
34+
"""
35+
Add an item to the cache.
36+
37+
Parameters
38+
----------
39+
item : Any
40+
The item to be stored in the cache.
41+
"""
42+
item_size = len(str(item))
43+
self.cache.append(item)
44+
self.current_size += item_size
45+
46+
def remove_oldest(self) -> Any:
47+
"""
48+
Remove and return the oldest item from the cache.
49+
50+
Returns
51+
-------
52+
Any
53+
The removed item.
54+
"""
55+
removed_item = self.cache.popleft()
56+
self.current_size -= len(str(removed_item))
57+
self.has_evicted = True
58+
return removed_item
59+
60+
def get(self) -> list:
61+
"""
62+
Retrieve items for this key.
63+
64+
Returns
65+
-------
66+
list
67+
List of items in the cache.
68+
"""
69+
return list(self.cache)
70+
71+
def clear(self) -> None:
72+
"""
73+
Clear the cache for this key.
74+
"""
75+
self.cache.clear()
76+
self.current_size = 0
77+
self.has_evicted = False
78+
79+
80+
class LoggerBufferCache:
81+
"""
82+
A multi-key buffer cache with size-based eviction and management.
83+
84+
This class provides a flexible caching mechanism that manages multiple keys,
85+
with each key having its own buffer cache. The total size of each key's cache
86+
is limited, and older items are automatically evicted when the size limit is reached.
87+
88+
Key Features:
89+
- Multiple key support
90+
- Size-based eviction
91+
- Tracking of evicted items
92+
- Configurable maximum buffer size
93+
94+
Example
95+
--------
96+
>>> buffer_cache = LoggerBufferCache(max_size_bytes=1000)
97+
>>> buffer_cache.add("logs", "First log message")
98+
>>> buffer_cache.add("debug", "Debug information")
99+
>>> buffer_cache.get("logs")
100+
['First log message']
101+
>>> buffer_cache.get_current_size("logs")
102+
16
103+
"""
104+
105+
def __init__(self, max_size_bytes: int):
106+
"""
107+
Initialize the LoggerBufferCache.
108+
109+
Parameters
110+
----------
111+
max_size_bytes : int
112+
Maximum size of the cache in bytes for each key.
113+
"""
114+
self.max_size_bytes: int = max_size_bytes
115+
self.cache: dict[str, KeyBufferCache] = {}
116+
117+
def add(self, key: str, item: Any) -> None:
118+
"""
119+
Add an item to the cache for a specific key.
120+
121+
Parameters
122+
----------
123+
key : str
124+
The key to store the item under.
125+
item : Any
126+
The item to be stored in the cache.
127+
128+
Returns
129+
-------
130+
bool
131+
True if item was added, False otherwise.
132+
"""
133+
# Check if item is larger than entire buffer
134+
item_size = len(str(item))
135+
if item_size > self.max_size_bytes:
136+
raise BufferError("Cannot add item to the buffer")
137+
138+
# Create the key's cache if it doesn't exist
139+
if key not in self.cache:
140+
self.cache[key] = KeyBufferCache()
141+
142+
# Calculate the size after adding the new item
143+
new_total_size = self.cache[key].current_size + item_size
144+
145+
# If adding the item would exceed max size, remove oldest items
146+
while new_total_size > self.max_size_bytes and self.cache[key].cache:
147+
self.cache[key].remove_oldest()
148+
new_total_size = self.cache[key].current_size + item_size
149+
150+
self.cache[key].add(item)
151+
152+
def get(self, key: str) -> list:
153+
"""
154+
Retrieve items for a specific key.
155+
156+
Parameters
157+
----------
158+
key : str
159+
The key to retrieve items for.
160+
161+
Returns
162+
-------
163+
list
164+
List of items for the given key, or an empty list if the key doesn't exist.
165+
"""
166+
return [] if key not in self.cache else self.cache[key].get()
167+
168+
def clear(self, key: str | None = None) -> None:
169+
"""
170+
Clear the cache, either for a specific key or entirely.
171+
172+
Parameters
173+
----------
174+
key : Optional[str], optional
175+
The key to clear. If None, clears the entire cache.
176+
"""
177+
if key:
178+
if key in self.cache:
179+
self.cache[key].clear()
180+
del self.cache[key]
181+
else:
182+
self.cache.clear()
183+
184+
def has_items_evicted(self, key: str) -> bool:
185+
"""
186+
Check if a specific key's cache has evicted items.
187+
188+
Parameters
189+
----------
190+
key : str
191+
The key to check for evicted items.
192+
193+
Returns
194+
-------
195+
bool
196+
True if items have been evicted, False otherwise.
197+
"""
198+
return False if key not in self.cache else self.cache[key].has_evicted
199+
200+
def get_current_size(self, key: str) -> int | None:
201+
"""
202+
Get the current size of the buffer for a specific key.
203+
204+
Parameters
205+
----------
206+
key : str
207+
The key to get the current size for.
208+
209+
Returns
210+
-------
211+
int
212+
The current size of the buffer for the key.
213+
Returns 0 if the key does not exist.
214+
"""
215+
return None if key not in self.cache else self.cache[key].current_size

Diff for: aws_lambda_powertools/logging/buffer/config.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
6+
class LoggerBufferConfig:
7+
"""
8+
Configuration for log buffering behavior.
9+
"""
10+
11+
# Define class-level constant for valid log levels
12+
VALID_LOG_LEVELS: list[str] = ["DEBUG", "INFO", "WARNING"]
13+
LOG_LEVEL_BUFFER_VALUES = Literal["DEBUG", "INFO", "WARNING"]
14+
15+
def __init__(
16+
self,
17+
max_bytes: int = 20480,
18+
buffer_at_verbosity: LOG_LEVEL_BUFFER_VALUES = "DEBUG",
19+
flush_on_error_log: bool = True,
20+
):
21+
"""
22+
Initialize logger buffer configuration.
23+
24+
Parameters
25+
----------
26+
max_bytes : int, optional
27+
Maximum size of the buffer in bytes
28+
buffer_at_verbosity : str, optional
29+
Minimum log level to buffer
30+
flush_on_error_log : bool, optional
31+
Whether to flush the buffer when an error occurs
32+
"""
33+
self._validate_inputs(max_bytes, buffer_at_verbosity, flush_on_error_log)
34+
35+
self._max_bytes = max_bytes
36+
self._buffer_at_verbosity = buffer_at_verbosity.upper()
37+
self._flush_on_error_log = flush_on_error_log
38+
39+
def _validate_inputs(
40+
self,
41+
max_bytes: int,
42+
buffer_at_verbosity: str,
43+
flush_on_error_log: bool,
44+
) -> None:
45+
"""
46+
Validate configuration inputs.
47+
48+
Parameters
49+
----------
50+
Same as __init__ method parameters
51+
"""
52+
if not isinstance(max_bytes, int) or max_bytes <= 0:
53+
raise ValueError("Max size must be a positive integer")
54+
55+
if not isinstance(buffer_at_verbosity, str):
56+
raise ValueError("Log level must be a string")
57+
58+
# Validate log level
59+
if buffer_at_verbosity.upper() not in self.VALID_LOG_LEVELS:
60+
raise ValueError(f"Invalid log level. Must be one of {self.VALID_LOG_LEVELS}")
61+
62+
if not isinstance(flush_on_error_log, bool):
63+
raise ValueError("flush_on_error must be a boolean")
64+
65+
@property
66+
def max_bytes(self) -> int:
67+
"""Maximum buffer size in bytes."""
68+
return self._max_bytes
69+
70+
@property
71+
def buffer_at_verbosity(self) -> str:
72+
"""Minimum log level to buffer."""
73+
return self._buffer_at_verbosity
74+
75+
@property
76+
def flush_on_error_log(self) -> bool:
77+
"""Flag to flush buffer on error."""
78+
return self._flush_on_error_log

0 commit comments

Comments
 (0)