Skip to content

docs(middleware-factory): snippets split, improved, and lint #1451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/media/middleware_factory_tracer_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/middleware_factory_tracer_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 68 additions & 46 deletions docs/utilities/middleware_factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ title: Middleware factory
description: Utility
---

<!-- markdownlint-disable MD043 -->

Middleware factory provides a decorator factory to create your own middleware to run logic before, and after each Lambda invocation synchronously.

## Key features

* Run logic before, after, and handle exceptions
* Trace each middleware when requested

## Getting started

???+ tip
All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}.

You might need a middleware factory to abstract non-functional code and focus on business logic or even validate the payload before the lambda run, among other cases.

## Middleware with no params

You can create your own middleware using `lambda_handler_decorator`. The decorator factory expects 3 arguments in your function signature:
Expand All @@ -18,74 +27,87 @@ You can create your own middleware using `lambda_handler_decorator`. The decorat
* **event** - Lambda function invocation event
* **context** - Lambda function context object

```python hl_lines="3-4 10" title="Creating your own middleware for before/after logic"
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
### Creating your own middleware for before logic

=== "getting_started_middleware_before_logic_function.py"
```python hl_lines="5 23 24 29 30 32 37 38"
--8<-- "examples/middleware_factory/src/getting_started_middleware_before_logic_function.py"
```

=== "getting_started_middleware_before_logic_payload.json"

```json hl_lines="9-13"
--8<-- "examples/middleware_factory/src/getting_started_middleware_before_logic_payload.json"
```

### Creating your own middleware for after logic

@lambda_handler_decorator
def middleware_before_after(handler, event, context):
# logic_before_handler_execution()
response = handler(event, context)
# logic_after_handler_execution()
return response
=== "getting_started_middleware_after_logic_function.py"
```python hl_lines="7 14 15 21-23 37"
--8<-- "examples/middleware_factory/src/getting_started_middleware_after_logic_function.py"
```

@middleware_before_after
def lambda_handler(event, context):
...
```
=== "getting_started_middleware_after_logic_payload.json"

```json
--8<-- "examples/middleware_factory/src/getting_started_middleware_after_logic_payload.json"
```

## Middleware with params

You can also have your own keyword arguments after the mandatory arguments.

```python hl_lines="2 12" title="Accepting arbitrary keyword arguments"
@lambda_handler_decorator
def obfuscate_sensitive_data(handler, event, context, fields: List = None):
# Obfuscate email before calling Lambda handler
if fields:
for field in fields:
if field in event:
event[field] = obfuscate(event[field])
=== "getting_started_middleware_with_params_function.py"
```python hl_lines="6 27 28 29 33 49"
--8<-- "examples/middleware_factory/src/getting_started_middleware_with_params_function.py"
```

return handler(event, context)
=== "getting_started_middleware_with_params_payload.json"

@obfuscate_sensitive_data(fields=["email"])
def lambda_handler(event, context):
...
```
```json hl_lines="18 19 20"
--8<-- "examples/middleware_factory/src/getting_started_middleware_with_params_payload.json"
```

## Tracing middleware execution

If you are making use of [Tracer](../core/tracer.md), you can trace the execution of your middleware to ease operations.

This makes use of an existing Tracer instance that you may have initialized anywhere in your code.

```python hl_lines="3" title="Tracing custom middlewares with Tracer"
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
???+ warning
You must enable Active Tracing in your Lambda function when using this feature, otherwise Lambda will not be able to send traces to XRay..

=== "getting_started_middleware_tracer_function.py"
```python hl_lines="8 14 15 36"
--8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_function.py"
```

=== "getting_started_middleware_tracer_payload.json"

```json hl_lines="18 19 20"
--8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_payload.json"
```

@lambda_handler_decorator(trace_execution=True)
def my_middleware(handler, event, context):
return handler(event, context)
When executed, your middleware name will [appear in AWS X-Ray Trace details as](../core/tracer.md) `## middleware_name`, in this example the middleware name is `## middleware_with_tracing`.

@my_middleware
def lambda_handler(event, context):
...
```
![Middleware simple Tracer](../media/middleware_factory_tracer_1.png)

When executed, your middleware name will [appear in AWS X-Ray Trace details as](../core/tracer.md) `## middleware_name`.
## Advanced

For advanced use cases, you can instantiate [Tracer](../core/tracer.md) inside your middleware, and add annotations as well as metadata for additional operational insights.

