Skip to content

Commit b7cb539

Browse files
author
Ran Isenberg
committed
Merge branch 'develop' of github.com:risenberg-cyberark/aws-lambda-powertools-python into pydantic
2 parents 47cd711 + 0b9294a commit b7cb539

File tree

21 files changed

+413
-47
lines changed

21 files changed

+413
-47
lines changed

CHANGELOG.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [1.6.1] - 2020-09-23
10+
11+
### Fixed
12+
- **Utilities**: Fix issue with boolean values in DynamoDB stream event data class.
13+
14+
## [1.6.0] - 2020-09-22
15+
916
### Added
10-
- **Metrics**: Support adding multiple metric values to a metric name
17+
- **Metrics**: Support adding multiple metric values to a single metric name
1118
- **Utilities**: Add new `Validator` utility to validate inbound events and responses using JSON Schema
19+
- **Utilities**: Add new `Event source data classes` utility to easily describe event schema of popular event sources
20+
- **Docs**: Add new `Testing your code` section to both Logger and Metrics page, and content width is now wider
21+
- **Tracer**: Support for automatically disable Tracer when running a Chalice app
22+
23+
### Fixed
24+
- **Docs**: Improve wording on log sampling feature in Logger, and removed duplicate content on main page
25+
- **Utilities**: Remove DeleteMessageBatch API call when there are no messages to delete
1226

1327
## [1.5.0] - 2020-09-04
1428

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master)
44
![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools)
55

6-
A suite of utilities for AWS Lambda functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier.
6+
A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, and more.
77

