Skip to content

Commit 1b33c9e

Browse files
authored
Merge branch 'awslabs:develop' into develop
2 parents 20be2f6 + e1927d5 commit 1b33c9e

File tree

20 files changed

+541
-401
lines changed

20 files changed

+541
-401
lines changed

.github/workflows/publish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
with:
4949
fetch-depth: 0
5050
- name: Set up Python
51-
uses: actions/setup-python@v2.2.2
51+
uses: actions/setup-python@v2.3.1
5252
with:
5353
python-version: "3.8"
5454
- name: Set release notes tag

.github/workflows/python_build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
steps:
2424
- uses: actions/checkout@v1
2525
- name: Set up Python ${{ matrix.python-version }}
26-
uses: actions/setup-python@v2.2.2
26+
uses: actions/setup-python@v2.3.1
2727
with:
2828
python-version: ${{ matrix.python-version }}
2929
- name: Install dependencies

.github/workflows/python_docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
with:
1818
fetch-depth: 0
1919
- name: Set up Python
20-
uses: actions/setup-python@v2.2.2
20+
uses: actions/setup-python@v2.3.1
2121
with:
2222
python-version: "3.8"
2323
- name: Install dependencies

.github/workflows/rebuild_latest_docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
with:
2626
fetch-depth: 0
2727
- name: Set up Python
28-
uses: actions/setup-python@v2.2.2
28+
uses: actions/setup-python@v2.3.1
2929
with:
3030
python-version: "3.8"
3131
- name: Set release notes tag

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ test:
2020
poetry run pytest -m "not perf" --cov=aws_lambda_powertools --cov-report=xml
2121
poetry run pytest --cache-clear tests/performance
2222

