AWSTemplateFormatVersion: "2010-09-09" Transform: 'AWS::Serverless-2016-10-31' Parameters: Environment: Type: String Default: dev Description: Environment name LogLevel: Type: String Default: INFO RetentionInDays: Type: Number Default: 30 Description: CloudWatch Logs retention period for Lambda functions EventBusName: Type: AWS::SSM::Parameter::Value<String> Description: EventBridge Event Bus Name Payment3PApiUrl: Type: AWS::SSM::Parameter::Value<String> Description: 3rd Party Payment API Gateway URL Globals: Function: Runtime: python3.9 Architectures: - arm64 Handler: main.handler Timeout: 30 Tracing: Active Environment: Variables: ENVIRONMENT: !Ref Environment TABLE_NAME: !Ref Table API_URL: !Ref Payment3PApiUrl POWERTOOLS_SERVICE_NAME: payment POWERTOOLS_TRACE_DISABLED: "false" LOG_LEVEL: !Ref LogLevel Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension-Arm64:1" Resources: ######### # TABLE # ######### Table: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: orderId AttributeType: S BillingMode: PAY_PER_REQUEST KeySchema: - AttributeName: orderId KeyType: HASH TableParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /ecommerce/${Environment}/payment/table/name Type: String Value: !Ref Table ############# # FUNCTIONS # ############# OnCompletedFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/on_completed/ Events: DeliveryCompleted: Type: CloudWatchEvent Properties: EventBusName: !Ref EventBusName Pattern: source: [ecommerce.delivery] detail-type: - DeliveryCompleted EventInvokeConfig: DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt DeadLetterQueue.Outputs.QueueArn Policies: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:DeleteItem - dynamodb:GetItem Resource: - !GetAtt Table.Arn OnCompletedLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${OnCompletedFunction}" RetentionInDays: !Ref RetentionInDays OnCreatedFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/on_created/ Events: OrderCreated: Type: CloudWatchEvent Properties: EventBusName: !Ref EventBusName Pattern: source: [ecommerce.orders] detail-type: - OrderCreated EventInvokeConfig: DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt DeadLetterQueue.Outputs.QueueArn Policies: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - Version: "2012-10-17" Statement: - Effect: Allow Action: dynamodb:PutItem Resource: - !GetAtt Table.Arn OnCreatedLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${OnCreatedFunction}" RetentionInDays: !Ref RetentionInDays OnFailedFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/on_failed/ Events: PackagingFailed: Type: CloudWatchEvent Properties: EventBusName: !Ref EventBusName Pattern: source: [ecommerce.warehouse] detail-type: - PackagingFailed DeliveryFailed: Type: CloudWatchEvent Properties: EventBusName: !Ref EventBusName Pattern: source: [ecommerce.delivery] detail-type: - DeliveryFailed EventInvokeConfig: DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt DeadLetterQueue.Outputs.QueueArn Policies: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:DeleteItem - dynamodb:GetItem Resource: - !GetAtt Table.Arn OnFailedLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${OnFailedFunction}" RetentionInDays: !Ref RetentionInDays OnModifiedFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/on_modified/ Events: OrderModified: Type: CloudWatchEvent Properties: EventBusName: !Ref EventBusName Pattern: source: [ecommerce.orders] detail-type: - OrderModified detail: changed: - total EventInvokeConfig: DestinationConfig: OnFailure: Type: SQS Destination: !GetAtt DeadLetterQueue.Outputs.QueueArn Policies: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - Version: "2012-10-17" Statement: - Effect: Allow Action: dynamodb:GetItem Resource: - !GetAtt Table.Arn OnModifiedLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${OnModifiedFunction}" RetentionInDays: !Ref RetentionInDays ValidateFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/validate/ Events: BackendApi: Type: Api Properties: Path: /backend/validate Method: POST RestApiId: !Ref Api Policies: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy ValidateLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ValidateFunction}" RetentionInDays: !Ref RetentionInDays ############### # API GATEWAY # ############### Api: Type: AWS::Serverless::Api Properties: DefinitionBody: Fn::Transform: Name: "AWS::Include" Parameters: Location: "resources/openapi.yaml" EndpointConfiguration: REGIONAL StageName: prod TracingEnabled: true ApiUrlParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /ecommerce/${Environment}/payment/api/url Type: String Value: !Sub "https://${Api}.execute-api.${AWS::Region}.amazonaws.com/prod" ApiArnParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub /ecommerce/${Environment}/payment/api/arn Type: String Value: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${Api}/prod" ##################### # DEAD LETTER QUEUE # ##################### DeadLetterQueue: Type: AWS::CloudFormation::Stack Properties: # The path starts with '../..' as this will be evaluated from the # payment/build folder, not the payment folder. TemplateURL: ../../shared/templates/dlq.yaml ############# # DASHBOARD # ############# Dashboard: Type: AWS::CloudWatch::Dashboard Properties: DashboardName: !Ref AWS::StackName DashboardBody: !Sub | { "start": "-PT6H", "periodOverride": "inherit", "widgets": [ { "type": "text", "x": 0, "y": 0, "width": 21, "height": 1, "properties": { "markdown": "\n# Key Metrics\n" } }, { "type": "text", "x": 21, "y": 0, "width": 3, "height": 7, "properties": { "markdown": "\n## Useful links\n\n__CloudWatch Logs Insights queries__:\n\n* [Last 100 warning messages](https://${AWS::Region}.console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#logs-insights:queryDetail=~\\(end~0~start~-21600~timeType~'RELATIVE~unit~'seconds~editorString~'fields*20timestamp*2c*20level*2c*20message.orderId*20as*20orderId*2c*20message.paymentToken*20as*20paymentToken*2c*20message.message*20as*20message*0a*7c*20filter*20level*20*3d*3d*20*22WARNING*22*0a*7c*20sort*20*40timestamp*20desc*0a*7c*20limit*2020*0a~isLiveTail~false~queryId~'38d167e2-e680-4dc3-80fc-411dbd93a317~source~(~'*2faws*2flambda*2f${OnCreatedFunction}~'*2faws*2flambda*2f${OnModifiedFunction}~'*2faws*2flambda*2f${OnFailedFunction}~'*2faws*2flambda*2f${OnCompletedFunction}~'*2faws*2flambda*2f${ValidateFunction})\\))\n\n__Other links__:\n\n* [Repository](https://github.com/aws-samples/aws-serverless-ecommerce-platform/tree/main/payment)\n" } }, { "type": "metric", "x": 0, "y": 1, "width": 8, "height": 6, "properties": { "metrics": [ ["ecommerce.payment", "amountWon", "environment", "${Environment}", "service", "payment", {"color": "#66bb6a", "label": "Amount Processed ($)"}], ["ecommerce.payment", "amountLost", "environment", "${Environment}", "service", "payment", {"color": "#d62728", "label": "Amount Cancelled ($)"}], ["ecommerce.payment", "paymentProcessed", "environment", "${Environment}", "service", "payment", {"color": "#dbdb8d", "label": "Payments Processed"}], ["ecommerce.payment", "paymentCancelled", "environment", "${Environment}", "service", "payment", {"color": "#ff7f0e", "label": "Payments Cancelled"}], ["ecommerce.payment", "paymentCreated", "environment", "${Environment}", "service", "payment", {"color": "#29b6f6", "label": "Payments Created"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Payments" } }, { "type": "metric", "x": 8, "y": 1, "width": 7, "height": 6, "properties": { "metrics": [ [{"expression": "le1+le2+le3+le4+le5+lt1+lt2+lt3+lt4+lt5", "label": "Lambda Errors", "color": "#66bb6a"}], [{"expression": "ag1", "label": "API Gateway 5XX", "color": "#ef5350"}], [{"expression": "db1+db2", "label": "DynamoDB Errors", "color": "#ffa726"}], ["AWS/Lambda", "Errors", "FunctionName", "${OnCreatedFunction}", {"id": "le1", "visible": false}], ["AWS/Lambda", "Errors", "FunctionName", "${OnCompletedFunction}", {"id": "le2", "visible": false}], ["AWS/Lambda", "Errors", "FunctionName", "${OnModifiedFunction}", {"id": "le3", "visible": false}], ["AWS/Lambda", "Errors", "FunctionName", "${OnFailedFunction}", {"id": "le4", "visible": false}], ["AWS/Lambda", "Errors", "FunctionName", "${ValidateFunction}", {"id": "le5", "visible": false}], ["AWS/Lambda", "Throttles", "FunctionName", "${OnCreatedFunction}", {"id": "lt1", "visible": false}], ["AWS/Lambda", "Throttles", "FunctionName", "${OnCompletedFunction}", {"id": "lt2", "visible": false}], ["AWS/Lambda", "Throttles", "FunctionName", "${OnModifiedFunction}", {"id": "lt3", "visible": false}], ["AWS/Lambda", "Throttles", "FunctionName", "${OnFailedFunction}", {"id": "lt4", "visible": false}], ["AWS/Lambda", "Throttles", "FunctionName", "${ValidateFunction}", {"id": "lt5", "visible": false}], ["AWS/ApiGateway", "5XXError", "ApiName", "${AWS::StackName}-api", {"id": "ag1", "visible": false}], ["AWS/DynamoDB", "UserErrors", "TableName", "${Table}", {"id": "db1", "visible": false}], ["AWS/DynamoDB", "SystemErrors", "TableName", "${Table}", {"id": "db2", "visible": false}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Errors" } }, { "type": "metric", "x": 15, "y": 1, "width": 6, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${ValidateFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "Validate Latency" } }, { "type": "text", "x": 0, "y": 7, "width": 24, "height": 1, "properties": { "markdown": "\n# Traffic\n" } }, { "type": "metric", "x": 0, "y": 8, "width": 12, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Invocations", "FunctionName", "${OnCreatedFunction}", {"color": "#29b6f6", "label": "CreatePayment"}], ["AWS/Lambda", "Invocations", "FunctionName", "${OnCompletedFunction}", {"color": "#66bb6a", "label": "CompletePayment"}], ["AWS/Lambda", "Invocations", "FunctionName", "${OnFailedFunction}", {"color": "#ec407a", "label": "CancelPayment"}], ["AWS/Lambda", "Invocations", "FunctionName", "${OnModifiedFunction}", {"color": "#b300b3", "label": "ModifyPayment"}], ["AWS/Lambda", "Invocations", "FunctionName", "${ValidateFunction}", {"color": "#ffa726", "label": "ValidatePayment"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Requests" } }, { "type": "metric", "x": 12, "y": 8, "width": 12, "height": 6, "properties": { "metrics": [ ["ecommerce.payment", "paymentCreated", "environment", "${Environment}", "service", "payment", {"color": "#29b6f6", "label": "Payment Created"}], ["ecommerce.payment", "paymentProcessed", "environment", "${Environment}", "service", "payment", {"color": "#66bb6a", "label": "Payment Processed"}], ["ecommerce.payment", "paymentCancelled", "environment", "${Environment}", "service", "payment", {"color": "#ef5350", "label": "Payment Cancelled"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Payment counts" } }, { "type": "metric", "x": 0, "y": 14, "width": 24, "height": 6, "properties": { "metrics": [ ["ecommerce.payment", "amountWon", "environment", "${Environment}", "service", "payment", {"color": "#66bb6a", "label": "Amount Processed ($)"}], ["ecommerce.payment", "amountLost", "environment", "${Environment}", "service", "payment", {"color": "#ef5350", "label": "Amount Cancelled ($)"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Payment amounts" } }, { "type": "text", "x": 0, "y": 20, "width": 24, "height": 1, "properties": { "markdown": "\n# Latency and Duration\n" } }, { "type": "metric", "x": 0, "y": 21, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnCreatedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "CreatePayment Duration" } }, { "type": "metric", "x": 8, "y": 21, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnCompletedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "CompletePayment Duration" } }, { "type": "metric", "x": 16, "y": 21, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnFailedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "CancelPayment Duration" } }, { "type": "metric", "x": 0, "y": 27, "width": 12, "height": 6, "properties": { "metrics": [ ["AWS/ApiGateway", "Latency", "ApiName", "${AWS::StackName}-api", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "API Latency" } }, { "type": "metric", "x": 12, "y": 27, "width": 12, "height": 6, "properties": { "metrics": [ ["AWS/DynamoDB", "SuccessfulRequestLatency", "TableName", "${Table}", "Operation", "GetItem", {"color": "#66bb6a", "label": "GetItem"}], ["AWS/DynamoDB", "SuccessfulRequestLatency", "TableName", "${Table}", "Operation", "PutItem", {"color": "#ffa726", "label": "PutItem"}], ["AWS/DynamoDB", "SuccessfulRequestLatency", "TableName", "${Table}", "Operation", "UpdateItem", {"color": "#29b6f6", "label": "UpdateItem"}], ["AWS/DynamoDB", "SuccessfulRequestLatency", "TableName", "${Table}", "Operation", "DeleteItem", {"color": "#ef5350", "label": "DeleteItem"}], ["AWS/DynamoDB", "SuccessfulRequestLatency", "TableName", "${Table}", "Operation", "Query", {"color": "#ec407a", "label": "Query"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Average", "region": "${AWS::Region}", "title": "DynamoDB Latency" } }, { "type": "metric", "x": 0, "y": 33, "width": 12, "height": 3, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnCreatedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "OnCreated Latency" } }, { "type": "metric", "x": 12, "y": 33, "width": 12, "height": 3, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnCompletedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "OnCompleted Latency" } }, { "type": "metric", "x": 0, "y": 36, "width": 12, "height": 3, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnModifiedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "OnModified Latency" } }, { "type": "metric", "x": 12, "y": 36, "width": 12, "height": 3, "properties": { "metrics": [ ["AWS/Lambda", "Duration", "FunctionName", "${OnFailedFunction}", {"color": "#9ccc65", "label": "p50"}], ["...", {"stat": "p90", "color": "#ffee58", "label": "p90"}], ["...", {"stat": "p99", "color": "#ef5350", "label": "p99"}] ], "view": "singleValue", "period": 86400, "stacked": false, "stat": "p50", "region": "${AWS::Region}", "title": "OnFailed Latency" } }, { "type": "text", "x": 0, "y": 39, "width": 24, "height": 1, "properties": { "markdown": "\n# Errors\n" } }, { "type": "metric", "x": 0, "y": 40, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/Lambda", "Errors", "FunctionName", "${OnCreatedFunction}", {"color": "#29b6f6", "label": "CreatePayment"}], ["AWS/Lambda", "Errors", "FunctionName", "${OnCompletedFunction}", {"color": "#66bb6a", "label": "CompletePayment"}], ["AWS/Lambda", "Errors", "FunctionName", "${OnFailedFunction}", {"color": "#ec407a", "label": "CancelPayment"}], ["AWS/Lambda", "Errors", "FunctionName", "${OnModifiedFunction}", {"color": "#b300b3", "label": "ModifyPayment"}], ["AWS/Lambda", "Errors", "FunctionName", "${ValidateFunction}", {"color": "#ffa726", "label": "ValidatePayment"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "Lambda Errors" } }, { "type": "metric", "x": 8, "y": 40, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/ApiGateway", "4XXError", "ApiName", "${AWS::StackName}-api", {"color": "#ffa726", "label": "4XX Errors"}], ["AWS/ApiGateway", "5XXError", "ApiName", "${AWS::StackName}-api", {"color": "#ef5350", "label": "5XX Errors"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "API Errors" } }, { "type": "metric", "x": 16, "y": 40, "width": 8, "height": 6, "properties": { "metrics": [ ["AWS/DynamoDB", "UserErrors", "TableName", "${Table}", {"color": "#ffa726", "label": "User Errors"}], ["AWS/DynamoDB", "SystemErrors", "TableName", "${Table}", {"color": "#ef5350", "label": "System Errors"}] ], "view": "timeSeries", "period": 60, "stacked": false, "stat": "Sum", "region": "${AWS::Region}", "title": "DynamoDB Errors" } }, { "type": "text", "x": 0, "y": 46, "width": 24, "height": 1, "properties": { "markdown": "\n# Logs\n" } }, { "type": "log", "x": 0, "y": 47, "width": 24, "height": 6, "properties": { "query": "SOURCE '/aws/lambda/${OnCreatedFunction}' | SOURCE '/aws/lambda/${OnCompletedFunction}' | SOURCE '/aws/lambda/${OnFailedFunction}' | SOURCE '/aws/lambda/${OnModifiedFunction}' | SOURCE '/aws/lambda/${ValidateFunction}' | fields timestamp, level, message.orderId as orderId, message.message as message, message.paymentToken as paymentToken \n| PARSE @message \"[*] *\" as loggingLevel, loggingMessage \n| filter (loggingLevel == \"ERROR\" or level == \"ERROR\")\n| sort @timestamp desc\n| limit 20\n", "region": "${AWS::Region}", "stacked": false, "view": "table", "title": "Last 20 errors" } } ] }