Skip to content

Commit 40272b6

Browse files
feat(logger): add support for exception notes (#6465)
* Adding support for exception notes * Adding support for exception notes * Python... --------- Co-authored-by: Ana Falcão <[email protected]>
1 parent 7482311 commit 40272b6

File tree

7 files changed

+79
-7
lines changed

7 files changed

+79
-7
lines changed

aws_lambda_powertools/logging/formatter.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,10 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003
194194

195195
# exception and exception_name fields can be added as extra key
196196
# in any log level, we try to extract and use them first
197-
extracted_exception, extracted_exception_name = self._extract_log_exception(log_record=record)
197+
extracted_exception, extracted_exception_name, exception_notes = self._extract_log_exception(log_record=record)
198198
formatted_log["exception"] = formatted_log.get("exception", extracted_exception)
199199
formatted_log["exception_name"] = formatted_log.get("exception_name", extracted_exception_name)
200+
formatted_log["exception_notes"] = formatted_log.get("exception_notes", exception_notes)
200201
if self.serialize_stacktrace:
201202
# Generate the traceback from the traceback library
202203
formatted_log["stack_trace"] = self._serialize_stacktrace(log_record=record)
@@ -380,7 +381,7 @@ def _serialize_stacktrace(self, log_record: logging.LogRecord) -> LogStackTrace
380381

381382
return None
382383

383-
def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, str] | tuple[None, None]:
384+
def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, str, list] | tuple[None, None, None]:
384385
"""Format traceback information, if available
385386
386387
Parameters
@@ -393,10 +394,12 @@ def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, st
393394
log_record: tuple[str, str] | tuple[None, None]
394395
Log record with constant traceback info and exception name
395396
"""
397+
396398
if isinstance(log_record.exc_info, tuple) and hasattr(log_record.exc_info[0], "__name__"):
397-
return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__ # type: ignore
399+
exception_notes = getattr(log_record.exc_info[1], "__notes__", None)
400+
return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__, exception_notes # type: ignore
398401

399-
return None, None
402+
return None, None, None
400403

401404
def _extract_log_keys(self, log_record: logging.LogRecord) -> dict[str, Any]:
402405
"""Extract and parse custom and reserved log keys

docs/core/logger.md

+16
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,22 @@ By default, the Logger will automatically include the full stack trace in JSON f
476476
--8<-- "examples/logger/src/logging_stacktrace_output.json"
477477
```
478478

479+
#### Adding exception notes
480+
481+
You can add notes to exceptions, which `logger.exception` propagates via a new `exception_notes` key in the log line. This works only in [Python 3.11 and later](https://peps.python.org/pep-0678/){target="_blank" rel="nofollow"}.
482+
483+
=== "logging_exception_notes.py"
484+
485+
```python hl_lines="15"
486+
--8<-- "examples/logger/src/logging_exception_notes.py"
487+
```
488+
489+
=== "logging_exception_notes_output.json"
490+
491+
```json hl_lines="9-11"
492+
--8<-- "examples/logger/src/logging_exception_notes_output.json"
493+
```
494+
479495
### Date formatting
480496

481497
Logger uses Python's standard logging date format with the addition of timezone: `2021-05-03 11:47:12,494+0000`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import requests
2+
3+
from aws_lambda_powertools import Logger
4+
from aws_lambda_powertools.utilities.typing import LambdaContext
5+
6+
ENDPOINT = "https://httpbin.org/status/500"
7+
logger = Logger(serialize_stacktrace=False)
8+
9+
10+
def lambda_handler(event: dict, context: LambdaContext) -> str:
11+
try:
12+
ret = requests.get(ENDPOINT)
13+
ret.raise_for_status()
14+
except requests.HTTPError as e:
15+
e.add_note("Can't connect to the endpoint") # type: ignore[attr-defined]
16+
logger.exception(e)
17+
raise RuntimeError("Unable to fullfil request") from e
18+
19+
return "hello world"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"level": "ERROR",
3+
"location": "collect.handler:15",
4+
"message": "Received a HTTP 5xx error",
5+
"timestamp": "2021-05-03 11:47:12,494+0000",
6+
"service": "payment",
7+
"exception_name": "RuntimeError",
8+
"exception": "Traceback (most recent call last):\n File \"<input>\", line 2, in <module> RuntimeError: Unable to fullfil request",
9+
"exception_notes":[
10+
"Can't connect to the endpoint"
11+
]
12+
}

examples/logger/src/logging_exceptions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from aws_lambda_powertools import Logger
44
from aws_lambda_powertools.utilities.typing import LambdaContext
55

6-
ENDPOINT = "http://httpbin.org/status/500"
7-
logger = Logger()
6+
ENDPOINT = "https://httpbin.org/status/500"
7+
logger = Logger(serialize_stacktrace=False)
88

99

1010
def lambda_handler(event: dict, context: LambdaContext) -> str:

examples/logger/src/logging_stacktrace.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from aws_lambda_powertools import Logger
44
from aws_lambda_powertools.utilities.typing import LambdaContext
55

6-
ENDPOINT = "http://httpbin.org/status/500"
6+
ENDPOINT = "https://httpbin.org/status/500"
77
logger = Logger(serialize_stacktrace=True)
88

99

tests/functional/logger/required_dependencies/test_logger.py

+22
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,28 @@ def test_logger_exception_extract_exception_name(stdout, service_name):
638638
assert "ValueError" == log["exception_name"]
639639

640640

641+
@pytest.mark.skipif(sys.version_info < (3, 11), reason="This only works in Python 3.11+")
642+
def test_logger_exception_extract_exception_notes(stdout, service_name):
643+
# GIVEN Logger is initialized
644+
logger = Logger(service=service_name, stream=stdout)
645+
646+
# WHEN calling a logger.exception with a ValueError and notes
647+
try:
648+
raise ValueError("something went wrong")
649+
except Exception as error:
650+
error.add_note("something went wrong")
651+
error.add_note("something went wrong again")
652+
logger.exception("Received an exception")
653+
654+
# THEN we expect a "exception_name" to be "ValueError"
655+
# THEN we except to have exception_notes in the exception
656+
log = capture_logging_output(stdout)
657+
assert len(log["exception_notes"]) == 2
658+
assert log["exception_notes"][0] == "something went wrong"
659+
assert log["exception_notes"][1] == "something went wrong again"
660+
assert "ValueError" == log["exception_name"]
661+
662+
641663
def test_logger_exception_should_not_fail_with_exception_block(stdout, service_name):
642664
# GIVEN Logger is initialized
643665
logger = Logger(service=service_name, stream=stdout)

0 commit comments

Comments
 (0)