Skip to content

Commit 48a64eb

Browse files
Michael Brewerheitorlessa
Michael Brewer
andauthored
feat(logging): Add correlation_id support (#321)
* feat(logging): inject correlation_id Prototype idea for correlation_id support in logger * chore: Bump CI * feat(logger): Add set_correlation_id * chore: Remove _get_correlation_id_path * feat(logging): Add correlation_id_path option * feat(logger): Add correlation_paths * feat(logger): Add more builtin correlation paths * chore: Update docs/core/logger.md Co-authored-by: Heitor Lessa <heitor.lessa@hotmail.com> Co-authored-by: Heitor Lessa <heitor.lessa@hotmail.com>
1 parent 7ec0648 commit 48a64eb

File tree

4 files changed

+107
-2
lines changed

4 files changed

+107
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Built-in correlation paths"""
2+
3+
API_GATEWAY_REST = "requestContext.requestId"
4+
API_GATEWAY_HTTP = API_GATEWAY_REST
5+
APPLICATION_LOAD_BALANCER = "headers.x-amzn-trace-id"
6+
EVENT_BRIDGE = "id"

aws_lambda_powertools/logging/logger.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys
77
from typing import Any, Callable, Dict, Union
88

9+
import jmespath
10+
911
from ..shared import constants
1012
from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice
1113
from .exceptions import InvalidLoggerSamplingRateError
@@ -204,7 +206,9 @@ def _configure_sampling(self):
204206
f"Please review POWERTOOLS_LOGGER_SAMPLE_RATE environment variable."
205207
)
206208

207-
def inject_lambda_context(self, lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = None):
209+
def inject_lambda_context(
210+
self, lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = None, correlation_id_path: str = None
211+
):
208212
"""Decorator to capture Lambda contextual info and inject into logger
209213
210214
Parameters
@@ -213,6 +217,8 @@ def inject_lambda_context(self, lambda_handler: Callable[[Dict, Any], Any] = Non
213217
Method to inject the lambda context
214218
log_event : bool, optional
215219
Instructs logger to log Lambda Event, by default False
220+
correlation_id_path: str, optional
221+
Optional JMESPath for the correlation_id
216222
217223
Environment variables
218224
---------------------
@@ -251,7 +257,9 @@ def handler(event, context):
251257
# Return a partial function with args filled
252258
if lambda_handler is None:
253259
logger.debug("Decorator called with parameters")
254-
return functools.partial(self.inject_lambda_context, log_event=log_event)
260+
return functools.partial(
261+
self.inject_lambda_context, log_event=log_event, correlation_id_path=correlation_id_path
262+
)
255263

256264
log_event = resolve_truthy_env_var_choice(
257265
choice=log_event, env=os.getenv(constants.LOGGER_LOG_EVENT_ENV, "false")
@@ -263,6 +271,9 @@ def decorate(event, context):
263271
cold_start = _is_cold_start()
264272
self.structure_logs(append=True, cold_start=cold_start, **lambda_context.__dict__)
265273

274+
if correlation_id_path:
275+
self.set_correlation_id(jmespath.search(correlation_id_path, event))
276+
266277
if log_event:
267278
logger.debug("Event received")
268279
self.info(event)
@@ -296,6 +307,16 @@ def structure_logs(self, append: bool = False, **kwargs):
296307
# Set a new formatter for a logger handler
297308
handler.setFormatter(JsonFormatter(**self._default_log_keys, **kwargs))
298309

310+
def set_correlation_id(self, value: str):
311+
"""Sets the correlation_id in the logging json
312+
313+
Parameters
314+
----------
315+
value : str
316+
Value for the correlation id
317+
"""
318+
self.structure_logs(append=True, correlation_id=value)
319+
299320
@staticmethod
300321
def _get_log_level(level: Union[str, int]) -> Union[str, int]:
301322
""" Returns preferred log level set by the customer in upper case """

docs/core/logger.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,47 @@ You can append your own keys to your existing Logger via `structure_logs(append=
188188

189189
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.
190190

191+
#### Setting correlation ID
192+
193+
You can set a correlation_id to your existing Logger via `set_correlation_id(value)` method.
194+
195+
=== "collect.py"
196+
197+
```python hl_lines="8"
198+
from aws_lambda_powertools import Logger
199+
from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
200+
201+
logger = Logger()
202+
203+
def handler(event, context):
204+
event = APIGatewayProxyEvent(event)
205+
logger.set_correlation_id(event.request_context.request_id)
206+
logger.info("Collecting payment")
207+
...
208+
```
209+
=== "Example Event"
210+
211+
```json hl_lines="3"
212+
{
213+
"requestContext": {
214+
"requestId": "correlation_id_value"
215+
}
216+
}
217+
```
218+
=== "Example CloudWatch Logs excerpt"
219+
220+
```json hl_lines="7"
221+
{
222+
"timestamp": "2020-05-24 18:17:33,774",
223+
"level": "INFO",
224+
"location": "collect.handler:1",
225+
"service": "payment",
226+
"sampling_rate": 0.0,
227+
"correlation_id": "correlation_id_value",
228+
"message": "Collecting payment"
229+
}
230+
```
231+
191232
#### extra parameter
192233

193234
Extra parameter is available for all log levels' methods, as implemented in the standard logging library - e.g. `logger.info, logger.warning`.

tests/functional/test_logger.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010

1111
from aws_lambda_powertools import Logger, Tracer
12+
from aws_lambda_powertools.logging import correlation_paths
1213
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError
1314
from aws_lambda_powertools.logging.logger import set_package_logger
1415
from aws_lambda_powertools.shared import constants
@@ -437,3 +438,39 @@ def test_logger_exception_extract_exception_name(stdout, service_name):
437438
# THEN we expect a "exception_name" to be "ValueError"
438439
log = capture_logging_output(stdout)
439440
assert "ValueError" == log["exception_name"]
441+
442+
443+
def test_logger_set_correlation_id(lambda_context, stdout, service_name):
444+
# GIVEN
445+
logger = Logger(service=service_name, stream=stdout)
446+
request_id = "xxx-111-222"
447+
mock_event = {"requestContext": {"requestId": request_id}}
448+
449+
def handler(event, _):
450+
logger.set_correlation_id(event["requestContext"]["requestId"])
451+
logger.info("Foo")
452+
453+
# WHEN
454+
handler(mock_event, lambda_context)
455+
456+
# THEN
457+
log = capture_logging_output(stdout)
458+
assert request_id == log["correlation_id"]
459+
460+
461+
def test_logger_set_correlation_id_path(lambda_context, stdout, service_name):
462+
# GIVEN
463+
logger = Logger(service=service_name, stream=stdout)
464+
request_id = "xxx-111-222"
465+
mock_event = {"requestContext": {"requestId": request_id}}
466+
467+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
468+
def handler(event, context):
469+
logger.info("Foo")
470+
471+
# WHEN
472+
handler(mock_event, lambda_context)
473+
474+
# THEN
475+
log = capture_logging_output(stdout)
476+
assert request_id == log["correlation_id"]

0 commit comments

Comments
 (0)