Skip to content

Commit 8071f5d

Browse files
committed
add context manager to logger
1 parent d51f73a commit 8071f5d

File tree

3 files changed

+139
-10
lines changed

3 files changed

+139
-10
lines changed

aws_lambda_powertools/logging/logger.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,8 @@
77
import random
88
import sys
99
import warnings
10-
from typing import (
11-
IO,
12-
TYPE_CHECKING,
13-
Any,
14-
Callable,
15-
Iterable,
16-
Mapping,
17-
TypeVar,
18-
overload,
19-
)
10+
from contextlib import contextmanager
11+
from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterable, Mapping, TypeVar, overload
2012

2113
from aws_lambda_powertools.logging.constants import (
2214
LOGGER_ATTRIBUTE_PRECONFIGURED,
@@ -338,6 +330,31 @@ def _configure_sampling(self) -> None:
338330
),
339331
)
340332

333+
@contextmanager
334+
def append_context_keys(self, **keys: Any) -> Generator[Any, Any, Any]:
335+
"""
336+
Context manager to temporarily add logging keys.
337+
338+
Parameters:
339+
-----------
340+
**keys: Any
341+
Key-value pairs to include in the log context during the lifespan of the context manager.
342+
343+
Example:
344+
--------
345+
>>> logger = Logger(service="example_service")
346+
>>> with logger.append_context_keys(user_id="123", operation="process"):
347+
>>> logger.info("Log with context")
348+
>>> logger.info("Log without context")
349+
"""
350+
# Add keys to the context
351+
self.append_keys(**keys)
352+
try:
353+
yield
354+
finally:
355+
# Remove the keys after exiting the context
356+
self.remove_keys(keys.keys())
357+
341358
@overload
342359
def inject_lambda_context(
343360
self,

docs/core/logger.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ You can append your own keys to your existing Logger via `append_keys(**addition
186186

187187
This example will add `order_id` if its value is not empty, and in subsequent invocations where `order_id` might not be present it'll remove it from the Logger.
188188

189+
#### append_context_keys method
190+
191+
???+ warning
192+
`append_context_keys` is not thread-safe.
193+
194+
The append_context_keys method allows temporary modification of a Logger instance's context without creating a new logger. It's useful for adding context keys to specific workflows while maintaining the logger's overall state and simplicity.
195+
196+
* Add examples
197+
189198
#### ephemeral metadata
190199

191200
You can pass an arbitrary number of keyword arguments (kwargs) to all log level's methods, e.g. `logger.info, logger.warning`.

tests/functional/logger/required_dependencies/test_logger.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,3 +1114,106 @@ def test_logger_json_unicode(stdout, service_name):
11141114

11151115
assert log["message"] == non_ascii_chars
11161116
assert log[japanese_field] == japanese_string
1117+
1118+
1119+
def test_append_context_keys_adds_and_removes_keys(stdout, service_name):
1120+
# GIVEN a Logger is initialized
1121+
logger = Logger(service=service_name, stream=stdout)
1122+
test_keys = {"user_id": "123", "operation": "test"}
1123+
1124+
# WHEN context keys are added
1125+
with logger.append_context_keys(**test_keys):
1126+
logger.info("message with context keys")
1127+
logger.info("message without context keys")
1128+
1129+
# THEN context keys should only be present in the first log statement
1130+
with_context_log, without_context_log = capture_multiple_logging_statements_output(stdout)
1131+
1132+
assert test_keys.items() <= with_context_log.items()
1133+
assert (test_keys.items() <= without_context_log.items()) is False
1134+
1135+
1136+
def test_append_context_keys_handles_empty_dict(stdout, service_name):
1137+
# GIVEN a Logger is initialized
1138+
logger = Logger(service=service_name, stream=stdout)
1139+
1140+
# WHEN context is added with no keys
1141+
with logger.append_context_keys():
1142+
logger.info("message with empty context")
1143+
1144+
# THEN log should contain only default keys
1145+
log_output = capture_logging_output(stdout)
1146+
assert set(log_output.keys()) == {"service", "timestamp", "level", "message", "location"}
1147+
1148+
1149+
def test_append_context_keys_handles_exception(stdout, service_name):
1150+
# GIVEN a Logger is initialized
1151+
logger = Logger(service=service_name, stream=stdout)
1152+
test_keys = {"user_id": "123"}
1153+
1154+
# WHEN an exception occurs within the context
1155+
try:
1156+
with logger.append_context_keys(**test_keys):
1157+
logger.info("message before exception")
1158+
raise ValueError("Test exception")
1159+
except ValueError:
1160+
logger.info("message after exception")
1161+
1162+
# THEN context keys should only be present in the first log statement
1163+
before_exception, after_exception = capture_multiple_logging_statements_output(stdout)
1164+
1165+
assert test_keys.items() <= before_exception.items()
1166+
assert (test_keys.items() <= after_exception.items()) is False
1167+
1168+
1169+
def test_append_context_keys_nested_contexts(stdout, service_name):
1170+
# GIVEN a Logger is initialized
1171+
logger = Logger(service=service_name, stream=stdout)
1172+
1173+
# WHEN nested contexts are used
1174+
with logger.append_context_keys(level1="outer"):
1175+
logger.info("outer context message")
1176+
with logger.append_context_keys(level2="inner"):
1177+
logger.info("nested context message")
1178+
logger.info("back to outer context message")
1179+
logger.info("no context message")
1180+
1181+
# THEN logs should contain appropriate context keys
1182+
outer, nested, back_outer, no_context = capture_multiple_logging_statements_output(stdout)
1183+
1184+
assert outer["level1"] == "outer"
1185+
assert "level2" not in outer
1186+
1187+
assert nested["level1"] == "outer"
1188+
assert nested["level2"] == "inner"
1189+
1190+
assert back_outer["level1"] == "outer"
1191+
assert "level2" not in back_outer
1192+
1193+
assert "level1" not in no_context
1194+
assert "level2" not in no_context
1195+
1196+
1197+
def test_append_context_keys_with_formatter(stdout, service_name):
1198+
# GIVEN a Logger is initialized with a custom formatter
1199+
class CustomFormatter(BasePowertoolsFormatter):
1200+
def append_keys(self, **additional_keys):
1201+
pass
1202+
1203+
def clear_state(self) -> None:
1204+
pass
1205+
1206+
def remove_keys(self, keys: Iterable[str]) -> None:
1207+
pass
1208+
1209+
custom_formatter = CustomFormatter()
1210+
logger = Logger(service=service_name, stream=stdout, logger_formatter=custom_formatter)
1211+
test_keys = {"request_id": "id", "context": "value"}
1212+
1213+
# WHEN context keys are added
1214+
with logger.append_context_keys(**test_keys):
1215+
logger.info("message with context")
1216+
1217+
# THEN the context keys should not persist
1218+
current_keys = logger.get_current_keys()
1219+
assert current_keys == {}

0 commit comments

Comments
 (0)