Skip to content

Commit 1bc698b

Browse files
authored
Merge branch 'develop' into time2
2 parents c24118c + 4cd4a88 commit 1bc698b

19 files changed

+529
-95
lines changed

CHANGELOG.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
<a name="unreleased"></a>
55
# Unreleased
66

7+
## Bug Fixes
8+
9+
* **apigateway:** support nested router decorators ([#1709](https://github.com/awslabs/aws-lambda-powertools-python/issues/1709))
10+
* **parameters:** get_secret correctly return SecretBinary value ([#1717](https://github.com/awslabs/aws-lambda-powertools-python/issues/1717))
11+
712
## Documentation
813

914
* project name consistency
@@ -14,23 +19,39 @@
1419
## Features
1520

1621
* **apigateway:** multiple exceptions in exception_handler ([#1707](https://github.com/awslabs/aws-lambda-powertools-python/issues/1707))
22+
* **event_sources:** extract CloudWatch Logs in Kinesis streams ([#1710](https://github.com/awslabs/aws-lambda-powertools-python/issues/1710))
23+
* **logger:** log uncaught exceptions via system's exception hook ([#1727](https://github.com/awslabs/aws-lambda-powertools-python/issues/1727))
24+
* **parser:** extract CloudWatch Logs in Kinesis streams ([#1726](https://github.com/awslabs/aws-lambda-powertools-python/issues/1726))
1725

1826
## Maintenance
1927

20-
* **ci:** revert custom hw for E2E due to lack of hw
21-
* **ci:** prevent dependabot updates to trigger E2E
22-
* **ci:** use new custom hw for E2E
28+
* apigw test event wrongly set with base64
2329
* **ci:** limit to src only to prevent dependabot failures
30+
* **ci:** use new custom hw for E2E
31+
* **ci:** prevent dependabot updates to trigger E2E
32+
* **ci:** revert custom hw for E2E due to lack of hw
2433
* **deps:** bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 ([#1689](https://github.com/awslabs/aws-lambda-powertools-python/issues/1689))
34+
* **deps-dev:** bump flake8-builtins from 2.0.0 to 2.0.1 ([#1715](https://github.com/awslabs/aws-lambda-powertools-python/issues/1715))
35+
* **deps-dev:** bump mypy-boto3-appconfigdata from 1.25.0 to 1.26.0.post1 ([#1704](https://github.com/awslabs/aws-lambda-powertools-python/issues/1704))
36+
* **deps-dev:** bump mypy-boto3-xray from 1.25.0 to 1.26.0.post1 ([#1703](https://github.com/awslabs/aws-lambda-powertools-python/issues/1703))
37+
* **deps-dev:** bump mypy-boto3-cloudwatch from 1.25.0 to 1.26.0.post1 ([#1714](https://github.com/awslabs/aws-lambda-powertools-python/issues/1714))
38+
* **deps-dev:** bump flake8-bugbear from 22.10.25 to 22.10.27 ([#1665](https://github.com/awslabs/aws-lambda-powertools-python/issues/1665))
39+
* **deps-dev:** bump mypy-boto3-s3 from 1.25.0 to 1.26.0.post1 ([#1716](https://github.com/awslabs/aws-lambda-powertools-python/issues/1716))
40+
* **deps-dev:** bump types-requests from 2.28.11.3 to 2.28.11.4 ([#1701](https://github.com/awslabs/aws-lambda-powertools-python/issues/1701))
2541
* **deps-dev:** bump mypy-boto3-logs from 1.25.0 to 1.26.3 ([#1702](https://github.com/awslabs/aws-lambda-powertools-python/issues/1702))
42+
* **deps-dev:** bump mypy-boto3-lambda from 1.25.0 to 1.26.0.post1 ([#1705](https://github.com/awslabs/aws-lambda-powertools-python/issues/1705))
43+
* **deps-dev:** bump mypy-boto3-xray from 1.26.0.post1 to 1.26.9 ([#1720](https://github.com/awslabs/aws-lambda-powertools-python/issues/1720))
44+
* **deps-dev:** bump mypy-boto3-ssm from 1.26.0.post1 to 1.26.4 ([#1721](https://github.com/awslabs/aws-lambda-powertools-python/issues/1721))
45+
* **deps-dev:** bump mypy-boto3-appconfig from 1.25.0 to 1.26.0.post1 ([#1722](https://github.com/awslabs/aws-lambda-powertools-python/issues/1722))
46+
* **deps-dev:** bump pytest-xdist from 2.5.0 to 3.0.2 ([#1655](https://github.com/awslabs/aws-lambda-powertools-python/issues/1655))
2647
* **deps-dev:** bump mkdocs-material from 8.5.7 to 8.5.9 ([#1697](https://github.com/awslabs/aws-lambda-powertools-python/issues/1697))
2748
* **deps-dev:** bump flake8-comprehensions from 3.10.0 to 3.10.1 ([#1699](https://github.com/awslabs/aws-lambda-powertools-python/issues/1699))
2849
* **deps-dev:** bump types-requests from 2.28.11.2 to 2.28.11.3 ([#1698](https://github.com/awslabs/aws-lambda-powertools-python/issues/1698))
2950
* **deps-dev:** bump pytest-benchmark from 3.4.1 to 4.0.0 ([#1659](https://github.com/awslabs/aws-lambda-powertools-python/issues/1659))
3051
* **deps-dev:** bump mypy-boto3-secretsmanager from 1.25.0 to 1.26.0.post1 ([#1691](https://github.com/awslabs/aws-lambda-powertools-python/issues/1691))
31-
* **deps-dev:** bump pytest-xdist from 2.5.0 to 3.0.2 ([#1655](https://github.com/awslabs/aws-lambda-powertools-python/issues/1655))
32-
* **deps-dev:** bump flake8-bugbear from 22.10.25 to 22.10.27 ([#1665](https://github.com/awslabs/aws-lambda-powertools-python/issues/1665))
52+
* **deps-dev:** bump pytest-asyncio from 0.20.1 to 0.20.2 ([#1723](https://github.com/awslabs/aws-lambda-powertools-python/issues/1723))
3353
* **deps-dev:** bump mypy-boto3-ssm from 1.25.0 to 1.26.0.post1 ([#1690](https://github.com/awslabs/aws-lambda-powertools-python/issues/1690))
54+
* **logger:** overload inject_lambda_context with generics ([#1583](https://github.com/awslabs/aws-lambda-powertools-python/issues/1583))
3455

3556

3657
<a name="v2.2.0"></a>

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ def register_route(func: Callable):
798798
# Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key
799799
methods = (method,) if isinstance(method, str) else tuple(method)
800800
self._routes[(rule, methods, cors, compress, cache_control)] = func
801+
return func
801802

802803
return register_route
803804

aws_lambda_powertools/logging/logger.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import functools
24
import inspect
35
import io
@@ -96,6 +98,11 @@ class Logger(logging.Logger): # lgtm [py/missing-call-to-init]
9698
custom logging formatter that implements PowertoolsFormatter
9799
logger_handler: logging.Handler, optional
98100
custom logging handler e.g. logging.FileHandler("file.log")
101+
log_uncaught_exceptions: bool, by default False
102+
logs uncaught exception using sys.excepthook
103+
104+
See: https://docs.python.org/3/library/sys.html#sys.excepthook
105+
99106
100107
Parameters propagated to LambdaPowertoolsFormatter
101108
--------------------------------------------------
@@ -203,6 +210,7 @@ def __init__(
203210
stream: Optional[IO[str]] = None,
204211
logger_formatter: Optional[PowertoolsFormatter] = None,
205212
logger_handler: Optional[logging.Handler] = None,
213+
log_uncaught_exceptions: bool = False,
206214
json_serializer: Optional[Callable[[Dict], str]] = None,
207215
json_deserializer: Optional[Callable[[Union[Dict, str, bool, int, float]], str]] = None,
208216
json_default: Optional[Callable[[Any], Any]] = None,
@@ -222,6 +230,8 @@ def __init__(
222230
self.child = child
223231
self.logger_formatter = logger_formatter
224232
self.logger_handler = logger_handler or logging.StreamHandler(stream)
233+
self.log_uncaught_exceptions = log_uncaught_exceptions
234+
225235
self.log_level = self._get_log_level(level)
226236
self._is_deduplication_disabled = resolve_truthy_env_var_choice(
227237
env=os.getenv(constants.LOGGER_LOG_DEDUPLICATION_ENV, "false")
@@ -244,6 +254,10 @@ def __init__(
244254

245255
self._init_logger(formatter_options=formatter_options, **kwargs)
246256

257+
if self.log_uncaught_exceptions:
258+
logger.debug("Replacing exception hook")
259+
sys.excepthook = functools.partial(log_uncaught_exception_hook, logger=self)
260+
247261
# Prevent __getattr__ from shielding unknown attribute errors in type checkers
248262
# https://github.com/awslabs/aws-lambda-powertools-python/issues/1660
249263
if not TYPE_CHECKING:
@@ -735,3 +749,8 @@ def _is_internal_frame(frame): # pragma: no cover
735749
"""Signal whether the frame is a CPython or logging module internal."""
736750
filename = os.path.normcase(frame.f_code.co_filename)
737751
return filename == logging._srcfile or ("importlib" in filename and "_bootstrap" in filename)
752+
753+
754+
def log_uncaught_exception_hook(exc_type, exc_value, exc_traceback, logger: Logger):
755+
"""Callback function for sys.excepthook to use Logger to log uncaught exceptions"""
756+
logger.exception("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) # pragma: no cover

aws_lambda_powertools/utilities/data_classes/kinesis_stream_event.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import base64
22
import json
3-
from typing import Iterator
3+
import zlib
4+
from typing import Iterator, List
45

6+
from aws_lambda_powertools.utilities.data_classes.cloud_watch_logs_event import (
7+
CloudWatchLogsDecodedData,
8+
)
59
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
610

711

@@ -43,6 +47,11 @@ def data_as_json(self) -> dict:
4347
"""Decode binary encoded data as json"""
4448
return json.loads(self.data_as_text())
4549

50+
def data_zlib_compressed_as_json(self) -> dict:
51+
"""Decode binary encoded data as bytes"""
52+
decompressed = zlib.decompress(self.data_as_bytes(), zlib.MAX_WBITS | 32)
53+
return json.loads(decompressed)
54+
4655

4756
class KinesisStreamRecord(DictWrapper):
4857
@property
@@ -98,3 +107,11 @@ class KinesisStreamEvent(DictWrapper):
98107
def records(self) -> Iterator[KinesisStreamRecord]:
99108
for record in self["Records"]:
100109
yield KinesisStreamRecord(record)
110+
111+
112+
def extract_cloudwatch_logs_from_event(event: KinesisStreamEvent) -> List[CloudWatchLogsDecodedData]:
113+
return [CloudWatchLogsDecodedData(record.kinesis.data_zlib_compressed_as_json()) for record in event.records]
114+
115+
116+
def extract_cloudwatch_logs_from_record(record: KinesisStreamRecord) -> CloudWatchLogsDecodedData:
117+
return CloudWatchLogsDecodedData(data=record.kinesis.data_zlib_compressed_as_json())

aws_lambda_powertools/utilities/parameters/secrets.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ def _get(self, name: str, **sdk_options) -> str:
9696
# Explicit arguments will take precedence over keyword arguments
9797
sdk_options["SecretId"] = name
9898

99-
return self.client.get_secret_value(**sdk_options)["SecretString"]
99+
secret_value = self.client.get_secret_value(**sdk_options)
100+
101+
if "SecretString" in secret_value:
102+
return secret_value["SecretString"]
103+
104+
return secret_value["SecretBinary"]
100105

101106
def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
102107
"""

aws_lambda_powertools/utilities/parser/models/kinesis.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
from typing import List, Type, Union
1+
import json
2+
import zlib
3+
from typing import Dict, List, Type, Union
24

35
from pydantic import BaseModel, validator
46

57
from aws_lambda_powertools.shared.functions import base64_decode
8+
from aws_lambda_powertools.utilities.parser.models.cloudwatch import (
9+
CloudWatchLogsDecode,
10+
)
611
from aws_lambda_powertools.utilities.parser.types import Literal
712

813

@@ -28,6 +33,21 @@ class KinesisDataStreamRecord(BaseModel):
2833
eventSourceARN: str
2934
kinesis: KinesisDataStreamRecordPayload
3035

36+
def decompress_zlib_record_data_as_json(self) -> Dict:
37+
"""Decompress Kinesis Record bytes data zlib compressed to JSON"""
38+
if not isinstance(self.kinesis.data, bytes):
39+
raise ValueError("We can only decompress bytes data, not custom models.")
40+
41+
return json.loads(zlib.decompress(self.kinesis.data, zlib.MAX_WBITS | 32))
42+
3143

3244
class KinesisDataStreamModel(BaseModel):
3345
Records: List[KinesisDataStreamRecord]
46+
47+
48+
def extract_cloudwatch_logs_from_event(event: KinesisDataStreamModel) -> List[CloudWatchLogsDecode]:
49+
return [CloudWatchLogsDecode(**record.decompress_zlib_record_data_as_json()) for record in event.Records]
50+
51+
52+
def extract_cloudwatch_logs_from_record(record: KinesisDataStreamRecord) -> CloudWatchLogsDecode:
53+
return CloudWatchLogsDecode(**record.decompress_zlib_record_data_as_json())

docs/core/logger.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,30 @@ Use `logger.exception` method to log contextual information about exceptions. Lo
291291
--8<-- "examples/logger/src/logging_exceptions_output.json"
292292
```
293293

294+
#### Uncaught exceptions
295+
296+
Logger can optionally log uncaught exceptions by setting `log_uncaught_exceptions=True` at initialization.
297+
298+
!!! info "Logger will replace any exception hook previously registered via [sys.excepthook](https://docs.python.org/3/library/sys.html#sys.excepthook){target='_blank'}."
299+
300+
??? question "What are uncaught exceptions?"
301+
302+
It's any raised exception that wasn't handled by the [`except` statement](https://docs.python.org/3.9/tutorial/errors.html#handling-exceptions){target="_blank"}, leading a Python program to a non-successful exit.
303+
304+
They are typically raised intentionally to signal a problem (`raise ValueError`), or a propagated exception from elsewhere in your code that you didn't handle it willingly or not (`KeyError`, `jsonDecoderError`, etc.).
305+
306+
=== "logging_uncaught_exceptions.py"
307+
308+
```python hl_lines="7"
309+
--8<-- "examples/logger/src/logging_uncaught_exceptions.py"
310+
```
311+
312+
=== "logging_uncaught_exceptions_output.json"
313+
314+
```json hl_lines="7-8"
315+
--8<-- "examples/logger/src/logging_uncaught_exceptions_output.json"
316+
```
317+
294318
### Date formatting
295319

296320
Logger uses Python's standard logging date format with the addition of timezone: `2021-05-03 11:47:12,494+0200`.

0 commit comments

Comments
 (0)