```python hl_lines="6-8" title="Add custom tracing insights before/after in your middlware"
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools import Tracer

@lambda_handler_decorator(trace_execution=True)
def middleware_name(handler, event, context):
# tracer = Tracer() # Takes a copy of an existing tracer instance
# tracer.add_annotation...
# tracer.add_metadata...
return handler(event, context)
```
=== "advanced_middleware_tracer_function.py"
```python hl_lines="7 9 12 16 17 19 25 42"
--8<-- "examples/middleware_factory/src/advanced_middleware_tracer_function.py"
```

=== "advanced_middleware_tracer_payload.json"

```json
--8<-- "examples/middleware_factory/src/advanced_middleware_tracer_payload.json"
```

![Middleware avanced Tracer](../media/middleware_factory_tracer_2.png)

## Tips

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import time
from typing import Callable

import requests
from requests import Response

from aws_lambda_powertools import Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.typing import LambdaContext

tracer = Tracer()
app = APIGatewayRestResolver()


@lambda_handler_decorator(trace_execution=True)
def middleware_with_advanced_tracing(handler, event, context) -> Callable:

tracer.put_metadata(key="resource", value=event.get("resource"))

start_time = time.time()
response = handler(event, context)
execution_time = time.time() - start_time

tracer.put_annotation(key="TotalExecutionTime", value=str(execution_time))

# adding custom headers in response object after lambda executing
response["headers"]["execution_time"] = execution_time
response["headers"]["aws_request_id"] = context.aws_request_id

return response


@app.get("/products")
def create_product() -> dict:
product: Response = requests.get("https://dummyjson.com/products/1")
product.raise_for_status()

return {"product": product.json()}


@middleware_with_advanced_tracing
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"resource": "/products",
"path": "/products",
"httpMethod": "GET"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import time
from typing import Callable

import requests
from requests import Response

from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.typing import LambdaContext

app = APIGatewayRestResolver()


@lambda_handler_decorator
def middleware_after(handler, event, context) -> Callable:

start_time = time.time()
response = handler(event, context)
execution_time = time.time() - start_time

# adding custom headers in response object after lambda executing
response["headers"]["execution_time"] = execution_time
response["headers"]["aws_request_id"] = context.aws_request_id

return response


@app.post("/todos")
def create_todo() -> dict:
todo_data: dict = app.current_event.json_body # deserialize json str to dict
todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data)
todo.raise_for_status()

return {"todo": todo.json()}


@middleware_after
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"resource": "/todos",
"path": "/todos",
"httpMethod": "POST",
"body": "{\"title\": \"foo\", \"userId\": 1, \"completed\": false}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from dataclasses import dataclass, field
from typing import Callable
from uuid import uuid4

from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.jmespath_utils import envelopes, extract_data_from_envelope
from aws_lambda_powertools.utilities.typing import LambdaContext


@dataclass
class Payment:
user_id: str
order_id: str
amount: float
status_id: str
payment_id: str = field(default_factory=lambda: f"{uuid4()}")


class PaymentError(Exception):
...


@lambda_handler_decorator
def middleware_before(handler, event, context) -> Callable:
# extract payload from a EventBridge event
detail: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE)

# check if status_id exists in payload, otherwise add default state before processing payment
if not detail.get("status_id"):
event["detail"]["status_id"] = "pending"

response = handler(event, context)

return response


@middleware_before
def lambda_handler(event, context: LambdaContext) -> dict:
try:
payment_payload: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE)
return {
"order": Payment(**payment_payload).__dict__,
"message": "payment created",
"success": True,
}
except Exception as e:
raise PaymentError("Unable to create payment") from e
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0",
"id": "9c95e8e4-96a4-ef3f-b739-b6aa5b193afb",
"detail-type": "PaymentCreated",
"source": "app.payment",
"account": "0123456789012",
"time": "2022-08-08T20:41:53Z",
"region": "eu-east-1",
"detail": {
"amount": "150.00",
"order_id": "8f1f1710-1b30-48a5-a6bd-153fd23b866b",
"user_id": "f80e3c51-5b8c-49d5-af7d-c7804966235f"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import time
from typing import Callable

import requests
from requests import Response

from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.typing import LambdaContext

app = APIGatewayRestResolver()


@lambda_handler_decorator(trace_execution=True)
def middleware_with_tracing(handler, event, context) -> Callable:

start_time = time.time()
response = handler(event, context)
execution_time = time.time() - start_time

# adding custom headers in response object after lambda executing
response["headers"]["execution_time"] = execution_time
response["headers"]["aws_request_id"] = context.aws_request_id

return response


@app.get("/products")
def create_product() -> dict:
product: Response = requests.get("https://dummyjson.com/products/1")
product.raise_for_status()

return {"product": product.json()}


@middleware_with_tracing
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"resource": "/products",
"path": "/products",
"httpMethod": "GET"
}
Loading