Skip to content

Commit e97d065

Browse files
Adding more logic
1 parent 58e26ae commit e97d065

File tree

6 files changed

+155
-7
lines changed

6 files changed

+155
-7
lines changed

aws_lambda_powertools/logging/buffer/cache.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def __init__(self, max_size_bytes: int):
2020
self.max_size_bytes: int = max_size_bytes
2121
self.cache: dict[str, deque] = {}
2222
self.current_size: dict[str, int] = {}
23+
self.has_evicted: bool = False
2324

2425
def add(self, key: str, item: Any) -> None:
2526
"""
@@ -53,6 +54,7 @@ def add(self, key: str, item: Any) -> None:
5354
while self.current_size[key] + item_size > self.max_size_bytes and self.cache[key]:
5455
removed_item = self.cache[key].popleft()
5556
self.current_size[key] -= len(str(removed_item))
57+
self.has_evicted = True
5658

5759
self.cache[key].append(item)
5860
self.current_size[key] += item_size

aws_lambda_powertools/logging/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
# logger.init attribute is set when Logger has been configured
44
LOGGER_ATTRIBUTE_PRECONFIGURED = "init"
55
LOGGER_ATTRIBUTE_HANDLER = "logger_handler"
6+
7+
LOGGER_BUFFER_FIRST_INVOKE = "FIRST_INVOKE"

aws_lambda_powertools/logging/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ class OrphanedChildLoggerError(Exception):
1212
"""
1313

1414
pass
15+
16+
17+
class InvalidBufferItem(Exception):
18+
"""
19+
Raised when a buffer item exceeds the maximum allowed buffer size.
20+
"""
21+
22+
pass

