diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py
index a330b043f75..4dadcaf1ef7 100644
--- a/aws_lambda_powertools/logging/formatter.py
+++ b/aws_lambda_powertools/logging/formatter.py
@@ -194,9 +194,10 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003
# exception and exception_name fields can be added as extra key
# in any log level, we try to extract and use them first
- extracted_exception, extracted_exception_name = self._extract_log_exception(log_record=record)
+ extracted_exception, extracted_exception_name, exception_notes = self._extract_log_exception(log_record=record)
formatted_log["exception"] = formatted_log.get("exception", extracted_exception)
formatted_log["exception_name"] = formatted_log.get("exception_name", extracted_exception_name)
+ formatted_log["exception_notes"] = formatted_log.get("exception_notes", exception_notes)
if self.serialize_stacktrace:
# Generate the traceback from the traceback library
formatted_log["stack_trace"] = self._serialize_stacktrace(log_record=record)
@@ -380,7 +381,7 @@ def _serialize_stacktrace(self, log_record: logging.LogRecord) -> LogStackTrace
return None
- def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, str] | tuple[None, None]:
+ def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, str, list] | tuple[None, None, None]:
"""Format traceback information, if available
Parameters
@@ -393,10 +394,12 @@ def _extract_log_exception(self, log_record: logging.LogRecord) -> tuple[str, st
log_record: tuple[str, str] | tuple[None, None]
Log record with constant traceback info and exception name
"""
+
if isinstance(log_record.exc_info, tuple) and hasattr(log_record.exc_info[0], "__name__"):
- return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__ # type: ignore
+ exception_notes = getattr(log_record.exc_info[1], "__notes__", None)
+ return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__, exception_notes # type: ignore
- return None, None
+ return None, None, None
def _extract_log_keys(self, log_record: logging.LogRecord) -> dict[str, Any]:
"""Extract and parse custom and reserved log keys
diff --git a/docs/core/logger.md b/docs/core/logger.md
index caf3381853b..41c52b69db4 100644
--- a/docs/core/logger.md
+++ b/docs/core/logger.md
@@ -476,6 +476,22 @@ By default, the Logger will automatically include the full stack trace in JSON f
--8<-- "examples/logger/src/logging_stacktrace_output.json"
```
+#### Adding exception notes
+
+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"}.
+
+=== "logging_exception_notes.py"
+
+ ```python hl_lines="15"
+ --8<-- "examples/logger/src/logging_exception_notes.py"
+ ```
+
+=== "logging_exception_notes_output.json"
+
+ ```json hl_lines="9-11"
+ --8<-- "examples/logger/src/logging_exception_notes_output.json"
+ ```
+
### Date formatting
Logger uses Python's standard logging date format with the addition of timezone: `2021-05-03 11:47:12,494+0000`.
diff --git a/examples/logger/src/logging_exception_notes.py b/examples/logger/src/logging_exception_notes.py
new file mode 100644
index 00000000000..7c05427b6e6
--- /dev/null
+++ b/examples/logger/src/logging_exception_notes.py
@@ -0,0 +1,19 @@
+import requests
+
+from aws_lambda_powertools import Logger
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+ENDPOINT = "https://httpbin.org/status/500"
+logger = Logger(serialize_stacktrace=False)
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> str:
+ try:
+ ret = requests.get(ENDPOINT)
+ ret.raise_for_status()
+ except requests.HTTPError as e:
+ e.add_note("Can't connect to the endpoint") # type: ignore[attr-defined]
+ logger.exception(e)
+ raise RuntimeError("Unable to fullfil request") from e
+
+ return "hello world"
diff --git a/examples/logger/src/logging_exception_notes_output.json b/examples/logger/src/logging_exception_notes_output.json
new file mode 100644
index 00000000000..f50f12d689a
--- /dev/null
+++ b/examples/logger/src/logging_exception_notes_output.json
@@ -0,0 +1,12 @@
+{
+ "level": "ERROR",
+ "location": "collect.handler:15",
+ "message": "Received a HTTP 5xx error",
+ "timestamp": "2021-05-03 11:47:12,494+0000",
+ "service": "payment",
+ "exception_name": "RuntimeError",
+ "exception": "Traceback (most recent call last):\n File \"\", line 2, in RuntimeError: Unable to fullfil request",
+ "exception_notes":[
+ "Can't connect to the endpoint"
+ ]
+}
diff --git a/examples/logger/src/logging_exceptions.py b/examples/logger/src/logging_exceptions.py
index 05e5c1a1e15..20a45102992 100644
--- a/examples/logger/src/logging_exceptions.py
+++ b/examples/logger/src/logging_exceptions.py
@@ -3,8 +3,8 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
-ENDPOINT = "http://httpbin.org/status/500"
-logger = Logger()
+ENDPOINT = "https://httpbin.org/status/500"
+logger = Logger(serialize_stacktrace=False)
def lambda_handler(event: dict, context: LambdaContext) -> str:
diff --git a/examples/logger/src/logging_stacktrace.py b/examples/logger/src/logging_stacktrace.py
index 128836f5138..40e7e052be8 100644
--- a/examples/logger/src/logging_stacktrace.py
+++ b/examples/logger/src/logging_stacktrace.py
@@ -3,7 +3,7 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
-ENDPOINT = "http://httpbin.org/status/500"
+ENDPOINT = "https://httpbin.org/status/500"
logger = Logger(serialize_stacktrace=True)
diff --git a/tests/functional/logger/required_dependencies/test_logger.py b/tests/functional/logger/required_dependencies/test_logger.py
index a33c59521ed..fdbeef26c64 100644
--- a/tests/functional/logger/required_dependencies/test_logger.py
+++ b/tests/functional/logger/required_dependencies/test_logger.py
@@ -638,6 +638,28 @@ def test_logger_exception_extract_exception_name(stdout, service_name):
assert "ValueError" == log["exception_name"]
+@pytest.mark.skipif(sys.version_info < (3, 11), reason="This only works in Python 3.11+")
+def test_logger_exception_extract_exception_notes(stdout, service_name):
+ # GIVEN Logger is initialized
+ logger = Logger(service=service_name, stream=stdout)
+
+ # WHEN calling a logger.exception with a ValueError and notes
+ try:
+ raise ValueError("something went wrong")
+ except Exception as error:
+ error.add_note("something went wrong")
+ error.add_note("something went wrong again")
+ logger.exception("Received an exception")
+
+ # THEN we expect a "exception_name" to be "ValueError"
+ # THEN we except to have exception_notes in the exception
+ log = capture_logging_output(stdout)
+ assert len(log["exception_notes"]) == 2
+ assert log["exception_notes"][0] == "something went wrong"
+ assert log["exception_notes"][1] == "something went wrong again"
+ assert "ValueError" == log["exception_name"]
+
+
def test_logger_exception_should_not_fail_with_exception_block(stdout, service_name):
# GIVEN Logger is initialized
logger = Logger(service=service_name, stream=stdout)