Skip to content

Commit adc6b40

Browse files
authored
Merge pull request #150 from heitorlessa/feat/logger-add-xray-trace-id
feat: add xray_trace_id key when tracing is active #137
2 parents 16002f6 + 20e7f33 commit adc6b40

File tree

4 files changed

+85
-10
lines changed

4 files changed

+85
-10
lines changed

aws_lambda_powertools/logging/formatter.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
import os
34

45

56
class JsonFormatter(logging.Formatter):
@@ -30,13 +31,26 @@ def __init__(self, **kwargs):
3031
self.default_json_formatter = kwargs.pop("json_default", str)
3132
# Set the insertion order for the log messages
3233
self.format_dict = dict.fromkeys(kwargs.pop("log_record_order", ["level", "location", "message", "timestamp"]))
34+
self.reserved_keys = ["timestamp", "level", "location"]
3335
# Set the date format used by `asctime`
3436
super(JsonFormatter, self).__init__(datefmt=kwargs.pop("datefmt", None))
3537

36-
self.reserved_keys = ["timestamp", "level", "location"]
37-
self.format_dict.update(
38-
{"level": "%(levelname)s", "location": "%(funcName)s:%(lineno)d", "timestamp": "%(asctime)s", **kwargs}
39-
)
38+
self.format_dict.update(self._build_root_keys(**kwargs))
39+
40+
def _build_root_keys(self, **kwargs):
41+
return {
42+
"level": "%(levelname)s",
43+
"location": "%(funcName)s:%(lineno)d",
44+
"timestamp": "%(asctime)s",
45+
**kwargs,
46+
}
47+
48+
@staticmethod
49+
def _get_latest_trace_id():
50+
xray_trace_id = os.getenv("_X_AMZN_TRACE_ID")
51+
trace_id = xray_trace_id.split(";")[0].replace("Root=", "") if xray_trace_id else None
52+
53+
return trace_id
4054

4155
def update_formatter(self, **kwargs):
4256
self.format_dict.update(kwargs)
@@ -76,6 +90,9 @@ def format(self, record): # noqa: A003
7690
if record.exc_text:
7791
log_dict["exception"] = record.exc_text
7892

93+
# fetch latest X-Ray Trace ID, if any
94+
log_dict.update({"xray_trace_id": self._get_latest_trace_id()})
95+
7996
# Filter out top level key with values that are None
8097
log_dict = {k: v for k, v in log_dict.items() if v is not None}
8198

aws_lambda_powertools/logging/logger.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,14 @@ def handler(event, context):
235235

236236
@functools.wraps(lambda_handler)
237237
def decorate(event, context):
238+
lambda_context = build_lambda_context_model(context)
239+
cold_start = _is_cold_start()
240+
self.structure_logs(append=True, cold_start=cold_start, **lambda_context.__dict__)
241+
238242
if log_event:
239243
logger.debug("Event received")
240244
self.info(event)
241245

242-
lambda_context = build_lambda_context_model(context)
243-
cold_start = _is_cold_start()
244-
245-
self.structure_logs(append=True, cold_start=cold_start, **lambda_context.__dict__)
246246
return lambda_handler(event, context)
247247

248248
return decorate

docs/content/core/logger.mdx

+3-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Logger(service="payment", level="INFO")
4747

4848
## Standard structured keys
4949

50-
Your Logger will always include the following keys to your structured logging:
50+
Your Logger will include the following keys to your structured logging, by default:
5151

5252
Key | Type | Example | Description
5353
------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------
@@ -57,6 +57,7 @@ Key | Type | Example | Description
5757
**service** | str | "payment" | Service name defined. "service_undefined" will be used if unknown
5858
**sampling_rate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case
5959
**message** | any | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string
60+
**xray_trace_id** | str | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when Lambda function has enabled Tracing
6061

6162
## Capturing Lambda context info
6263

@@ -316,7 +317,7 @@ logger = Logger(stream=stdout, location="[%(funcName)s] %(module)s", datefmt="fa
316317
logger = Logger(stream=stdout, location=None) # highlight-line
317318
```
318319

319-
Alternatively, you can also change the order of the following log record keys via the `log_record_order` parameter: `level`, location`, message`, and timestamp`
320+
Alternatively, you can also change the order of the following log record keys via the `log_record_order` parameter: `level`, `location`, `message`, and `timestamp`
320321

321322
```python
322323
from aws_lambda_powertools import Logger

tests/functional/test_aws_lambda_logging.py

+57
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,60 @@ def test_log_dict_key_strip_nones(stdout):
170170

171171
# THEN the keys should only include `level`, `message`, `service`, `sampling_rate`
172172
assert sorted(log_dict.keys()) == ["level", "message", "sampling_rate", "service"]
173+
174+
175+
def test_log_dict_xray_is_present_when_tracing_is_enabled(stdout, monkeypatch):
176+
# GIVEN a logger is initialized within a Lambda function with X-Ray enabled
177+
trace_id = "1-5759e988-bd862e3fe1be46a994272793"
178+
trace_header = f"Root={trace_id};Parent=53995c3f42cd8ad8;Sampled=1"
179+
monkeypatch.setenv(name="_X_AMZN_TRACE_ID", value=trace_header)
180+
logger = Logger(stream=stdout)
181+
182+
# WHEN logging a message
183+
logger.info("foo")
184+
185+
log_dict: dict = json.loads(stdout.getvalue())
186+
187+
# THEN `xray_trace_id`` key should be present
188+
assert log_dict["xray_trace_id"] == trace_id
189+
190+
monkeypatch.delenv(name="_X_AMZN_TRACE_ID")
191+
192+
193+
def test_log_dict_xray_is_not_present_when_tracing_is_disabled(stdout, monkeypatch):
194+
# GIVEN a logger is initialized within a Lambda function with X-Ray disabled (default)
195+
logger = Logger(stream=stdout)
196+
197+
# WHEN logging a message
198+
logger.info("foo")
199+
200+
log_dict: dict = json.loads(stdout.getvalue())
201+
202+
# THEN `xray_trace_id`` key should not be present
203+
assert "xray_trace_id" not in log_dict
204+
205+
206+
def test_log_dict_xray_is_updated_when_tracing_id_changes(stdout, monkeypatch):
207+
# GIVEN a logger is initialized within a Lambda function with X-Ray enabled
208+
trace_id = "1-5759e988-bd862e3fe1be46a994272793"
209+
trace_header = f"Root={trace_id};Parent=53995c3f42cd8ad8;Sampled=1"
210+
monkeypatch.setenv(name="_X_AMZN_TRACE_ID", value=trace_header)
211+
logger = Logger(stream=stdout)
212+
213+
# WHEN logging a message
214+
logger.info("foo")
215+
216+
# and Trace ID changes to mimick a new invocation
217+
trace_id_2 = "1-5759e988-bd862e3fe1be46a949393982437"
218+
trace_header_2 = f"Root={trace_id_2};Parent=53995c3f42cd8ad8;Sampled=1"
219+
monkeypatch.setenv(name="_X_AMZN_TRACE_ID", value=trace_header_2)
220+
221+
logger.info("foo bar")
222+
223+
log_dict, log_dict_2 = [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line]
224+
225+
# THEN `xray_trace_id`` key should be different in both invocations
226+
assert log_dict["xray_trace_id"] == trace_id
227+
assert log_dict_2["xray_trace_id"] == trace_id_2
228+
229+
monkeypatch.delenv(name="_X_AMZN_TRACE_ID")

0 commit comments

Comments
 (0)