23+
unit-test:
24+
poetry run pytest tests/unit
25+
2326
coverage-html:
2427
poetry run pytest -m "not perf" --cov=aws_lambda_powertools --cov-report=html
2528

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ With [pip](https://pip.pypa.io/en/latest/index.html) installed, run: ``pip insta
4747

4848
## Connect
4949

50-
* **AWS Developers Slack**: `#lambda-powertools`** - **[Invite, if you don't have an account](https://join.slack.com/t/awsdevelopers/shared_invite/zt-gu30gquv-EhwIYq3kHhhysaZ2aIX7ew)**
50+
* **AWS Developers Slack**: `#lambda-powertools`** - **[Invite, if you don't have an account](https://join.slack.com/t/awsdevelopers/shared_invite/zt-yryddays-C9fkWrmguDv0h2EEDzCqvw)**
5151
* **Email**: [email protected]
5252

5353
## License

aws_lambda_powertools/event_handler/api_gateway.py

+5
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,10 @@ def include_router(self, router: "Router", prefix: Optional[str] = None) -> None
664664
prefix : str, optional
665665
An optional prefix to be added to the originally defined rule
666666
"""
667+
668+
# Add reference to parent ApiGatewayResolver to support use cases where people subclass it to add custom logic
669+
router.api_resolver = self
670+
667671
for route, func in router._routes.items():
668672
if prefix:
669673
rule = route[0]
@@ -678,6 +682,7 @@ class Router(BaseRouter):
678682

679683
def __init__(self):
680684
self._routes: Dict[tuple, Callable] = {}
685+
self.api_resolver: Optional[BaseRouter] = None
681686

682687
def route(
683688
self,

aws_lambda_powertools/metrics/metric.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
from contextlib import contextmanager
4-
from typing import Dict, Optional, Union
4+
from typing import Dict, Optional, Union, Generator
55

66
from .base import MetricManager, MetricUnit
77

@@ -61,7 +61,7 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
6161

6262

6363
@contextmanager
64-
def single_metric(name: str, unit: MetricUnit, value: float, namespace: Optional[str] = None):
64+
def single_metric(name: str, unit: MetricUnit, value: float, namespace: Optional[str] = None) -> Generator[SingleMetric, None, None]:
6565
"""Context manager to simplify creation of a single metric
6666
6767
Example

aws_lambda_powertools/tracing/tracer.py

+39-12
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
logger = logging.getLogger(__name__)
1818

1919
aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE)
20-
aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) # type: ignore # noqa: E501
2120

2221

2322
class Tracer:
@@ -137,7 +136,7 @@ def handler(event: dict, context: Any) -> Dict:
137136
"""
138137

139138
_default_config: Dict[str, Any] = {
140-
"service": "service_undefined",
139+
"service": "",
141140
"disabled": False,
142141
"auto_patch": True,
143142
"patch_modules": None,
@@ -156,7 +155,7 @@ def __init__(
156155
self.__build_config(
157156
service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider
158157
)
159-
self.provider: BaseProvider = self._config["provider"]
158+
self.provider = self._config["provider"]
160159
self.disabled = self._config["disabled"]
161160
self.service = self._config["service"]
162161
self.auto_patch = self._config["auto_patch"]
@@ -167,10 +166,8 @@ def __init__(
167166
if self.auto_patch:
168167
self.patch(modules=patch_modules)
169168

170-
# Set the streaming threshold to 0 on the default recorder to force sending
171-
# subsegments individually, rather than batching them.
172-
# See https://github.com/awslabs/aws-lambda-powertools-python/issues/283
173-
aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800
169+
if self._is_xray_provider():
170+
self._disable_xray_trace_batching()
174171

175172
def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]):
176173
"""Adds annotation to existing segment or subsegment
@@ -239,9 +236,9 @@ def patch(self, modules: Optional[Sequence[str]] = None):
239236
return
240237

241238
if modules is None:
242-
aws_xray_sdk.core.patch_all()
239+
self.provider.patch_all()
243240
else:
244-
aws_xray_sdk.core.patch(modules)
241+
self.provider.patch(modules)
245242

246243
def capture_lambda_handler(
247244
self,
@@ -304,11 +301,15 @@ def handler(event, context):
304301
def decorate(event, context, **kwargs):
305302
with self.provider.in_subsegment(name=f"## {lambda_handler_name}") as subsegment:
306303
global is_cold_start
304+
logger.debug("Annotating cold start")
305+
subsegment.put_annotation(key="ColdStart", value=is_cold_start)
306+
307307
if is_cold_start:
308-
logger.debug("Annotating cold start")
309-
subsegment.put_annotation(key="ColdStart", value=True)
310308
is_cold_start = False
311309

310+
if self.service:
311+
subsegment.put_annotation(key="Service", value=self.service)
312+
312313
try:
313314
logger.debug("Calling lambda handler")
314315
response = lambda_handler(event, context, **kwargs)
@@ -742,7 +743,8 @@ def __build_config(
742743
is_disabled = disabled if disabled is not None else self._is_tracer_disabled()
743744
is_service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV))
744745

745-
self._config["provider"] = provider or self._config["provider"] or aws_xray_sdk.core.xray_recorder
746+
# Logic: Choose overridden option first, previously cached config, or default if available
747+
self._config["provider"] = provider or self._config["provider"] or self._patch_xray_provider()
746748
self._config["auto_patch"] = auto_patch if auto_patch is not None else self._config["auto_patch"]
747749
self._config["service"] = is_service or self._config["service"]
748750
self._config["disabled"] = is_disabled or self._config["disabled"]
@@ -751,3 +753,28 @@ def __build_config(
751753
@classmethod
752754
def _reset_config(cls):
753755
cls._config = copy.copy(cls._default_config)
756+
757+
def _patch_xray_provider(self):
758+
# Due to Lazy Import, we need to activate `core` attrib via import
759+
# we also need to include `patch`, `patch_all` methods
760+
# to ensure patch calls are done via the provider
761+
from aws_xray_sdk.core import xray_recorder
762+
763+
provider = xray_recorder
764+
provider.patch = aws_xray_sdk.core.patch
765+
provider.patch_all = aws_xray_sdk.core.patch_all
766+
767+
return provider
768+
769+
def _disable_xray_trace_batching(self):
770+
"""Configure X-Ray SDK to send subsegment individually over batching
771+
Known issue: https://github.com/awslabs/aws-lambda-powertools-python/issues/283
772+
"""
773+
if self.disabled:
774+
logger.debug("Tracing has been disabled, aborting streaming override")
775+
return
776+
777+
aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0)
778+
779+
def _is_xray_provider(self):
780+
return "aws_xray_sdk" in self.provider.__module__

aws_lambda_powertools/utilities/idempotency/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
self.fn_args = function_args
5757
self.fn_kwargs = function_kwargs
5858

59-
persistence_store.configure(config)
59+
persistence_store.configure(config, self.function.__name__)
6060
self.persistence_store = persistence_store
6161

6262
def handle(self) -> Any:

aws_lambda_powertools/utilities/idempotency/persistence/base.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class BasePersistenceLayer(ABC):
112112

113113
def __init__(self):
114114
"""Initialize the defaults"""
115+
self.function_name = ""
115116
self.configured = False
116117
self.event_key_jmespath: Optional[str] = None
117118
self.event_key_compiled_jmespath = None
@@ -124,15 +125,19 @@ def __init__(self):
124125
self._cache: Optional[LRUDict] = None
125126
self.hash_function = None
126127

127-
def configure(self, config: IdempotencyConfig) -> None:
128+
def configure(self, config: IdempotencyConfig, function_name: Optional[str] = None) -> None:
128129
"""
129130
Initialize the base persistence layer from the configuration settings
130131
131132
Parameters
132133
----------
133134
config: IdempotencyConfig
134135
Idempotency configuration settings
136+
function_name: str, Optional
137+
The name of the function being decorated
135138
"""
139+
self.function_name = f"{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, 'test-func')}.{function_name or ''}"
140+
136141
if self.configured:
137142
# Prevent being reconfigured multiple times
138143
return
@@ -178,8 +183,7 @@ def _get_hashed_idempotency_key(self, data: Dict[str, Any]) -> str:
178183
warnings.warn(f"No value found for idempotency_key. jmespath: {self.event_key_jmespath}")
179184

180185
generated_hash = self._generate_hash(data=data)
181-
function_name = os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, "test-func")
182-
return f"{function_name}#{generated_hash}"
186+
return f"{self.function_name}#{generated_hash}"
183187

