Skip to content

Commit 68b108d

Browse files
authored
Decorator factory Feat: Create your own middleware (#17)
* feat(utils): add decorator factory * improv: use partial to reduce complexity * improv: add error handling * chore: type hint * docs: include pypi downloads badge * feat: opt in to trace each middleware that runs * improv: add initial util tests * improv: test explicit and implicit trace_execution * improv: test decorator with params * chore: linting * docs: include utilities * improv: correct tests, dec_factory only for func * improv: make util name more explicit * improv: doc trace_execution, fix casting * docs: add limitations, improve syntax * docs: use new docs syntax * fix: remove middleware decorator from libs * feat: build docs in CI * chore: linting * fix: CI python-version type * chore: remove docs CI * chore: kick CI * chore: include build badge master branch * chore: refactor naming * fix: rearrange tracing tests * improv(tracer): toggle default auto patching * feat(tracer): retrieve registered class instance * fix(Makefile): make cov target more explicit * improv(Register): support multiple classes reg. * improv(Register): inject class methods correctly * docs: add how to reutilize Tracer * improv(tracer): test auto patch method * improv: address nicolas feedback * improv: update example to reflect middleware feat * fix: metric dimension in root blob * chore: version bump Co-authored-by: heitorlessa <[email protected]>
1 parent 6c076f7 commit 68b108d

File tree

16 files changed

+850
-348
lines changed

16 files changed

+850
-348
lines changed

Diff for: python/HISTORY.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# HISTORY
22

3+
## April 20th, 2020
4+
5+
**0.7.0**
6+
7+
* Introduces Middleware Factory to build your own middleware
8+
* Fixes Metrics dimensions not being included correctly in EMF
9+
310
## April 9th, 2020
411

512
**0.6.3**

Diff for: python/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ lint: format
1717
test:
1818
poetry run pytest -vvv
1919

20-
test-html:
20+
coverage-html:
2121
poetry run pytest --cov-report html
2222

2323
pr: lint test

Diff for: python/README.md

+118-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Lambda Powertools
22

3-
![PackageStatus](https://img.shields.io/static/v1?label=status&message=beta&color=blueviolet?style=flat-square) ![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)
3+
![PackageStatus](https://img.shields.io/static/v1?label=status&message=beta&color=blueviolet?style=flat-square) ![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) ![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master)
44

55
A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier - Currently available for Python only and compatible with Python >=3.6.
66

@@ -32,12 +32,20 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray,
3232
* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc)
3333
* No stack, custom resource, data collection needed — Metrics are created async by CloudWatch EMF
3434

35+
**Bring your own middleware**
36+
37+
* Utility to easily create your own middleware
38+
* Run logic before, after, and handle exceptions
39+
* Receive lambda handler, event, context
40+
* Optionally create sub-segment for each custom middleware
41+
3542
**Environment variables** used across suite of utilities
3643

3744
Environment variable | Description | Default | Utility
3845
------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------
3946
POWERTOOLS_SERVICE_NAME | Sets service name used for tracing namespace, metrics dimensions and structured logging | "service_undefined" | all
4047
POWERTOOLS_TRACE_DISABLED | Disables tracing | "false" | tracing
48+
POWERTOOLS_TRACE_MIDDLEWARES | Creates sub-segment for each middleware created by lambda_handler_decorator | "false" | middleware_factory
4149
POWERTOOLS_LOGGER_LOG_EVENT | Logs incoming event | "false" | logging
4250
POWERTOOLS_LOGGER_SAMPLE_RATE | Debug log sampling | 0 | logging
4351
POWERTOOLS_METRICS_NAMESPACE | Metrics namespace | None | metrics
@@ -85,6 +93,23 @@ def handler(event, context)
8593
...
8694
```
8795

96+
**Fetching a pre-configured tracer anywhere**
97+
98+
```python
99+
# handler.py
100+
from aws_lambda_powertools.tracing import Tracer
101+
tracer = Tracer(service="payment")
102+
103+
@tracer.capture_lambda_handler
104+
def handler(event, context)
105+
charge_id = event.get('charge_id')
106+
payment = collect_payment(charge_id)
107+
...
108+
109+
# another_file.py
110+
from aws_lambda_powertools.tracing import Tracer
111+
tracer = Tracer(auto_patch=False) # new instance using existing configuration with auto patching overriden
112+
```
88113

89114
### Logging
90115

@@ -154,7 +179,7 @@ def handler(event, context)
154179
}
155180
```
156181

157-
#### Custom Metrics async
182+
### Custom Metrics async
158183

159184
> **NOTE** `log_metric` will be removed once it's GA.
160185
@@ -204,6 +229,97 @@ with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric:
204229
metric.add_dimension(name="function_context", value="$LATEST")
205230
```
206231

232+
233+
### Utilities
234+
235+
#### Bring your own middleware
236+
237+
This feature allows you to create your own middleware as a decorator with ease by following a simple signature.
238+
239+
* Accept 3 mandatory args - `handler, event, context`
240+
* Always return the handler with event/context or response if executed
241+
- Supports nested middleware/decorators use case
242+
243+
**Middleware with no params**
244+
245+
```python
246+
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
247+
248+
@lambda_handler_decorator
249+
def middleware_name(handler, event, context):
250+
return handler(event, context)
251+
252+
@lambda_handler_decorator
253+
def middleware_before_after(handler, event, context):
254+
logic_before_handler_execution()
255+
response = handler(event, context)
256+
logic_after_handler_execution()
257+
return response
258+
259+
260+
# middleware_name will wrap Lambda handler
261+
# and simply return the handler as we're not pre/post-processing anything
262+
# then middleware_before_after will wrap middleware_name
263+
# run some code before/after calling the handler returned by middleware_name
264+
# This way, lambda_handler is only actually called once (top-down)
265+
@middleware_before_after # This will run last
266+
@middleware_name # This will run first
267+
def lambda_handler(event, context):
268+
return True
269+
```
270+
271+
**Middleware with params**
272+
273+
```python
274+
@lambda_handler_decorator
275+
def obfuscate_sensitive_data(handler, event, context, fields=None):
276+
# Obfuscate email before calling Lambda handler
277+
if fields:
278+
for field in fields:
279+
field = event.get(field, "")
280+
event[field] = obfuscate_pii(field)
281+
282+
return handler(event, context)
283+
284+
@obfuscate_sensitive_data(fields=["email"])
285+
def lambda_handler(event, context):
286+
return True
287+
```
288+
289+
**Optionally trace middleware execution**
290+
291+
This makes use of an existing Tracer instance that you may have initialized anywhere in your code, otherwise it'll initialize one using default options and provider (X-Ray).
292+
293+
```python
294+
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
295+
296+
@lambda_handler_decorator(trace_execution=True)
297+
def middleware_name(handler, event, context):
298+
return handler(event, context)
299+
300+
@middleware_name
301+
def lambda_handler(event, context):
302+
return True
303+
```
304+
305+
Optionally, you can enrich the final trace with additional annotations and metadata by retrieving a copy of the Tracer used.
306+
307+
```python
308+
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
309+
from aws_lambda_powertools.tracing import Tracer
310+
311+
@lambda_handler_decorator(trace_execution=True)
312+
def middleware_name(handler, event, context):
313+
tracer = Tracer() # Takes a copy of an existing tracer instance
314+
tracer.add_anotation...
315+
tracer.metadata...
316+
return handler(event, context)
317+
318+
@middleware_name
319+
def lambda_handler(event, context):
320+
return True
321+
```
322+
207323
## Beta
208324

209325
> **[Progress towards GA](https://github.com/awslabs/aws-lambda-powertools/projects/1)**

Diff for: python/aws_lambda_powertools/logging/logger.py

+49-47
Original file line numberDiff line numberDiff line change
@@ -93,32 +93,34 @@ def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = No
9393
Environment variables
9494
---------------------
9595
POWERTOOLS_LOGGER_LOG_EVENT : str
96-
instruct logger to log Lambda Event (e.g. "true", "True", "TRUE")
96+
instruct logger to log Lambda Event (e.g. `"true", "True", "TRUE"`)
9797
9898
Example
9999
-------
100-
Captures Lambda contextual runtime info (e.g memory, arn, req_id)
101-
>>> from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context
102-
>>> import logging
103-
>>>
104-
>>> logger = logging.getLogger(__name__)
105-
>>> logging.setLevel(logging.INFO)
106-
>>> logger_setup()
107-
>>>
108-
>>> @logger_inject_lambda_context
109-
>>> def handler(event, context):
100+
**Captures Lambda contextual runtime info (e.g memory, arn, req_id)**
101+
102+
from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context
103+
import logging
104+
105+
logger = logging.getLogger(__name__)
106+
logging.setLevel(logging.INFO)
107+
logger_setup()
108+
109+
@logger_inject_lambda_context
110+
def handler(event, context):
110111
logger.info("Hello")
111112
112-
Captures Lambda contextual runtime info and logs incoming request
113-
>>> from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context
114-
>>> import logging
115-
>>>
116-
>>> logger = logging.getLogger(__name__)
117-
>>> logging.setLevel(logging.INFO)
118-
>>> logger_setup()
119-
>>>
120-
>>> @logger_inject_lambda_context(log_event=True)
121-
>>> def handler(event, context):
113+
**Captures Lambda contextual runtime info and logs incoming request**
114+
115+
from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context
116+
import logging
117+
118+
logger = logging.getLogger(__name__)
119+
logging.setLevel(logging.INFO)
120+
logger_setup()
121+
122+
@logger_inject_lambda_context(log_event=True)
123+
def handler(event, context):
122124
logger.info("Hello")
123125
124126
Returns
@@ -128,9 +130,7 @@ def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = No
128130
"""
129131

130132
# If handler is None we've been called with parameters
131-
# We then return a partial function with args filled
132-
# Next time we're called we'll call our Lambda
133-
# This allows us to avoid writing wrapper_wrapper type of fn
133+
# Return a partial function with args filled
134134
if lambda_handler is None:
135135
logger.debug("Decorator called with parameters")
136136
return functools.partial(logger_inject_lambda_context, log_event=log_event)
@@ -178,14 +178,16 @@ def log_metric(
178178
):
179179
"""Logs a custom metric in a statsD-esque format to stdout.
180180
181+
**This will be removed when GA - Use `aws_lambda_powertools.metrics.metrics.Metrics` instead**
182+
181183
Creating Custom Metrics synchronously impact on performance/execution time.
182184
Instead, log_metric prints a metric to CloudWatch Logs.
183185
That allows us to pick them up asynchronously via another Lambda function and create them as a metric.
184186
185187
NOTE: It takes up to 9 dimensions by default, and Metric units are conveniently available via MetricUnit Enum.
186188
If service is not passed as arg or via env var, "service_undefined" will be used as dimension instead.
187189
188-
Output in CloudWatch Logs: MONITORING|<metric_value>|<metric_unit>|<metric_name>|<namespace>|<dimensions>
190+
**Output in CloudWatch Logs**: `MONITORING|<metric_value>|<metric_unit>|<metric_name>|<namespace>|<dimensions>`
189191
190192
Serverless Application Repository App that creates custom metric from this log output:
191193
https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:374852340823:applications~async-custom-metrics
@@ -195,23 +197,39 @@ def log_metric(
195197
POWERTOOLS_SERVICE_NAME: str
196198
service name
197199
200+
Parameters
201+
----------
202+
name : str
203+
metric name, by default None
204+
namespace : str
205+
metric namespace (e.g. application name), by default None
206+
unit : MetricUnit, by default MetricUnit.Count
207+
metric unit enum value (e.g. MetricUnit.Seconds), by default None\n
208+
API Info: https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html
209+
value : float, optional
210+
metric value, by default 0
211+
service : str, optional
212+
service name used as dimension, by default "service_undefined"
213+
dimensions: dict, optional
214+
keyword arguments as additional dimensions (e.g. `customer=customerId`)
215+
198216
Example
199217
-------
200-
Log metric to count number of successful payments; define service via env var
218+
**Log metric to count number of successful payments; define service via env var**
201219
202220
$ export POWERTOOLS_SERVICE_NAME="payment"
203-
>>> from aws_lambda_powertools.logging import MetricUnit, log_metric
204-
>>> log_metric(
221+
from aws_lambda_powertools.logging import MetricUnit, log_metric
222+
log_metric(
205223
name="SuccessfulPayments",
206224
unit=MetricUnit.Count,
207225
value=1,
208226
namespace="DemoApp"
209227
)
210228
211-
Log metric to count number of successful payments per campaign & customer
229+
**Log metric to count number of successful payments per campaign & customer**
212230
213-
>>> from aws_lambda_powertools.logging import MetricUnit, log_metric
214-
>>> log_metric(
231+
from aws_lambda_powertools.logging import MetricUnit, log_metric
232+
log_metric(
215233
name="SuccessfulPayments",
216234
service="payment",
217235
unit=MetricUnit.Count,
@@ -220,22 +238,6 @@ def log_metric(
220238
campaign=campaign_id,
221239
customer=customer_id
222240
)
223-
224-
Parameters
225-
----------
226-
name : str
227-
metric name, by default None
228-
namespace : str
229-
metric namespace (e.g. application name), by default None
230-
unit : MetricUnit, by default MetricUnit.Count
231-
metric unit enum value (e.g. MetricUnit.Seconds), by default None
232-
API Info: https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html
233-
value : float, optional
234-
metric value, by default 0
235-
service : str, optional
236-
service name used as dimension, by default "service_undefined"
237-
dimensions: dict, optional
238-
keyword arguments as additional dimensions (e.g. customer=customerId)
239241
"""
240242

241243
warnings.warn(message="This method will be removed in GA; use Metrics instead", category=DeprecationWarning)

Diff for: python/aws_lambda_powertools/metrics/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None) ->
177177
}
178178
metrics_timestamp = {"Timestamp": int(datetime.datetime.now().timestamp() * 1000)}
179179
metric_set["_aws"] = {**metrics_timestamp, **metrics_definition}
180+
metric_set.update(**dimensions)
180181

181182
try:
182183
logger.debug("Validating serialized metrics against CloudWatch EMF schema", metric_set)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
""" Utilities to enhance middlewares """
2+
from .factory import lambda_handler_decorator
3+
4+
__all__ = ["lambda_handler_decorator"]

0 commit comments

Comments
 (0)