aws_lambda_powertools/logging/logger.py

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@
2222
LOGGER_ATTRIBUTE_HANDLER,
2323
LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER,
2424
LOGGER_ATTRIBUTE_PRECONFIGURED,
25+
LOGGER_BUFFER_FIRST_INVOKE,
26+
)
27+
from aws_lambda_powertools.logging.exceptions import (
28+
InvalidBufferItem,
29+
InvalidLoggerSamplingRateError,
30+
OrphanedChildLoggerError,
2531
)
26-
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError, OrphanedChildLoggerError
2732
from aws_lambda_powertools.logging.filters import SuppressFilter
2833
from aws_lambda_powertools.logging.formatter import (
2934
RESERVED_FORMATTER_CUSTOM_KEYS,
@@ -34,10 +39,12 @@
3439
from aws_lambda_powertools.shared import constants
3540
from aws_lambda_powertools.shared.functions import (
3641
extract_event_from_common_models,
42+
get_tracer_id,
3743
resolve_env_var_choice,
3844
resolve_truthy_env_var_choice,
3945
)
4046
from aws_lambda_powertools.utilities import jmespath_utils
47+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
4148

4249
if TYPE_CHECKING:
4350
from aws_lambda_powertools.logging.buffer.config import LoggerBufferConfig
@@ -518,14 +525,62 @@ def _add_log_record_to_buffer(
518525
level: int,
519526
msg: object,
520527
args: object,
521-
exc_info: logging._ExcInfoType,
522-
stack_info: bool,
523-
extra: Mapping[str, object],
528+
exc_info: logging._ExcInfoType = None,
529+
stack_info: bool = False,
530+
extra: Mapping[str, object] | None = None,
524531
):
525-
tracer_id = os.getenv(constants.XRAY_TRACE_ID_ENV, None)
526-
if tracer_id:
532+
"""
533+
Add log record to buffer with intelligent tracer ID handling.
534+
535+
Parameters
536+
----------
537+
level : int
538+
Logging level of the record.
539+
msg : object
540+
Log message to be recorded.
541+
args : object
542+
Additional arguments for the log message.
543+
exc_info : logging._ExcInfoType, optional
544+
Exception information for the log record.
545+
stack_info : bool, optional
546+
Whether to include stack information.
547+
extra : Mapping[str, object], optional
548+
Additional contextual information for the log record.
549+
550+
Raises
551+
------
552+
InvalidBufferItem
553+
If the log record cannot be added to the buffer.
554+
555+
Notes
556+
-----
557+
Handles special first invocation buffering and migration of log records
558+
between different tracer contexts.
559+
"""
560+
# Determine tracer ID, defaulting to first invoke marker
561+
tracer_id = get_tracer_id() or LOGGER_BUFFER_FIRST_INVOKE
562+
563+
try:
564+
# Create log record for buffering
527565
log_record: dict[str, Any] = _create_buffer_record(level=level, msg=msg, args=args, extra=extra)
566+
567+
# Migrate log records from first invoke to current tracer context
568+
if tracer_id != LOGGER_BUFFER_FIRST_INVOKE and self._buffer_cache.get(LOGGER_BUFFER_FIRST_INVOKE):
569+
# Retrieve first invoke log records
570+
first_invoke_items = self._buffer_cache.get(LOGGER_BUFFER_FIRST_INVOKE)
571+
572+
# Transfer log records to current tracer context
573+
for item in first_invoke_items:
574+
self._buffer_cache.add(tracer_id, item)
575+
576+
# Clear first invoke buffer
577+
self._buffer_cache.clear(LOGGER_BUFFER_FIRST_INVOKE)
578+
579+
# Add current log record to buffer
528580
self._buffer_cache.add(tracer_id, log_record)
581+
except InvalidBufferItem as exc:
582+
# Wrap and re-raise buffer addition error
583+
raise InvalidBufferItem("Cannot add item to the buffer") from exc
529584

530585
def flush_buffer(self):
531586
"""
@@ -535,17 +590,27 @@ def flush_buffer(self):
535590
-----
536591
Retrieves log records for current trace from buffer
537592
Immediately processes and logs each record
593+
Warning if some cache was evicted in that execution
538594
Clears buffer after complete processing
539595
540596
Raises
541597
------
542598
Any exceptions from underlying logging or buffer mechanisms
543599
will be propagated to caller
544600
"""
545-
tracer_id = os.getenv(constants.XRAY_TRACE_ID_ENV, None)
601+
tracer_id = get_tracer_id()
546602
for log_line in self._buffer_cache.get(tracer_id):
547603
self._create_and_flush_log_record(log_line)
548604

605+
if self._buffer_cache.has_evicted:
606+
warnings.warn(
607+
message="Some logs are not displayed because they were evicted from the buffer. "
608+
"Increase buffer size to store more logs in the buffer",
609+
category=PowertoolsUserWarning,
610+
stacklevel=2,
611+
)
612+
self._buffer_cache.has_evicted = False
613+
549614
self._buffer_cache.clear()
550615

551616
def debug(

aws_lambda_powertools/shared/functions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,8 @@ def abs_lambda_path(relative_path: str = "") -> str:
283283

284284
def sanitize_xray_segment_name(name: str) -> str:
285285
return re.sub(constants.INVALID_XRAY_NAME_CHARACTERS, "", name)
286+
287+
288+
def get_tracer_id() -> str | None:
289+
xray_trace_id = os.getenv(constants.XRAY_TRACE_ID_ENV)
290+
return xray_trace_id.split(";")[0].replace("Root=", "") if xray_trace_id else None

tests/functional/logger/required_dependencies/test_powertools_logger_buffer.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import json
55
import random
66
import string
7+
import warnings
78
from collections import namedtuple
89

910
import pytest
1011

1112
from aws_lambda_powertools import Logger
1213
from aws_lambda_powertools.logging.buffer import LoggerBufferConfig
1314
from aws_lambda_powertools.shared import constants
15+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
1416

1517

1618
@pytest.fixture
@@ -158,3 +160,67 @@ def test_create_and_flush_logs(stdout, service_name, monkeypatch):
158160
# THEN: We expect the log record is not buffered
159161
log = capture_multiple_logging_statements_output(stdout)
160162
assert "this log line will be flushed" == log[0]["message"]
163+
164+
165+
def test_create_buffer_with_item_overflow(stdout, service_name, monkeypatch):
166+
monkeypatch.setenv(constants.XRAY_TRACE_ID_ENV, "1234")
167+
168+
# GIVEN: A logger configured with 2 bytes
169+
logger_buffer_config = LoggerBufferConfig(max_size=2, minimum_log_level="DEBUG")
170+
171+
logger = Logger(level="DEBUG", service=service_name, stream=stdout, logger_buffer=logger_buffer_config)
172+
173+
# WHEN logging a line with a size higher than buffer
174+
# THEN must raise a warning
175+
with pytest.warns(PowertoolsUserWarning, match="Item size*"):
176+
logger.debug("this log line will be flushed")
177+
178+
179+
def test_create_buffer_with_items_evicted(stdout, service_name, monkeypatch):
180+
monkeypatch.setenv(constants.XRAY_TRACE_ID_ENV, "1234")
181+
182+
# GIVEN: A logger configured with 1024 bytes
183+
logger_buffer_config = LoggerBufferConfig(max_size=1024, minimum_log_level="DEBUG")
184+
185+
logger = Logger(level="DEBUG", service=service_name, stream=stdout, logger_buffer=logger_buffer_config)
186+
187+
# WHEN we add 3 lines that exceeds than 1024 bytes
188+
logger.debug("this log line will be flushed")
189+
logger.debug("this log line will be flushed")
190+
logger.debug("this log line will be flushed")
191+
logger.debug("this log line will be flushed")
192+
logger.debug("this log line will be flushed")
193+
194+
# THEN must raise a warning when trying to flush the lugs
195+
with pytest.warns(PowertoolsUserWarning, match="Some logs are not displayed because*"):
196+
logger.flush_buffer()
197+
198+
199+
def test_create_buffer_with_items_evicted_next_invocation(stdout, service_name, monkeypatch):
200+
monkeypatch.setenv(constants.XRAY_TRACE_ID_ENV, "1234")
201+
202+
# GIVEN: A logger configured with 1024 bytes
203+
logger_buffer_config = LoggerBufferConfig(max_size=1024, minimum_log_level="DEBUG")
204+
205+
logger = Logger(level="DEBUG", service=service_name, stream=stdout, logger_buffer=logger_buffer_config)
206+
207+
# WHEN Add multiple log entries that exceed buffer size
208+
message = "this log line will be flushed"
209+
logger.debug(message)
210+
logger.debug(message)
211+
logger.debug(message)
212+
logger.debug(message)
213+
logger.debug(message)
214+
215+
# THEN First buffer flush triggers warning about log eviction
216+
with pytest.warns(PowertoolsUserWarning, match="Some logs are not displayed because*"):
217+
logger.flush_buffer()
218+
219+
# WHEN Add another log entry
220+
logger.debug("new log entry after buffer flush")
221+
222+
# THEN Subsequent buffer flush should not trigger warning
223+
with warnings.catch_warnings(record=True) as warning_list:
224+
warnings.simplefilter("always")
225+
logger.flush_buffer()
226+
assert len(warning_list) == 0, "No warnings should be raised"

0 commit comments

Comments
 (0)