184188
@staticmethod
185189
def is_missing_idempotency_key(data) -> bool:

docs/core/event_handler/api_gateway.md

+30-39
Original file line numberDiff line numberDiff line change
@@ -1027,43 +1027,42 @@ When necessary, you can set a prefix when including a router object. This means
10271027

10281028
#### Sample layout
10291029

1030-
!!! info "We use ALB to demonstrate that the UX remains the same"
1031-
1032-
This sample project contains an Users function with two distinct set of routes, `/users` and `/health`. The layout optimizes for code sharing, no custom build tooling, and it uses [Lambda Layers](../../index.md#lambda-layer) to install Lambda Powertools.
1030+
This sample project contains a Users function with two distinct set of routes, `/users` and `/health`. The layout optimizes for code sharing, no custom build tooling, and it uses [Lambda Layers](../../index.md#lambda-layer) to install Lambda Powertools.
10331031

10341032
=== "Project layout"
10351033

10361034

1037-
```python hl_lines="6 8 10-13"
1035+
```python hl_lines="1 8 10 12-15"
10381036
.
1039-
├── Pipfile # project app & dev dependencies; poetry, pipenv, etc.
1037+
├── Pipfile # project app & dev dependencies; poetry, pipenv, etc.
10401038
├── Pipfile.lock
1041-
├── mypy.ini # namespace_packages = True
1042-
├── .env # VSCode only. PYTHONPATH="users:${PYTHONPATH}"
1043-
├── users
1044-
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: users, e.g. pipenv lock -r > users/requirements.txt
1045-
│ ├── lambda_function.py # this will be our users Lambda fn; it could be split in folders if we want separate fns same code base
1046-
│ ├── constants.py
1047-
│ └── routers # routers module
1039+
├── README.md
1040+
├── src
10481041
│ ├── __init__.py
1049-
│ ├── users.py # /users routes, e.g. from routers import users; users.router
1050-
│ ├── health.py # /health routes, e.g. from routers import health; health.router
1051-
├── template.yaml # SAM template.yml, CodeUri: users, Handler: users.main.lambda_handler
1042+
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src, e.g. pipenv lock -r > src/requirements.txt
1043+
│ └── users
1044+
│ ├── __init__.py
1045+
│ ├── main.py # this will be our users Lambda fn; it could be split in folders if we want separate fns same code base
1046+
│ └── routers # routers module
1047+
│ ├── __init__.py
1048+
│ ├── health.py # /users routes, e.g. from routers import users; users.router
1049+
│ └── users.py # /users routes, e.g. from .routers import users; users.router
1050+
├── template.yml # SAM template.yml, CodeUri: src, Handler: users.main.lambda_handler
10521051
└── tests
10531052
├── __init__.py
10541053
├── unit
10551054
│ ├── __init__.py
1056-
│ └── test_users.py # unit tests for the users router
1057-
│ └── test_health.py # unit tests for the health router
1055+
│ └── test_users.py # unit tests for the users router
1056+
│ └── test_health.py # unit tests for the health router
10581057
└── functional
10591058
├── __init__.py
1060-
├── conftest.py # pytest fixtures for the functional tests
1061-
└── test_lambda_function.py # functional tests for the main lambda handler
1059+
├── conftest.py # pytest fixtures for the functional tests
1060+
└── test_main.py # functional tests for the main lambda handler
10621061
```
10631062

10641063
=== "template.yml"
10651064

1066-
```yaml hl_lines="20-21"
1065+
```yaml hl_lines="22-23"
10671066
AWSTemplateFormatVersion: '2010-09-09'
10681067
Transform: AWS::Serverless-2016-10-31
10691068
Description: Example service with multiple routes
@@ -1073,6 +1072,8 @@ This sample project contains an Users function with two distinct set of routes,
10731072
MemorySize: 512
10741073
Runtime: python3.9
10751074
Tracing: Active
1075+
Architectures:
1076+
- x86_64
10761077
Environment:
10771078
Variables:
10781079
LOG_LEVEL: INFO
@@ -1083,11 +1084,11 @@ This sample project contains an Users function with two distinct set of routes,
10831084
UsersService:
10841085
Type: AWS::Serverless::Function
10851086
Properties:
1086-
Handler: lambda_function.lambda_handler
1087-
CodeUri: users
1087+
Handler: users.main.lambda_handler
1088+
CodeUri: src
10881089
Layers:
10891090
# Latest version: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer
1090-
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3
1091+
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:4
10911092
Events:
10921093
ByUser:
10931094
Type: Api
@@ -1119,7 +1120,7 @@ This sample project contains an Users function with two distinct set of routes,
11191120
Value: !GetAtt UsersService.Arn
11201121
```
11211122

1122-
=== "users/lambda_function.py"
1123+
=== "src/users/main.py"
11231124

11241125
```python hl_lines="9 15-16"
11251126
from typing import Dict
@@ -1130,23 +1131,23 @@ This sample project contains an Users function with two distinct set of routes,
11301131
from aws_lambda_powertools.logging.correlation_paths import APPLICATION_LOAD_BALANCER
11311132
from aws_lambda_powertools.utilities.typing import LambdaContext
11321133

1133-
from routers import health, users
1134+
from .routers import health, users
11341135

11351136
tracer = Tracer()
11361137
logger = Logger()
1137-
app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent)
1138+
app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent)
11381139

11391140
app.include_router(health.router)
11401141
app.include_router(users.router)
11411142

11421143

1143-
@logger.inject_lambda_context(correlation_id_path=APPLICATION_LOAD_BALANCER)
1144+
@logger.inject_lambda_context(correlation_id_path=API_GATEWAY_REST)
11441145
@tracer.capture_lambda_handler
11451146
def lambda_handler(event: Dict, context: LambdaContext):
11461147
return app.resolve(event, context)
11471148
```
11481149

1149-
=== "users/routers/health.py"
1150+
=== "src/users/routers/health.py"
11501151

11511152
```python hl_lines="4 6-7 10"
11521153
from typing import Dict
@@ -1169,7 +1170,7 @@ This sample project contains an Users function with two distinct set of routes,
11691170
```python hl_lines="3"
11701171
import json
11711172

1172-
from users import main # follows namespace package from root
1173+
from src.users import main # follows namespace package from root
11731174

11741175

11751176
def test_lambda_handler(apigw_event, lambda_context):
@@ -1180,16 +1181,6 @@ This sample project contains an Users function with two distinct set of routes,
11801181
assert ret["body"] == expected
11811182
```
11821183

1183-
=== ".env"
1184-
1185-
> Note: It is not needed for PyCharm (select folder as source).
1186-
1187-
This is necessary for Visual Studio Code, so integrated tooling works without failing import.
1188-
1189-
```bash
1190-
PYTHONPATH="users:${PYTHONPATH}"
1191-
```
1192-
11931184
### Considerations
11941185

11951186
This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework.

0 commit comments

Comments
 (0)