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 1 commit
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
35 changes: 34 additions & 1 deletion docs/utilities/middleware_factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ For advanced use cases, you can instantiate [Tracer](../core/tracer.md) inside y
--8<-- "examples/middleware_factory/src/advanced_middleware_tracer_payload.json"
```

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

### Tracing middleware **execution**

Expand All @@ -109,6 +109,39 @@ When executed, your middleware name will [appear in AWS X-Ray Trace details as](

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

### Combining Powertools utilities

You 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 payload contain a specific header
* Extract specific keys from event
* Add automatically security headers in response of all executions
* Validate if a feature flag is enabled
* Generate execution history and save to a DynamoDB table

=== "combining_powertools_utilities_function.py"
```python hl_lines="8 14 15 36"
--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"
```

=== "SAM TEMPLATE"
```python hl_lines="8 14 15 36"
--8<-- "examples/middleware_factory/sam/combining_powertools_utilities_template.yaml"
```

## Tips

* Use `trace_execution` to quickly understand the performance impact of your middlewares, and reduce or merge tasks when necessary
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 FOR HISTORIC
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

# APPCONFIG FOR FEATURE FLAGS
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,79 @@
{
"body":"None",
"headers":{
"Accept":"*/*",
"Accept-Encoding":"gzip, deflate, br",
"Connection":"keep-alive",
"Host":"127.0.0.1:3001",
"Postman-Token":"a9d49365-ebe1-4bb0-8627-d5e37cdce86d",
"User-Agent":"PostmanRuntime/7.29.0",
"X-Customer-Id":"1",
"X-Forwarded-Port":"3001",
"X-Forwarded-Proto":"http"
},
"httpMethod":"GET",
"isBase64Encoded":false,
"multiValueHeaders":{
"Accept":[
"*/*"
],
"Accept-Encoding":[
"gzip, deflate, br"
],
"Connection":[
"keep-alive"
],
"Host":[
"127.0.0.1:3001"
],
"Postman-Token":[
"a9d49365-ebe1-4bb0-8627-d5e37cdce86d"
],
"User-Agent":[
"PostmanRuntime/7.29.0"
],
"X-Customer-Id":[
"1"
],
"X-Forwarded-Port":[
"3001"
],
"X-Forwarded-Proto":[
"http"
]
},
"multiValueQueryStringParameters":"None",
"path":"/comments",
"pathParameters":"None",
"queryStringParameters":"None",
"requestContext":{
"accountId":"123456789012",
"apiId":"1234567890",
"domainName":"127.0.0.1:3001",
"extendedRequestId":"None",
"httpMethod":"GET",
"identity":{
"accountId":"None",
"apiKey":"None",
"caller":"None",
"cognitoAuthenticationProvider":"None",
"cognitoAuthenticationType":"None",
"cognitoIdentityPoolId":"None",
"sourceIp":"127.0.0.1",
"user":"None",
"userAgent":"Custom User Agent String",
"userArn":"None"
},
"path":"/comments",
"protocol":"HTTP/1.1",
"requestId":"56d1a102-6d9d-4f13-b4f7-26751c10a131",
"requestTime":"20/Aug/2022:18:18:58 +0000",
"requestTimeEpoch":1661019538,
"resourceId":"123456",
"resourcePath":"/comments",
"stage":"Prod"
},
"resource":"/comments",
"stageVariables":"None",
"version":"1.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import json
from typing import Callable

import boto3
import combining_powertools_utilities_schema as schemas
import requests

from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.exceptions import InternalServerError
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.shared.types import JSONType
from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags
from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate

app = APIGatewayRestResolver()
tracer = Tracer()
logger = Logger()

table_historic = boto3.resource("dynamodb").Table("HistoricTable")

app_config = AppConfigStore(environment="dev", application="comments", name="features")
feature_flags = FeatureFlags(store=app_config)


@lambda_handler_decorator(trace_execution=True)
def middleware_custom(handler: Callable, event: dict, context: LambdaContext):

# validating the INPUT with the given schema
# X-Customer-Id header must be informed in all requests
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.

Early return? I think I need other opinions here.

try:
validate(event=event, schema=schemas.INPUT)
except SchemaValidationError as e:
return {
"statusCode": 400,
"body": json.dumps(str(e)),
}

# extracting headers and requestContext from event
headers = extract_data_from_envelope(data=event, envelope="headers")
request_context = extract_data_from_envelope(data=event, envelope="requestContext")

logger.debug(f"X-Customer-Id => {headers.get('X-Customer-Id')}")
tracer.put_annotation(key="CustomerId", value=headers.get("X-Customer-Id"))

response = handler(event, context)

# automatically adding security headers to all responses
# see: https://securityheaders.com/
logger.info("Injecting security headers")
response["headers"]["Referrer-Policy"] = "no-referrer"
response["headers"]["Strict-Transport-Security"] = "max-age=15552000; includeSubDomains; preload"
response["headers"]["X-DNS-Prefetch-Control"] = "off"
response["headers"]["X-Content-Type-Options"] = "nosniff"
response["headers"]["X-Permitted-Cross-Domain-Policies"] = "none"
response["headers"]["X-Download-Options"] = "noopen"

logger.info("Saving api call in history table")
save_api_execution_history(str(event.get("path")), headers, request_context)

# return lambda execution
return response


@tracer.capture_method
def save_api_execution_history(path: str, headers: dict, request_context: dict) -> None:

try:
# using the feature flags utility to check if the new feature "save api call to history" is enabled by default
# see: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/feature_flags/#static-flags
save_history: JSONType = feature_flags.evaluate(name="save_history", default=False)
if save_history:
# saving history in dynamodb table
tracer.put_metadata(key="execution detail", value=request_context)
table_historic.put_item(
Item={
"customer_id": headers.get("X-Customer-Id"),
"request_id": request_context.get("requestId"),
"path": path,
"request_time": request_context.get("requestTime"),
"source_ip": request_context.get("identity", {}).get("sourceIp"),
"http_method": request_context.get("httpMethod"),
}
)

return None
except Exception:
# you can add more logic here to handle exceptions or even save this to a DLQ
# but not to make this example too long, we just return None since the Lambda has been successfully executed
return None


@app.get("/comments")
@tracer.capture_method
def get_comments():
try:
comments: requests.Response = requests.get("https://jsonplaceholder.typicode.com/comments")
comments.raise_for_status()

return {"comments": comments.json()[:10]}
except Exception as exc:
raise InternalServerError(str(exc))


@app.get("/comments/<comment_id>")
@tracer.capture_method
def get_comments_by_id(comment_id: str):
try:
comments: requests.Response = requests.get(f"https://jsonplaceholder.typicode.com/comments/{comment_id}")
comments.raise_for_status()

return {"comments": comments.json()}
except Exception as exc:
raise InternalServerError(str(exc))


@middleware_custom
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
Loading