88
**[📜Documentation](https://awslabs.github.io/aws-lambda-powertools-python/)** | **[API Docs](https://awslabs.github.io/aws-lambda-powertools-python/api/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Feature request](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[Kitchen sink example](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/example)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/)**
99

aws_lambda_powertools/metrics/base.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class MetricManager:
8080

8181
def __init__(
8282
self,
83-
metric_set: Dict[str, str] = None,
83+
metric_set: Dict[str, Any] = None,
8484
dimension_set: Dict = None,
8585
namespace: str = None,
8686
metadata_set: Dict[str, Any] = None,
@@ -125,7 +125,7 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
125125
raise MetricValueError(f"{value} is not a valid number")
126126

127127
unit = self.__extract_metric_unit_value(unit=unit)
128-
metric = self.metric_set.get(name, defaultdict(list))
128+
metric: Dict = self.metric_set.get(name, defaultdict(list))
129129
metric["Unit"] = unit
130130
metric["Value"].append(float(value))
131131
logger.debug(f"Adding metric: {name} with {metric}")
@@ -185,7 +185,7 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None, me
185185
logger.debug({"details": "Serializing metrics", "metrics": metrics, "dimensions": dimensions})
186186

187187
metric_names_and_units: List[Dict[str, str]] = [] # [ { "Name": "metric_name", "Unit": "Count" } ]
188-
metric_names_and_values: Dict[str, str] = {} # { "metric_name": 1.0 }
188+
metric_names_and_values: Dict[str, float] = {} # { "metric_name": 1.0 }
189189

190190
for metric_name in metrics:
191191
metric: dict = metrics[metric_name]

aws_lambda_powertools/tracing/tracer.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -622,14 +622,15 @@ def _is_tracer_disabled() -> bool:
622622
"""
623623
logger.debug("Verifying whether Tracing has been disabled")
624624
is_lambda_sam_cli = os.getenv("AWS_SAM_LOCAL")
625+
is_chalice_cli = os.getenv("AWS_CHALICE_CLI_MODE")
625626
env_option = str(os.getenv("POWERTOOLS_TRACE_DISABLED", "false"))
626627
disabled_env = strtobool(env_option)
627628

628629
if disabled_env:
629630
logger.debug("Tracing has been disabled via env var POWERTOOLS_TRACE_DISABLED")
630631
return disabled_env
631632

632-
if is_lambda_sam_cli:
633+
if is_lambda_sam_cli or is_chalice_cli:
633634
logger.debug("Running under SAM CLI env or not in Lambda env; disabling Tracing")
634635
return True
635636

aws_lambda_powertools/utilities/batch/sqs.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ def _clean(self):
115115
queue_url = self._get_queue_url()
116116
entries_to_remove = self._get_entries_to_clean()
117117

118-
delete_message_response = self.client.delete_message_batch(QueueUrl=queue_url, Entries=entries_to_remove)
118+
delete_message_response = None
119+
if entries_to_remove:
120+
delete_message_response = self.client.delete_message_batch(QueueUrl=queue_url, Entries=entries_to_remove)
119121

120122
if self.suppress_exception:
121123
logger.debug(f"{len(self.fail_messages)} records failed processing, but exceptions are suppressed")

aws_lambda_powertools/utilities/data_classes/alb_event.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ALBEvent(BaseProxyEvent):
1919

2020
@property
2121
def request_context(self) -> ALBEventRequestContext:
22-
return ALBEventRequestContext(self)
22+
return ALBEventRequestContext(self._data)
2323

2424
@property
2525
def http_method(self) -> str:

aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def multi_value_query_string_parameters(self) -> Optional[Dict[str, List[str]]]:
231231

232232
@property
233233
def request_context(self) -> APIGatewayEventRequestContext:
234-
return APIGatewayEventRequestContext(self)
234+
return APIGatewayEventRequestContext(self._data)
235235

236236
@property
237237
def path_parameters(self) -> Optional[Dict[str, str]]:
@@ -371,7 +371,7 @@ def cookies(self) -> Optional[List[str]]:
371371

372372
@property
373373
def request_context(self) -> RequestContextV2:
374-
return RequestContextV2(self)
374+
return RequestContextV2(self._data)
375375

376376
@property
377377
def path_parameters(self) -> Optional[Dict[str, str]]:

aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def user_name(self) -> str:
5151
@property
5252
def caller_context(self) -> CallerContext:
5353
"""The caller context"""
54-
return CallerContext(self)
54+
return CallerContext(self._data)
5555

5656

5757
class PreSignUpTriggerEventRequest(DictWrapper):
@@ -119,11 +119,11 @@ class PreSignUpTriggerEvent(BaseTriggerEvent):
119119

120120
@property
121121
def request(self) -> PreSignUpTriggerEventRequest:
122-
return PreSignUpTriggerEventRequest(self)
122+
return PreSignUpTriggerEventRequest(self._data)
123123

124124
@property
125125
def response(self) -> PreSignUpTriggerEventResponse:
126-
return PreSignUpTriggerEventResponse(self)
126+
return PreSignUpTriggerEventResponse(self._data)
127127

128128

129129
class PostConfirmationTriggerEventRequest(DictWrapper):
@@ -156,7 +156,7 @@ class PostConfirmationTriggerEvent(BaseTriggerEvent):
156156

157157
@property
158158
def request(self) -> PostConfirmationTriggerEventRequest:
159-
return PostConfirmationTriggerEventRequest(self)
159+
return PostConfirmationTriggerEventRequest(self._data)
160160

161161

162162
class UserMigrationTriggerEventRequest(DictWrapper):
@@ -257,11 +257,11 @@ class UserMigrationTriggerEvent(BaseTriggerEvent):
257257

258258
@property
259259
def request(self) -> UserMigrationTriggerEventRequest:
260-
return UserMigrationTriggerEventRequest(self)
260+
return UserMigrationTriggerEventRequest(self._data)
261261

262262
@property
263263
def response(self) -> UserMigrationTriggerEventResponse:
264-
return UserMigrationTriggerEventResponse(self)
264+
return UserMigrationTriggerEventResponse(self._data)
265265

266266

267267
class CustomMessageTriggerEventRequest(DictWrapper):
@@ -342,11 +342,11 @@ class CustomMessageTriggerEvent(BaseTriggerEvent):
342342

343343
@property
344344
def request(self) -> CustomMessageTriggerEventRequest:
345-
return CustomMessageTriggerEventRequest(self)
345+
return CustomMessageTriggerEventRequest(self._data)
346346

347347
@property
348348
def response(self) -> CustomMessageTriggerEventResponse:
349-
return CustomMessageTriggerEventResponse(self)
349+
return CustomMessageTriggerEventResponse(self._data)
350350

351351

352352
class PreAuthenticationTriggerEventRequest(DictWrapper):
@@ -386,7 +386,7 @@ class PreAuthenticationTriggerEvent(BaseTriggerEvent):
386386
@property
387387
def request(self) -> PreAuthenticationTriggerEventRequest:
388388
"""Pre Authentication Request Parameters"""
389-
return PreAuthenticationTriggerEventRequest(self)
389+
return PreAuthenticationTriggerEventRequest(self._data)
390390

391391

392392
class PostAuthenticationTriggerEventRequest(DictWrapper):
@@ -428,7 +428,7 @@ class PostAuthenticationTriggerEvent(BaseTriggerEvent):
428428
@property
429429
def request(self) -> PostAuthenticationTriggerEventRequest:
430430
"""Post Authentication Request Parameters"""
431-
return PostAuthenticationTriggerEventRequest(self)
431+
return PostAuthenticationTriggerEventRequest(self._data)
432432

433433

434434
class GroupOverrideDetails(DictWrapper):
@@ -552,9 +552,9 @@ class PreTokenGenerationTriggerEvent(BaseTriggerEvent):
552552
@property
553553
def request(self) -> PreTokenGenerationTriggerEventRequest:
554554
"""Pre Token Generation Request Parameters"""
555-
return PreTokenGenerationTriggerEventRequest(self)
555+
return PreTokenGenerationTriggerEventRequest(self._data)
556556

557557
@property
558558
def response(self) -> PreTokenGenerationTriggerEventResponse:
559559
"""Pre Token Generation Response Parameters"""
560-
return PreTokenGenerationTriggerEventResponse(self)
560+
return PreTokenGenerationTriggerEventResponse(self._data)

aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def bool_value(self) -> Optional[bool]:
3535
Example:
3636
>>> {"BOOL": True}
3737
"""
38-
item = self.get("bool")
38+
item = self.get("BOOL")
3939
return None if item is None else bool(item)
4040

4141
@property

docs/content/core/logger.mdx

+49-7
Original file line numberDiff line numberDiff line change
@@ -222,21 +222,24 @@ If you ever forget to use `child` param, we will return an existing `Logger` wit
222222

223223
## Sampling debug logs
224224

225-
You can dynamically set a percentage of your logs to **DEBUG** level using `sample_rate` param or via env var `POWERTOOLS_LOGGER_SAMPLE_RATE`.
225+
Sampling allows you to set your Logger Log Level as DEBUG based on a percentage of your concurrent/cold start invocations. You can set a sampling value of `0.0` to `1` (100%) using either `sample_rate` parameter or `POWERTOOLS_LOGGER_SAMPLE_RATE` env var.
226226

227-
Sampling calculation happens at the Logger class initialization. This means, when configured it, sampling it's more likely to happen during concurrent requests, or infrequent invocations as [new Lambda execution contexts are created](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), not reused.
227+
This is useful when you want to troubleshoot an issue, say a sudden increase in concurrency, and you might not have enough information in your logs as Logger log level was understandably set as INFO.
228+
229+
Sampling decision happens at the Logger class initialization, which only happens during a cold start. This means sampling may happen significantly more or less than you expect if you have a steady low number of invocations and thus few cold starts.
228230

229231
<Note type="info">
230-
If you want this logic to happen on every invocation regardless whether Lambda reuses the execution environment or not, then create your Logger inside your Lambda handler.
232+
If you want Logger to calculate sampling on every invocation, then please open a feature request.
231233
</Note><br/>
232234

233235
```python:title=collect.py
234236
from aws_lambda_powertools import Logger
235237

236238
# Sample 10% of debug logs e.g. 0.1
237-
logger = Logger(sample_rate=0.1) # highlight-line
239+
logger = Logger(sample_rate=0.1, level="INFO") # highlight-line
238240

239241
def handler(event, context):
242+
logger.debug("Verifying whether order_id is present")
240243
if "order_id" in event:
241244
logger.info("Collecting payment")
242245
...
@@ -245,7 +248,21 @@ def handler(event, context):
245248
<details>
246249
<summary><strong>Excerpt output in CloudWatch Logs</strong></summary>
247250

248-
```json:title=cloudwatch_logs.json
251+
```json:title=sampled_log_request_as_debug.json
252+
{
253+
"timestamp": "2020-05-24 18:17:33,774",
254+
"level": "DEBUG", // highlight-line
255+
"location": "collect.handler:1",
256+
"service": "payment",
257+
"lambda_function_name": "test",
258+
"lambda_function_memory_size": 128,
259+
"lambda_function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test",
260+
"lambda_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72",
261+
"cold_start": true,
262+
"sampling_rate": 0.1, // highlight-line
263+
"message": "Verifying whether order_id is present"
264+
}
265+
249266
{
250267
"timestamp": "2020-05-24 18:17:33,774",
251268
"level": "INFO",
@@ -260,6 +277,7 @@ def handler(event, context):
260277
"message": "Collecting payment"
261278
}
262279
```
280+
263281
</details>
264282

265283

@@ -305,7 +323,7 @@ This can be fixed by either ensuring both has the `service` value as `payment`,
305323

306324
You might want to continue to use the same date formatting style, or override `location` to display the `package.function_name:line_number` as you previously had.
307325

308-
Logger allows you to either change the format or suppress the following keys altogether at the initialization: `location`, `timestamp`, `level`, and `datefmt`
326+
Logger allows you to either change the format or suppress the following keys altogether at the initialization: `location`, `timestamp`, `level`, `xray_trace_id`, and `datefmt`
309327

310328
```python
311329
from aws_lambda_powertools import Logger
@@ -317,7 +335,7 @@ logger = Logger(stream=stdout, location="[%(funcName)s] %(module)s", datefmt="fa
317335
logger = Logger(stream=stdout, location=None) # highlight-line
318336
```
319337

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`
338+
Alternatively, you can also change the order of the following log record keys via the `log_record_order` parameter: `level`, `location`, `message`, `xray_trace_id`, and `timestamp`
321339

322340
```python
323341
from aws_lambda_powertools import Logger
@@ -358,3 +376,27 @@ except Exception:
358376
}
359377
```
360378
</details>
379+
380+
381+
## Testing your code
382+
383+
When unit testing your code that makes use of `inject_lambda_context` decorator, you need to pass a dummy Lambda Context, or else Logger will fail.
384+
385+
This is a Pytest sample that provides the minimum information necessary for Logger to succeed:
386+
387+
```python:title=fake_lambda_context_for_logger.py
388+
@pytest.fixture
389+
def lambda_context():
390+
lambda_context = {
391+
"function_name": "test",
392+
"memory_limit_in_mb": 128,
393+
"invoked_function_arn": "arn:aws:lambda:eu-west-1:809313241:function:test",
394+
"aws_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72",
395+
}
396+
397+
return namedtuple("LambdaContext", lambda_context.keys())(*lambda_context.values())
398+
399+
def test_lambda_handler(lambda_handler, lambda_context):
400+
test_event = {'test': 'event'}
401+
lambda_handler(test_event, lambda_context) # this will now have a Context object populated
402+
```

docs/content/core/metrics.mdx

+28-2
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,37 @@ This has the advantage of keeping cold start metric separate from your applicati
251251

252252
## Testing your code
253253

254+
### Environment variables
255+
254256
Use `POWERTOOLS_METRICS_NAMESPACE` and `POWERTOOLS_SERVICE_NAME` env vars when unit testing your code to ensure metric namespace and dimension objects are created, and your code doesn't fail validation.
255257

256258
```bash:title=pytest_metric_namespace.sh
257-
258259
POWERTOOLS_SERVICE_NAME="Example" POWERTOOLS_METRICS_NAMESPACE="Application" python -m pytest
259260
```
260261

261-
You can ignore this if you are explicitly setting namespace/default dimension by passing the `namespace` and `service` parameters when initializing Metrics: `metrics = Metrics(namespace=ApplicationName, service=ServiceName)`.
262+
If you prefer setting environment variable for specific tests, and are using Pytest, you can use [monkeypatch](https://docs.pytest.org/en/latest/monkeypatch.html) fixture:
263+
264+
```python:title=pytest_env_var.py
265+
def test_namespace_env_var(monkeypatch):
266+
# Set POWERTOOLS_METRICS_NAMESPACE before initializating Metrics
267+
monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace)
268+
269+
metrics = Metrics()
270+
...
271+
```
272+
273+
> Ignore this, if you are explicitly setting namespace/default dimension via `namespace` and `service` parameters: `metrics = Metrics(namespace=ApplicationName, service=ServiceName)`
274+
275+
### Clearing metrics
276+
277+
`Metrics` keep metrics in memory across multiple instances. If you need to test this behaviour, you can use the following Pytest fixture to ensure metrics are reset incl. cold start:
278+
279+
```python:title=pytest_metrics_reset_fixture.py
280+
@pytest.fixture(scope="function", autouse=True)
281+
def reset_metric_set():
282+
# Clear out every metric data prior to every test
283+
metrics = Metrics()
284+
metrics.clear_metrics()
285+
metrics_global.is_cold_start = True # ensure each test has cold start
286+
yield
287+
```

0 commit comments

Comments
 (0)