Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This repo contains AWS CDK implementation for the patterns at [Serverless Land :
| [Orchestration to Choreography](patterns/orchestration_%20to_choreography.md) | Convert the collaboration pattern from orchestration to choreography |
| [Replace Parallel with SNS Scatter-Gather](patterns/parallel_to_sns_scatter_gather.md) | Instead of making parallel invocations from a StepFunctions `Parallel` step, send a message to SNS |
|[Extract Message Filter](patterns/extract-message-filter.md)|Instead of conditional statements at the consumer, eliminate unwanted messages with Amazon EventBridge.|
| [Replace Amazon Event Bridge Event Patterns with Lambda filter](patterns/replace_event_pattern_with_lambda.md) | Replace Event Bridge Event Pattern with Lambda filter for complex filtering logic |



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<mxfile host="Electron" modified="2023-04-04T12:13:16.628Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.1.2 Chrome/106.0.5249.199 Electron/21.4.3 Safari/537.36" etag="hVVfXEn3OkKLpiybQ7ZJ" version="21.1.2" type="device">
<diagram name="Before_and_After" id="-HM-OosM7QlpeyuSYqsR">
<mxGraphModel dx="1306" dy="894" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="jjvaqmho2UHqBmuWb2lH-7" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="130" y="80" width="350" height="240" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-1" value="" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;gradientColor=#FF4F8B;gradientDirection=north;fillColor=#BC1356;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.eventbridge;" vertex="1" parent="1">
<mxGeometry x="130" y="80" width="38" height="38" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-2" target="jjvaqmho2UHqBmuWb2lH-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-2" value="Rule1" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#B0084D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.rule_3;" vertex="1" parent="1">
<mxGeometry x="336" y="100" width="78" height="68" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-3" target="jjvaqmho2UHqBmuWb2lH-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-3" target="jjvaqmho2UHqBmuWb2lH-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-3" value="EventBus" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#B0084D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.eventbridge_default_event_bus_resource;" vertex="1" parent="1">
<mxGeometry x="190" y="160" width="78" height="53" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-4" value="Rule1 Handler" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#D45B07;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.lambda_function;" vertex="1" parent="1">
<mxGeometry x="530" y="110" width="48" height="48" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-5" target="jjvaqmho2UHqBmuWb2lH-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-5" value="Rule2" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#B0084D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.rule_3;" vertex="1" parent="1">
<mxGeometry x="336" y="210" width="78" height="68" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-6" value="Rule2 Handler" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#D45B07;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.lambda_function;" vertex="1" parent="1">
<mxGeometry x="530" y="220" width="48" height="48" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-12" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="130" y="420" width="350" height="240" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-13" value="" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;gradientColor=#FF4F8B;gradientDirection=north;fillColor=#BC1356;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.eventbridge;" vertex="1" parent="1">
<mxGeometry x="130" y="420" width="38" height="38" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-15" target="jjvaqmho2UHqBmuWb2lH-19">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-15" value="Rule(no logic)" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#B0084D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.rule_3;" vertex="1" parent="1">
<mxGeometry x="336" y="492.5" width="78" height="68" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-18" target="jjvaqmho2UHqBmuWb2lH-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-18" value="EventBus" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#B0084D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.eventbridge_default_event_bus_resource;" vertex="1" parent="1">
<mxGeometry x="190" y="500" width="78" height="53" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-24" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-19" target="jjvaqmho2UHqBmuWb2lH-23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-25" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="jjvaqmho2UHqBmuWb2lH-19" target="jjvaqmho2UHqBmuWb2lH-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-19" value="Filter Function&lt;br&gt;(logic)" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#D45B07;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.lambda_function;" vertex="1" parent="1">
<mxGeometry x="520" y="502.5" width="48" height="48" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-22" value="Rule2 Handler" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#D45B07;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.lambda_function;" vertex="1" parent="1">
<mxGeometry x="670" y="580" width="48" height="48" as="geometry" />
</mxCell>
<mxCell id="jjvaqmho2UHqBmuWb2lH-23" value="Rule1 Handler" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#D45B07;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.lambda_function;" vertex="1" parent="1">
<mxGeometry x="670" y="420" width="48" height="48" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
8 changes: 8 additions & 0 deletions implementation/replace-eventpattern-with-lambda/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions implementation/replace-eventpattern-with-lambda/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
224 changes: 224 additions & 0 deletions implementation/replace-eventpattern-with-lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Replace Amazon Event Bridge Event Patterns with Lambda filter

This project is the CDK implementation of ['Replace Amazon Event Bridge Event Patterns with Lambda filter'](https://serverlessland.com/refactoring-serverless/replace-eventpattern-with-lambda) pattern. It shows how can you use Lambda in CDK to filter and route messages to target service(s) to process the event instead of Amazon EventBridge's event pattern matching when complex filtering logic or data manipulation may be required before forwarding the events to the target service(s).

## How it works

The provided example mimics an e-commerce application where Event Bridge is used as destination of user review messages. In our example we would like to route based on product rating, number of historic purchased items.

Example JSON event message:

``` Json
{
"source": "ecommerce.application",
"detail-type": "user.review",
"detail": {
"productId": "12345",
"rating": 4.5,
"reviewText": "Great product!",
"reviewer": {
"userId": "67890",
"email": "bob.buyer@example.com",
"signupDate": "2023-04-01T00:00:00Z",
"purchaseHistory": [
{
"orderId": "10001",
"orderDate": "2023-04-01T02:30:00Z",
"items": [{"productId": "12345", "quantity": 1}]
},
{
"orderId": "10002",
"orderDate": "2023-07-01T02:30:00Z",
"items": [{"productId": "12981920", "quantity": 2}]
},
{
"orderId": "10003",
"orderDate": "2023-08-01T02:30:00Z",
"items": [{"productId": "55667788", "quantity": 3}]
}
]
}
}
}
```

The provided code will deploy the following two stacks:

* OriginalReplaceEventpatternWithLambdaStack
* RefactoredReplaceEventpatternWithLambdaStack

Both stack deploy two lambda functions: [Original|Refactored]EventDestinationFunction1 and [Original|Refactored]EventDestinationFunction2.

OriginalReplaceEventpatternWithLambdaStack in addition deploys two Event Bridge Rules Patterns:

``` typescript
const eventPatternFilterFunc1: events.EventPattern = {
source: ['ecommerce.application'],
detailType: ['user.review'],
detail: {
rating: [
{
numeric: ['>=', 4.5],
},
],
},
};

const eventPatternFilterFunc2: events.EventPattern = {
source: ['ecommerce.application'],
detailType: ['user.review'],
detail: {
rating: [
{
numeric: ['<', 2],
},
],
},
};
```

RefactoredReplaceEventpatternWithLambdaStack deploys a simple event pattern and a lambda function `EventFilterFunction` that executes complex filtering and routing.

Event Pattern:

``` typescript
const eventPatternPass: events.EventPattern = {
source: ['ecommerce.application'],
detailType: ['user.review'],
};
```

Filter logic in the lambda function:

``` Python
def process_review_event(event):
rating = event['detail']['rating']
purchase_history = event['detail']['reviewer']['purchaseHistory']
total_items_purchased = sum(item['quantity'] for order in purchase_history for item in order['items'])

if rating >= 4.5 and total_items_purchased <= 10:
lambda_client.invoke(FunctionName=LAMBDA_DEST_FUNCTION_1, Payload=json.dumps(event))
logging.info(f"rating >= 4.5 and total_items_purchased({total_items_purchased}) <= 10 -> invoking {LAMBDA_DEST_FUNCTION_1}")
elif rating < 2 and total_items_purchased > 10:
lambda_client.invoke(FunctionName=LAMBDA_DEST_FUNCTION_2, Payload=json.dumps(event))
logging.info(f"rating < 2 and total_items_purchased({total_items_purchased}) > 10 -> invoking {LAMBDA_DEST_FUNCTION_2}")
```

## Deploy the infrastructure

To build this app, navigate to `implementation/replace-eventpattern-with-lambda` folder. Then run the following:

```bash
npm install -g aws-cdk
npm install
npm run build
```

This will install the necessary CDK, dependencies, build your TypeScript files and CloudFormation template.

Next, deploy the 2 Stacks to your AWS Account.

``` bash
cdk deploy --all
```

## Testing it out

- First lets send a message to the `default` event bus executing the following command:

``` bash
aws events put-events --entries file://example_event.json
```

You should see the following:

``` Json
{
"FailedEntryCount": 0,
"Entries": [
{
"EventId": "fdb55454-114c-3642-6a55-fb960ba0dcbf"
}
]
}
```

## Verify

``` bash
aws logs describe-log-groups --query 'logGroups[?ends_with(logGroupName, `ScatterGatherWithSNSStack-refactorlambdaaggregator-nfXGleA7rZsh`)].logGroupName' --output text
```

Use the log group name from the previous command to get the logs of the aggregator function and filter by "quotes". Please replace your log group name in the following command.

``` bash
aws logs tail /aws/lambda/ScatterGatherWithSNSStack-refactorlambdaaggregator-nfXGleA7rZsh --filter-pattern "quotes"
```

The two stacks provisioned the following Lambda functions:

* ```EventFilterFunction```
* ```RefactoredEventDestinationFunction1```
* ```RefactoredEventDestinationFunction2```
* ```OriginalEventDestinationFunction2```
* ```OriginalEventDestinationFunction1```

Let's inspect the logs of the ```EventFilterFunction```, ```RefactoredEventDestinationFunction1``` and ```OriginalEventDestinationFunction1```.

For each of the use cases we will first get the lates log stream of the and then get the log events using the aws cli.

EventFilterFunction:

``` Bash
stream_name=$(aws logs describe-log-streams --log-group-name /aws/lambda/EventFilterFunction --order-by LastEventTime --descending --limit 1 | jq -r '.logStreams[0].logStreamName')
aws logs get-log-events --log-group-name /aws/lambda/EventFilterFunction --log-stream-name $stream_name | jq '.events | map(select(.message | contains("[INFO]")))'
```

You should see:

``` Json
[
{
"timestamp": 1691161162130,
"message": "[INFO]\t2023-08-04T14:59:22.129Z\tf5366ef3-2a31-4b49-8218-e1b4c4abfc7a\trating >= 4.5 and total_items_purchased(6) <= 10 -> invoking RefactoredEventDestinationFunction1\n",
"ingestionTime": 1691161162889
}
]
```

RefactoredEventDestinationFunction1 and OriginalEventDestinationFunction1:

``` Bash
stream_name=$(aws logs describe-log-streams --log-group-name /aws/lambda/OriginalEventDestinationFunction1 --order-by LastEventTime --descending --limit 1 | jq -r '.logStreams[0].logStreamName')
aws logs get-log-events --log-group-name /aws/lambda/OriginalEventDestinationFunction1 --log-stream-name $stream_name | jq '.events | map(select(.message | contains("[INFO]")))'
```

and

``` Bash
stream_name=$(aws logs describe-log-streams --log-group-name /aws/lambda/RefactoredEventDestinationFunction1 --order-by LastEventTime --descending --limit 1 | jq -r '.logStreams[0].logStreamName')
aws logs get-log-events --log-group-name /aws/lambda/RefactoredEventDestinationFunction1 --log-stream-name $stream_name | jq '.events | map(select(.message | contains("[INFO]")))'
```

You should get:

``` Json
[
{
"timestamp": 1691158134746,
"message": "[INFO]\t2023-08-04T14:08:54.746Z\t0115f9ac-f35a-4b33-91cd-7d9aebb401ff\tHello from Rule1 handler function\n",
"ingestionTime": 1691158137055
},
{
"timestamp": 1691158134746,
"message": "[INFO]\t2023-08-04T14:08:54.746Z\t0115f9ac-f35a-4b33-91cd-7d9aebb401ff\tReceived Event: {\"version\": \"0\", \"id\": \"e15f7622-3c9d-1186-a3a3-736f84aeb58b\", \"detail-type\": \"user.review\", \"source\": \"ecommerce.application\", \"account\": \"446084752553\", \"time\": \"2023-08-04T14:08:53Z\", \"region\": \"us-east-1\", \"resources\": [], \"detail\": {\"productId\": \"12345\", \"rating\": 4.5, \"reviewText\": \"Great product!\", \"reviewer\": {\"userId\": \"67890\", \"email\": \"example@example.com\", \"signupDate\": \"2023-04-01T00:00:00Z\", \"purchaseHistory\": [{\"orderId\": \"10001\", \"orderDate\": \"2023-04-01T02:30:00Z\", \"items\": [{\"productId\": \"12345\", \"quantity\": 1}]}]}}}\n",
"ingestionTime": 1691158137055
}
]
```

## Cleanup

``` bash
cdk destroy --all
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { OriginalReplaceEventpatternWithLambdaStack } from '../lib/original-replace-eventpattern-with-lambda-stack';
import { RefactoredReplaceEventpatternWithLambdaStack } from '../lib/refactored-replace-eventpattern-with-lambda-stack';

const app = new cdk.App();
new OriginalReplaceEventpatternWithLambdaStack(app, 'OriginalReplaceEventpatternWithLambdaStack', {});
new RefactoredReplaceEventpatternWithLambdaStack(app, 'RefactoredReplaceEventpatternWithLambdaStack', {});
Loading