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 all 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.
157 changes: 106 additions & 51 deletions docs/utilities/middleware_factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,144 @@ 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
* Built-in tracing opt-in capability

## 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 custom middleware to abstract non-functional code. These are often custom authorization or any reusable logic you might need to run before/after a Lambda function invocation.

## Middleware with no params
### Middleware with no params

You can create your own middleware using `lambda_handler_decorator`. The decorator factory expects 3 arguments in your function signature:

* **handler** - Lambda function handler
* **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
### Middleware with 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"
```

@lambda_handler_decorator
def middleware_before_after(handler, event, context):
# logic_before_handler_execution()
response = handler(event, context)
# logic_after_handler_execution()
return response
### Middleware with after logic

@middleware_before_after
def lambda_handler(event, context):
...
```
=== "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 with params
=== "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"
```

=== "getting_started_middleware_with_params_payload.json"

return handler(event, context)
```json hl_lines="18 19 20"
--8<-- "examples/middleware_factory/src/getting_started_middleware_with_params_payload.json"
```

## 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.

@obfuscate_sensitive_data(fields=["email"])
def lambda_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"
```

## Tracing middleware execution
=== "advanced_middleware_tracer_payload.json"

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

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

### 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](../core/tracer/#permissions) in your Lambda function when using this feature, otherwise Lambda cannot send traces to XRay.

@lambda_handler_decorator(trace_execution=True)
def my_middleware(handler, event, context):
return handler(event, context)
=== "getting_started_middleware_tracer_function.py"
```python hl_lines="8 14 15 36"
--8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_function.py"
```

@my_middleware
def lambda_handler(event, context):
...
```
=== "getting_started_middleware_tracer_payload.json"

When executed, your middleware name will [appear in AWS X-Ray Trace details as](../core/tracer.md) `## middleware_name`.
```json hl_lines="18 19 20"
--8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_payload.json"
```

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.
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`.

![Middleware simple Tracer](../media/middleware_factory_tracer_1.png)

### Combining Powertools utilities

You can create your own middleware and combine many features of Lambda Powertools such as [trace](../core/logger.md), [logs](../core/logger.md), [feature flags](feature_flags.md), [validation](validation.md), [jmespath_functions](jmespath_functions.md) and others to abstract non-functional code.

In the example below, we create a Middleware with the following features:

* Logs and traces
* Validate if the payload contains a specific header
* Extract specific keys from event
* Automatically add security headers on every execution
* Validate if a specific feature flag is enabled
* Save execution history to a DynamoDB table

=== "combining_powertools_utilities_function.py"
```python hl_lines="11 28 29 119 52 61 73"
--8<-- "examples/middleware_factory/src/combining_powertools_utilities_function.py"
```

=== "combining_powertools_utilities_schema.py"
```python hl_lines="12 14"
--8<-- "examples/middleware_factory/src/combining_powertools_utilities_schema.py"
```

=== "combining_powertools_utilities_event.json"
```python hl_lines="10"
--8<-- "examples/middleware_factory/src/combining_powertools_utilities_event.json"
```

```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)
```
=== "SAM TEMPLATE"
```python hl_lines="66 83 89 96 103 108-113 119 130"
--8<-- "examples/middleware_factory/sam/combining_powertools_utilities_template.yaml"
```

## Tips

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Middleware-powertools-utilities example

Globals:
Function:
Timeout: 5
Runtime: python3.9
Tracing: Active
Architectures:
- x86_64
Environment:
Variables:
LOG_LEVEL: DEBUG
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
POWERTOOLS_SERVICE_NAME: middleware

Resources:
MiddlewareFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: middleware/
Handler: app.lambda_handler
Description: Middleware function
Policies:
- AWSLambdaBasicExecutionRole # Managed Policy
- Version: '2012-10-17' # Policy Document
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: !GetAtt HistoryTable.Arn
- Effect: Allow
Action: # https://docs.aws.amazon.com/appconfig/latest/userguide/getting-started-with-appconfig-permissions.html
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm comfortable with this, but from what I can see it's a real example in an official AWS documentation.

- ssm:GetDocument
- ssm:ListDocuments
- appconfig:GetLatestConfiguration
- appconfig:StartConfigurationSession
- appconfig:ListApplications
- appconfig:GetApplication
- appconfig:ListEnvironments
- appconfig:GetEnvironment
- appconfig:ListConfigurationProfiles
- appconfig:GetConfigurationProfile
- appconfig:ListDeploymentStrategies
- appconfig:GetDeploymentStrategy
- appconfig:GetConfiguration
- appconfig:ListDeployments
- appconfig:GetDeployment
Resource: "*"
Events:
GetComments:
Type: Api
Properties:
Path: /comments
Method: GET
GetCommentsById:
Type: Api
Properties:
Path: /comments/{comment_id}
Method: GET

# DynamoDB table to store historical data
HistoryTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: "HistoryTable"
AttributeDefinitions:
- AttributeName: customer_id
AttributeType: S
- AttributeName: request_id
AttributeType: S
KeySchema:
- AttributeName: customer_id
KeyType: HASH
- AttributeName: request_id
KeyType: "RANGE"
BillingMode: PAY_PER_REQUEST

# Feature flags using AppConfig
FeatureCommentApp:
Type: AWS::AppConfig::Application
Properties:
Description: "Comments Application for feature toggles"
Name: comments

FeatureCommentDevEnv:
Type: AWS::AppConfig::Environment
Properties:
ApplicationId: !Ref FeatureCommentApp
Description: "Development Environment for the App Config Comments"
Name: dev

FeatureCommentConfigProfile:
Type: AWS::AppConfig::ConfigurationProfile
Properties:
ApplicationId: !Ref FeatureCommentApp
Name: features
LocationUri: "hosted"

HostedConfigVersion:
Type: AWS::AppConfig::HostedConfigurationVersion
Properties:
ApplicationId: !Ref FeatureCommentApp
ConfigurationProfileId: !Ref FeatureCommentConfigProfile
Description: 'A sample hosted configuration version'
Content: |
{
"save_history": {
"default": true
}
}
ContentType: 'application/json'

# this is just an example
# change this values according your deployment strategy
BasicDeploymentStrategy:
Copy link
Contributor Author

@leandrodamascena leandrodamascena Aug 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the deployment strategy to avoid the default baking time.. The default time (10 minutes) is too long to someone that wants to test this template.

Type: AWS::AppConfig::DeploymentStrategy
Properties:
Name: "Deployment"
Description: "Deployment strategy for comments app."
DeploymentDurationInMinutes: 1
FinalBakeTimeInMinutes: 1
GrowthFactor: 100
GrowthType: LINEAR
ReplicateTo: NONE

ConfigDeployment:
Type: AWS::AppConfig::Deployment
Properties:
ApplicationId: !Ref FeatureCommentApp
ConfigurationProfileId: !Ref FeatureCommentConfigProfile
ConfigurationVersion: !Ref HostedConfigVersion
DeploymentStrategyId: !Ref BasicDeploymentStrategy
EnvironmentId: !Ref FeatureCommentDevEnv
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"
}
Loading