Repository: theburningmonk/manning-aws-lambda-in-motion
Branch: master
Commit: eab98a4e1824
Files: 50
Total size: 118.3 KB
Directory structure:
gitextract_0qv7lc5k/
├── .gitignore
├── .vscode/
│ └── launch.json
├── README.md
├── build.sh
├── buildspec.yml
├── examples/
│ ├── create-alarm.json
│ ├── get-index.json
│ ├── notify-restaurant.json
│ ├── place-order.json
│ ├── retry-notify-restaurant.json
│ └── search-restaurants.json
├── functions/
│ ├── accept-order.js
│ ├── create-alarms.js
│ ├── fulfill-order.js
│ ├── get-index.js
│ ├── get-restaurants.js
│ ├── notify-restaurant.js
│ ├── notify-user.js
│ ├── place-order.js
│ ├── retry-notify-restaurant.js
│ ├── retry-notify-user.js
│ └── search-restaurants.js
├── lib/
│ ├── aws4.js
│ ├── awscred.js
│ ├── cloudwatch.js
│ ├── correlation-ids.js
│ ├── http.js
│ ├── kinesis.js
│ ├── log.js
│ ├── lru.js
│ ├── notify.js
│ ├── retry.js
│ └── sns.js
├── middleware/
│ ├── capture-correlation-ids.js
│ ├── flush-metrics.js
│ ├── function-shield.js
│ ├── sample-logging.js
│ └── wrapper.js
├── package.json
├── seed-restaurants.js
├── serverless.yml
├── static/
│ └── index.html
├── template.yml
└── tests/
├── steps/
│ ├── given.js
│ ├── init.js
│ ├── tearDown.js
│ └── when.js
└── test_cases/
├── get-index.js
├── get-restaurants.js
└── search-restaurants.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# package directories
node_modules
jspm_packages
# Serverless directories
.serverless
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to SAM Local",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 5858,
"localRoot": "${workspaceRoot}",
"remoteRoot": "/var/task"
},
{
"type": "node",
"request": "launch",
"name": "get-index",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"get-index",
"-p",
"examples/get-index.json"
],
"env": {
"restaurants_api": "https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/restaurants",
"cognito_user_pool_id": "us-east-1_DfuAwa0vB",
"cognito_client_id": "49lunjf7j7vsgmq9lhtfn4q7ma",
"SLS_DEBUG": "*",
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
}
},
{
"type": "node",
"request": "launch",
"name": "get-restaurants",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"get-restaurants",
"-d",
"{}"
],
"env": {
"restaurants_table": "restaurants",
"SLS_DEBUG": "*",
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
}
},
{
"type": "node",
"request": "launch",
"name": "search-restaurants",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"search-restaurants",
"-p",
"examples/search-restaurants.json"
],
"env": {
"restaurants_table": "restaurants",
"SLS_DEBUG": "*",
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
}
},
{
"type": "node",
"request": "launch",
"name": "create-alarm",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"auto-create-api-alarms",
"-p",
"examples/create-alarm.json"
]
},
{
"type": "node",
"request": "launch",
"name": "notify-restaurant",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"notify-restaurant",
"-p",
"examples/notify-restaurant.json"
],
"env": {
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
}
},
{
"type": "node",
"request": "launch",
"name": "place-order",
"program": "${workspaceFolder}/node_modules/.bin/sls",
"args": [
"invoke",
"local",
"-f",
"place-order",
"-p",
"examples/place-order.json"
],
"env": {
"AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
}
},
{
"type": "node",
"request": "launch",
"name": "integration tests",
"program": "${workspaceFolder}/node_modules/.bin/mocha",
"env": {
"TEST_MODE": "handler"
},
"args": [
"tests/test_cases",
"--reporter",
"spec"
]
},
{
"type": "node",
"request": "launch",
"name": "acceptance tests",
"program": "${workspaceFolder}/node_modules/.bin/mocha",
"env": {
"TEST_MODE": "http",
"TEST_ROOT": "https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/"
},
"args": [
"tests/test_cases",
"-t",
"2000",
"--reporter",
"spec"
]
},
]
}
================================================
FILE: README.md
================================================
# manning-aws-lambda-operational-patterns-and-practices
Code for the Manning video course "AWS Lambda: Operational Patterns and Practices"
================================================
FILE: build.sh
================================================
#!/bin/bash
set -e
set -o pipefail
instruction()
{
echo "usage: ./build.sh deploy <env>"
echo ""
echo "env: eg. int, staging, prod, ..."
echo ""
echo "for example: ./deploy.sh int"
}
if [ $# -eq 0 ]; then
instruction
exit 1
elif [ "$1" = "int-test" ] && [ $# -eq 1 ]; then
npm install
npm run integration-test
elif [ "$1" = "acceptance-test" ] && [ $# -eq 1 ]; then
npm install
npm run acceptance-test
elif [ "$1" = "deploy" ] && [ $# -eq 3 ]; then
STAGE=$2
REGION=$3
npm install
'node_modules/.bin/sls' deploy -s $STAGE -r $REGION
else
instruction
exit 1
fi
================================================
FILE: buildspec.yml
================================================
version: 0.2
phases:
build:
commands:
- chmod +x build.sh
- ./build.sh int-test
- ./build.sh deploy dev
- ./build.sh acceptance-test
================================================
FILE: examples/create-alarm.json
================================================
{
"version": "0",
"id": "dee9a69c-8166-1ad7-41d4-1dad201e29f6",
"detail-type": "AWS API Call via CloudTrail",
"source": "aws.apigateway",
"account": "374852340823",
"time": "2018-04-09T00:17:47Z",
"region": "us-east-1",
"resources": [],
"detail": {
"eventVersion": "1.05",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAIRMUZZEGPO27IPFYW",
"arn": "arn:aws:iam::374852340823:user/yan.cui",
"accountId": "374852340823",
"accessKeyId": "ASIAJNZDKN26DXPZFYQA",
"userName": "yan.cui",
"sessionContext": {
"attributes": {
"mfaAuthenticated": "false",
"creationDate": "2018-04-09T00:17:30Z"
}
},
"invokedBy": "cloudformation.amazonaws.com"
},
"eventTime": "2018-04-09T00:17:47Z",
"eventSource": "apigateway.amazonaws.com",
"eventName": "CreateDeployment",
"awsRegion": "us-east-1",
"sourceIPAddress": "cloudformation.amazonaws.com",
"userAgent": "cloudformation.amazonaws.com",
"requestParameters": {
"restApiId": "8kbasri6v6",
"createDeploymentInput": {
"stageName": "dev"
},
"template": false
},
"responseElements": {
"id": "cj2y0f",
"createdDate": "Apr 9, 2018 12:17:47 AM",
"deploymentUpdate": {
"restApiId": "8kbasri6v6",
"deploymentId": "cj2y0f",
"template": false
},
"deploymentStages": {
"deploymentId": "cj2y0f",
"restApiId": "8kbasri6v6",
"template": false,
"templateSkipList": [
"position"
]
},
"deploymentDelete": {
"deploymentId": "cj2y0f",
"restApiId": "8kbasri6v6",
"template": false
},
"self": {
"deploymentId": "cj2y0f",
"restApiId": "8kbasri6v6",
"template": false
}
},
"requestID": "6e25bd56-3b8b-11e8-a351-e5e3d3161fe7",
"eventID": "a150d941-7a54-4572-97b2-0614a81fd25b",
"readOnly": false,
"eventType": "AwsApiCall"
}
}
================================================
FILE: examples/get-index.json
================================================
{
"body": null,
"resource": "/",
"requestContext": {
"resourceId": "123456",
"apiId": "1234567890",
"resourcePath": "/",
"httpMethod": "POST",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"accountId": "123456789012",
"identity": {
"apiKey": null,
"userArn": null,
"cognitoAuthenticationType": null,
"caller": null,
"userAgent": "Custom User Agent String",
"user": null,
"cognitoIdentityPoolId": null,
"cognitoIdentityId": null,
"cognitoAuthenticationProvider": null,
"sourceIp": "127.0.0.1",
"accountId": null
},
"authorizer": {
"claims": {
"sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
"email_verified": "true",
"iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
"cognito:username": "theburningmonk",
"given_name": "Yan",
"aud": "1tf8pb1s1n53p7u608e7ic5ih8",
"event_id": "e727e68e-1424-11e8-be95-8774da63618a",
"token_use": "id",
"auth_time": "1518986085",
"exp": "Sun Feb 18 21:34:45 UTC 2018",
"iat": "Sun Feb 18 20:34:45 UTC 2018",
"family_name": "Cui",
"email": "theburningmonk@gmail.com"
}
},
"stage": "prod"
},
"queryStringParameters": {
"foo": "bar"
},
"headers": {
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"Accept-Language": "en-US,en;q=0.8",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Mobile-Viewer": "false",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"CloudFront-Viewer-Country": "US",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Upgrade-Insecure-Requests": "1",
"X-Forwarded-Port": "443",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"X-Forwarded-Proto": "https",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"CloudFront-Is-Tablet-Viewer": "false",
"Cache-Control": "max-age=0",
"User-Agent": "Custom User Agent String",
"CloudFront-Forwarded-Proto": "https",
"Accept-Encoding": "gzip, deflate, sdch"
},
"pathParameters": {
},
"httpMethod": "GET",
"stageVariables": {
"baz": "qux"
},
"path": "/"
}
================================================
FILE: examples/notify-restaurant.json
================================================
{
"Records":[
{
"kinesis": {
"kinesisSchemaVersion":"1.0",
"partitionKey":"e4a5eae6-19f7-5284-9cb5-f97d94518e34",
"sequenceNumber":"49584583598311915935008171659389880270421244691116720130",
"data":"eyJvcmRlcklkIjoiZTRhNWVhZTYtMTlmNy01Mjg0LTljYjUtZjk3ZDk0NTE4ZTM0IiwidXNlckVtYWlsIjoidGhlYnVybmluZ21vbmtAZ21haWwuY29tIiwicmVzdGF1cmFudE5hbWUiOiJGYW5ndGFzaWEiLCJldmVudFR5cGUiOiJvcmRlcl9wbGFjZWQifQ==",
"approximateArrivalTimestamp":1527507873.753
},
"eventSource":"aws:kinesis",
"eventVersion":"1.0",
"eventID":"shardId-000000000000:49584583598311915935008171659389880270421244691116720130",
"eventName":"aws:kinesis:record",
"invokeIdentityArn":"arn:aws:iam::374852340823:role/big-mouth-dev-notify-restaurant-us-east-1-lambdaRole",
"awsRegion":"us-east-1",
"eventSourceARN":"arn:aws:kinesis:us-east-1:374852340823:stream/order-events"
}
]
}
================================================
FILE: examples/place-order.json
================================================
{
"resource": "\/orders",
"path": "\/orders",
"httpMethod": "POST",
"headers": {
"Accept": "*\/*",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7,zh-CN;q=0.6,it;q=0.5",
"Authorization": "eyJraWQiOiJJTnFEZWM5a2crdGx3WnNISEhabjY3dWRyXC9Hc3BrSXE4QTMwNlwvV054VTg9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJmZWYzYjRjOC1jYjE2LTRlYjAtOWQ3Yy0yMDFhYzRjYjA5MDUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfRGZ1QXdhMHZCIiwiY29nbml0bzp1c2VybmFtZSI6InRoZWJ1cm5pbmdtb25rIiwiZ2l2ZW5fbmFtZSI6IllhbiIsImF1ZCI6IjF0ZjhwYjFzMW41M3A3dTYwOGU3aWM1aWg4IiwiZXZlbnRfaWQiOiJhMTA0YmZmMi02MjBkLTExZTgtODI4My1mNzgxOGJkMzhhNWEiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTUyNzQ2NzEzMSwiZXhwIjoxNTI4NjgxNzA4LCJpYXQiOjE1Mjg2NzgxMDgsImZhbWlseV9uYW1lIjoiQ3VpIiwiZW1haWwiOiJ0aGVidXJuaW5nbW9ua0BnbWFpbC5jb20ifQ.ejKf8d6f9jTU72mTmpSkuj_oHIqf_ZA9dUk3_92Y-NvmkEf3AVGJWYdLm2_MoAZwYrzHNQD5cxqIN973pgem73bI9SH0pJqZcbIZ7hS0ieGPkfn_LuMnf4_M3XMPEkGxTcGObl8batYJPeW5ohPE-rfmDjOjUVZxbzemIFY6Pf9ulZTvpVLg6VweR9637GcVktnc8pEKIggG33asqj693MEr_0xX-ioBMWb7NouSD9lz_c-XgiP13LkcNeWuC1DqACRlb8IxB7EzVfFhb-80TTXdYhVKMCnH7e1y5icipdp_lfm1aV9NlhnrWSrVctTWUSnesOmELImFrDQWlH_slg",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "GB",
"content-type": "application\/json",
"Host": "8kbasri6v6.execute-api.us-east-1.amazonaws.com",
"origin": "https:\/\/8kbasri6v6.execute-api.us-east-1.amazonaws.com",
"Referer": "https:\/\/8kbasri6v6.execute-api.us-east-1.amazonaws.com\/dev\/",
"User-Agent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/67.0.3396.79 Safari\/537.36",
"Via": "2.0 37b010671d329179b4de819b0a4d4f15.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "7cCnqSDjyqrFSTv3lxs-2EjD4gZfu-MowCk4xWkKmqXlDA6RxJZOCQ==",
"X-Amzn-Trace-Id": "Root=1-5b1dc99e-a55a324c20164c503b94e858",
"X-Forwarded-For": "88.98.204.234, 52.46.38.90",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"queryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"resourceId": "7wew6z",
"authorizer": {
"claims": {
"sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
"email_verified": "true",
"iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
"cognito:username": "theburningmonk",
"given_name": "Yan",
"aud": "1tf8pb1s1n53p7u608e7ic5ih8",
"event_id": "a104bff2-620d-11e8-8283-f7818bd38a5a",
"token_use": "id",
"auth_time": "1527467131",
"exp": "Mon Jun 11 01:48:28 UTC 2018",
"iat": "Mon Jun 11 00:48:28 UTC 2018",
"family_name": "Cui",
"email": "theburningmonk@gmail.com"
}
},
"resourcePath": "\/orders",
"httpMethod": "POST",
"extendedRequestId": "ISxwwGuhoAMFg-Q=",
"requestTime": "11\/Jun\/2018:01:00:14 +0000",
"path": "\/dev\/orders",
"accountId": "374852340823",
"protocol": "HTTP\/1.1",
"stage": "dev",
"requestTimeEpoch": 1528678814455,
"requestId": "cc9e10de-6d12-11e8-ad56-9338b9713eb7",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"sourceIp": "88.98.204.234",
"accessKey": null,
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/67.0.3396.79 Safari\/537.36",
"user": null
},
"apiId": "8kbasri6v6"
},
"body": "{\"restaurantName\":\"Fangtasia\"}",
"isBase64Encoded": false
}
================================================
FILE: examples/retry-notify-restaurant.json
================================================
{
"Records": [
{
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
"EventSource": "aws:sns",
"Sns": {
"SignatureVersion": "1",
"Timestamp": "1970-01-01T00:00:00.000Z",
"Signature": "EXAMPLE",
"SigningCertUrl": "EXAMPLE",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": "{\"orderId\":\"875ae3c4-c21c-59e6-9057-23694b75f5a2\",\"userEmail\":\"theburningmonk@gmail.com\",\"restaurantName\":\"Fangtasia\"}",
"MessageAttributes": {
},
"Type": "Notification",
"UnsubscribeUrl": "EXAMPLE",
"TopicArn": "arn:aws:sns:EXAMPLE",
"Subject": "TestInvoke"
}
}
]
}
================================================
FILE: examples/search-restaurants.json
================================================
{
"body": "{\"theme\":\"cartoon\"}",
"resource": "/{proxy+}",
"requestContext": {
"resourceId": "123456",
"apiId": "1234567890",
"resourcePath": "/{proxy+}",
"httpMethod": "POST",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"accountId": "123456789012",
"identity": {
"apiKey": null,
"userArn": null,
"cognitoAuthenticationType": null,
"caller": null,
"userAgent": "Custom User Agent String",
"user": null,
"cognitoIdentityPoolId": null,
"cognitoIdentityId": null,
"cognitoAuthenticationProvider": null,
"sourceIp": "127.0.0.1",
"accountId": null
},
"authorizer": {
"claims": {
"sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
"email_verified": "true",
"iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
"cognito:username": "theburningmonk",
"given_name": "Yan",
"aud": "1tf8pb1s1n53p7u608e7ic5ih8",
"event_id": "e727e68e-1424-11e8-be95-8774da63618a",
"token_use": "id",
"auth_time": "1518986085",
"exp": "Sun Feb 18 21:34:45 UTC 2018",
"iat": "Sun Feb 18 20:34:45 UTC 2018",
"family_name": "Cui",
"email": "theburningmonk@gmail.com"
}
},
"stage": "prod"
},
"queryStringParameters": {
"foo": "bar"
},
"headers": {
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"Accept-Language": "en-US,en;q=0.8",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Mobile-Viewer": "false",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"CloudFront-Viewer-Country": "US",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Upgrade-Insecure-Requests": "1",
"X-Forwarded-Port": "443",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"X-Forwarded-Proto": "https",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"CloudFront-Is-Tablet-Viewer": "false",
"Cache-Control": "max-age=0",
"User-Agent": "Custom User Agent String",
"CloudFront-Forwarded-Proto": "https",
"Accept-Encoding": "gzip, deflate, sdch"
},
"pathParameters": {
"proxy": "path/to/resource"
},
"httpMethod": "POST",
"stageVariables": {
"baz": "qux"
},
"path": "/path/to/resource"
}
================================================
FILE: functions/accept-order.js
================================================
'use strict';
const co = require('co');
const kinesis = require('../lib/kinesis');
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const streamName = process.env.order_events_stream;
const handler = co.wrap(function* (event, context, cb) {
let req = JSON.parse(event.body);
log.debug(`request body is valid JSON`, { requestBody: event.body });
let restaurantName = req.restaurantName;
let orderId = req.orderId;
let userEmail = req.userEmail;
correlationIds.set('order-id', orderId);
correlationIds.set('restaurant-name', restaurantName);
correlationIds.set('user-email', userEmail);
log.debug('restaurant accepted order', { orderId, restaurantName, userEmail });
let data = {
orderId,
userEmail,
restaurantName,
eventType: 'order_accepted'
}
let kinesisReq = {
Data: JSON.stringify(data), // the SDK would base64 encode this for us
PartitionKey: orderId,
StreamName: streamName
};
yield cloudwatch.trackExecTime(
"KinesisPutRecordLatency",
() => kinesis.putRecord(kinesisReq).promise()
);
log.debug(`published event into Kinesis`, { eventName: 'order_accepted' });
let response = {
statusCode: 200,
body: JSON.stringify({ orderId })
}
cb(null, response);
});
module.exports.handler = wrapper(handler);
================================================
FILE: functions/create-alarms.js
================================================
'use strict';
const _ = require('lodash');
const co = require('co');
const AWS = require('aws-sdk');
const apigateway = new AWS.APIGateway();
const cloudwatch = new AWS.CloudWatch();
const log = require('../lib/log');
const alarmActions = (process.env.alarm_actions || '').split(',');
const okAction = (process.env.ok_actions || '').split(',');
let enableDetailedMetrics = co.wrap(function* (restApiId, stageName) {
let getResp = yield apigateway.getStage({ restApiId, stageName }).promise();
log.debug('get stage settings', getResp.methodSettings);
let isDetailedMetricsEnabled = _.get(getResp, 'methodSettings.*/*.metricsEnabled', false);
if (isDetailedMetricsEnabled) {
log.debug('detailed metrics already enabled', { restApiId, stageName });
} else {
let updateReq = {
restApiId,
stageName,
patchOperations: [
{
path: "/*/*/metrics/enabled",
value: "true",
op: "replace"
}
]
};
yield apigateway.updateStage(updateReq).promise();
log.debug('enabled detailed metrics', { restApiId, stageName });
}
});
let getRestEndpoints = co.wrap(function* (restApiId) {
let resp = yield apigateway.getResources({ restApiId }).promise();
log.debug('got REST resources', { restApiId });
let resourceMethods = resp.items.map(x => {
let methods = _.keys(x.resourceMethods);
return methods.map(method => ({ resource: x.path, method }));
});
return _.flattenDeep(resourceMethods);
});
let getRestApiName = co.wrap(function* (restApiId) {
let resp = yield apigateway.getRestApi({ restApiId }).promise();
log.debug('got REST api', { restApiId });
return resp.name;
});
let createAlarmsForEndpoints = co.wrap(function* (restApiId, stageName) {
let apiName = yield getRestApiName(restApiId);
log.debug(`API name is ${apiName}`, { restApiId, stageName });
let restEndpoints = yield getRestEndpoints(restApiId);
log.debug('got REST endpoints', { restApiId, stageName, restEndpoints });
for (let endpoint of restEndpoints) {
let putReq = {
AlarmName: `API [${apiName}] stage [${stageName}] ${endpoint.method} ${endpoint.resource} : p99 > 1s`,
MetricName: 'Latency',
Dimensions: [
{ Name: 'ApiName', Value: apiName },
{ Name: 'Resource', Value: endpoint.resource },
{ Name: 'Method', Value: endpoint.method },
{ Name: 'Stage', Value: stageName }
],
Namespace: 'AWS/ApiGateway',
Threshold: 1000, // 1s
ComparisonOperator: 'GreaterThanThreshold',
Period: 60, // per min
EvaluationPeriods: 5,
DatapointsToAlarm: 5, // 5 consecutive mins to trigger alarm
ExtendedStatistic: 'p99',
ActionsEnabled: true,
AlarmActions: alarmActions,
AlarmDescription: `auto-generated by Lambda [${process.env.AWS_LAMBDA_FUNCTION_NAME}]`,
OKActions: okAction,
Unit: 'Milliseconds'
};
yield cloudwatch.putMetricAlarm(putReq).promise();
}
log.debug('auto-created latency ALARMS for REST endpoints', { restApiId, stageName, restEndpoints });
});
module.exports.handler = co.wrap(function* (event, context, cb) {
let restApiId = event.detail.requestParameters.restApiId;
let stageName = event.detail.requestParameters.createDeploymentInput.stageName;
yield enableDetailedMetrics(restApiId, stageName);
yield createAlarmsForEndpoints(restApiId, stageName);
cb(null, 'ok');
});
================================================
FILE: functions/fulfill-order.js
================================================
'use strict';
const co = require('co');
const kinesis = require('../lib/kinesis');
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const streamName = process.env.order_events_stream;
const handler = co.wrap(function* (event, context, cb) {
let body = JSON.parse(event.body);
log.debug(`request body is valid JSON`, { requestBody: event.body });
let restaurantName = body.restaurantName;
let orderId = body.orderId;
let userEmail = body.userEmail;
correlationIds.set('order-id', orderId);
correlationIds.set('restaurant-name', restaurantName);
correlationIds.set('user-email', userEmail);
log.debug('restaurant has fulfilled order', { orderId, restaurantName, userEmail });
let data = {
orderId,
userEmail,
restaurantName,
eventType: 'order_fulfilled'
}
let kinesisReq = {
Data: JSON.stringify(data), // the SDK would base64 encode this for us
PartitionKey: orderId,
StreamName: streamName
};
yield cloudwatch.trackExecTime(
"KinesisPutRecordLatency",
() => kinesis.putRecord(kinesisReq).promise()
);
log.debug(`published event into Kinesis`, { eventName: 'order_fulfilled' });
let response = {
statusCode: 200,
body: JSON.stringify({ orderId })
}
cb(null, response);
});
module.exports.handler = wrapper(handler);
================================================
FILE: functions/get-index.js
================================================
'use strict';
const co = require("co");
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
const Mustache = require('mustache');
const http = require('../lib/http');
const URL = require('url');
const aws4 = require('../lib/aws4');
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const AWSXRay = require('aws-xray-sdk');
const wrapper = require('../middleware/wrapper');
const { ssm, secretsManager } = require('middy/middlewares');
const STAGE = process.env.STAGE;
const awsRegion = process.env.AWS_REGION;
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var html;
function* loadHtml() {
if (!html) {
html = yield fs.readFileAsync('static/index.html', 'utf-8');
}
return html;
}
function* getRestaurants(restaurantsApiUrl) {
let url = URL.parse(restaurantsApiUrl);
let opts = {
host: url.hostname,
path: url.pathname
};
aws4.sign(opts);
let httpReq = http({
uri: restaurantsApiUrl,
headers: opts.headers
});
return new Promise((resolve, reject) => {
let f = co.wrap(function* (subsegment) {
if (subsegment) {
subsegment.addMetadata('url', restaurantsApiUrl);
}
try {
let body = (yield httpReq).body;
if (subsegment) {
subsegment.close();
}
resolve(body);
} catch (err) {
if (subsegment) {
subsegment.close(err);
}
reject(err);
}
});
// the current sub/segment
let segment = AWSXRay.getSegment();
AWSXRay.captureAsyncFunc("getting restaurants", f, segment);
});
}
const handler = co.wrap(function* (event, context, callback) {
yield aws4.init();
let template = yield loadHtml();
log.debug("loaded HTML template");
let restaurants = yield cloudwatch.trackExecTime(
"GetRestaurantsLatency",
() => getRestaurants(context.restaurants_api)
);
log.debug(`loaded ${restaurants.length} restaurants`);
let dayOfWeek = days[new Date().getDay()];
let view = {
dayOfWeek,
restaurants,
awsRegion,
cognitoUserPoolId: context.cognito.user_pool_id,
cognitoClientId: context.cognito.client_id,
searchUrl: `${context.restaurants_api}/search`,
placeOrderUrl: `${context.orders_api}`
};
let html = Mustache.render(template, view);
log.debug(`rendered HTML [${html.length} bytes]`);
// uncomment this to cause function to err
// yield http({ uri: 'https://theburningmonk.com' });
cloudwatch.incrCount('RestaurantsReturned', restaurants.length);
const response = {
statusCode: 200,
body: html,
headers: {
'content-type': 'text/html; charset=UTF-8'
}
};
callback(null, response);
});
module.exports.handler = wrapper(handler)
.use(ssm({
cache: true,
cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins
setToContext: true,
names: {
restaurants_api: `/bigmouth/${STAGE}/restaurants_api`,
orders_api: `/bigmouth/${STAGE}/orders_api`
}
}))
.use(secretsManager({
cache: true,
cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins
secrets: {
cognito: `/bigmouth/${STAGE}/cognito`
}
}));
================================================
FILE: functions/get-restaurants.js
================================================
'use strict';
const co = require('co');
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const dynamodb = new AWS.DynamoDB.DocumentClient();
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const defaultResults = process.env.defaultResults || 8;
const tableName = process.env.restaurants_table;
function* getRestaurants(count) {
let req = {
TableName: tableName,
Limit: count
};
let resp = yield cloudwatch.trackExecTime(
"DynamoDBScanLatency",
() => dynamodb.scan(req).promise()
);
return resp.Items;
}
const handler = co.wrap(function* (event, context, cb) {
let restaurants = yield getRestaurants(defaultResults);
log.debug(`loaded ${restaurants.length} restaurants`);
cloudwatch.incrCount("RestaurantsReturned", restaurants.length);
let response = {
statusCode: 200,
body: JSON.stringify(restaurants)
}
cb(null, response);
});
module.exports.handler = wrapper(handler);
================================================
FILE: functions/notify-restaurant.js
================================================
'use strict';
const co = require('co');
const notify = require('../lib/notify');
const retry = require('../lib/retry');
const log = require('../lib/log');
const wrapper = require('../middleware/wrapper');
const flushMetrics = require('../middleware/flush-metrics');
const handler = co.wrap(function* (event, context, cb) {
let events = context.parsedKinesisEvents;
let orderPlaced = events.filter(r => r.eventType === 'order_placed');
log.debug(`found ${orderPlaced.length} 'order_placed' events`);
for (let order of orderPlaced) {
order.scopeToThis();
try {
yield notify.restaurantOfOrder(order);
} catch (err) {
yield retry.restaurantNotification(order);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.warn('failed to notify restaurant of new order', logContext, err);
}
order.unscope();
}
cb(null, "all done");
});
module.exports.handler = wrapper(handler)
.use(flushMetrics);
================================================
FILE: functions/notify-user.js
================================================
'use strict';
const co = require('co');
const notify = require('../lib/notify');
const retry = require('../lib/retry');
const log = require('../lib/log');
const wrapper = require('../middleware/wrapper');
const flushMetrics = require('../middleware/flush-metrics');
const handler = co.wrap(function* (event, context, cb) {
let events = context.parsedKinesisEvents;
let orderAccepted = events.filter(r => r.eventType === 'order_accepted');
log.debug(`found ${orderAccepted.length} 'order_accepted' events`);
for (let order of orderAccepted) {
order.scopeToThis();
try {
yield notify.userOfOrderAccepted(order);
} catch (err) {
yield retry.userNotification(order);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.warn('failed to notify user of accepted order', logContext, err);
}
order.unscope();
}
cb(null, "all done");
});
module.exports.handler = wrapper(handler)
.use(flushMetrics);
================================================
FILE: functions/place-order.js
================================================
'use strict';
const _ = require('lodash');
const co = require('co');
const kinesis = require('../lib/kinesis');
const chance = require('chance').Chance();
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const correlationIds = require('../lib/correlation-ids');
const wrapper = require('../middleware/wrapper');
const streamName = process.env.order_events_stream;
const UNAUTHORIZED = {
statusCode: 401,
body: "unauthorized"
}
const handler = co.wrap(function* (event, context, cb) {
let req = JSON.parse(event.body);
log.debug(`request body is valid JSON`, { requestBody: event.body });
let userEmail = _.get(event, 'requestContext.authorizer.claims.email');
if (!userEmail) {
cb(null, UNAUTHORIZED);
log.error('unauthorized request, user email is not provided');
return;
}
let restaurantName = req.restaurantName;
let orderId = chance.guid();
correlationIds.set('order-id', orderId);
correlationIds.set('restaurant-name', restaurantName);
correlationIds.set('user-email', userEmail);
log.debug(`placing order...`, { orderId, restaurantName, userEmail });
let data = {
orderId,
userEmail,
restaurantName,
eventType: 'order_placed'
}
let kinesisReq = {
Data: JSON.stringify(data), // the SDK would base64 encode this for us
PartitionKey: orderId,
StreamName: streamName
};
yield cloudwatch.trackExecTime(
"KinesisPutRecordLatency",
() => kinesis.putRecord(kinesisReq).promise()
);
log.debug(`published event into Kinesis`, { eventName: 'order_placed' });
let response = {
statusCode: 200,
body: JSON.stringify({ orderId })
}
cb(null, response);
});
module.exports.handler = wrapper(handler);
================================================
FILE: functions/retry-notify-restaurant.js
================================================
'use strict';
const co = require('co');
const notify = require('../lib/notify');
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const flushMetrics = require('../middleware/flush-metrics');
const handler = co.wrap(function* (event, context, cb) {
let order = JSON.parse(event.Records[0].Sns.Message);
order.retried = true;
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail,
retry: true
};
try {
yield notify.restaurantOfOrder(order);
cb(null, "all done");
} catch (err) {
log.warn('failed to notify restaurant of new order', logContext, err);
cb(err);
} finally {
cloudwatch.incrCount("NotifyRestaurantRetried");
}
});
module.exports.handler = wrapper(handler)
.use(flushMetrics);
================================================
FILE: functions/retry-notify-user.js
================================================
'use strict';
const co = require('co');
const notify = require('../lib/notify');
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const flushMetrics = require('../middleware/flush-metrics');
const handler = co.wrap(function* (event, context, cb) {
let order = JSON.parse(event.Records[0].Sns.Message);
order.retried = true;
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail,
retry: true
};
try {
yield notify.userOfOrderAccepted(order);
cb(null, "all done");
} catch (err) {
log.warn('failed to notify user of accepted order', logContext, err);
cb(err);
} finally {
cloudwatch.incrCount("NotifyUserRetried");
}
});
module.exports.handler = wrapper(handler)
.use(flushMetrics);
================================================
FILE: functions/search-restaurants.js
================================================
'use strict';
const co = require('co');
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const dynamodb = new AWS.DynamoDB.DocumentClient();
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper = require('../middleware/wrapper');
const defaultResults = process.env.defaultResults || 8;
const tableName = process.env.restaurants_table;
function* findRestaurantsByTheme(theme, count) {
let req = {
TableName: tableName,
Limit: count,
FilterExpression: "contains(themes, :theme)",
ExpressionAttributeValues: { ":theme": theme }
};
let resp = yield cloudwatch.trackExecTime(
"DynamoDBScanLatency",
() => dynamodb.scan(req).promise()
);
return resp.Items;
}
const handler = co.wrap(function* (event, context, cb) {
let req = JSON.parse(event.body);
log.debug(`request body is valid JSON`, { requestBody: event.body });
let restaurants = yield findRestaurantsByTheme(req.theme, defaultResults);
log.debug(`found ${restaurants.length} restaurants`);
cloudwatch.incrCount("RestaurantsFound", restaurants.length);
let response = {
statusCode: 200,
body: JSON.stringify(restaurants)
}
cb(null, response);
});
module.exports.handler = wrapper(handler);
================================================
FILE: lib/aws4.js
================================================
var aws4 = exports,
url = require('url'),
querystring = require('querystring'),
crypto = require('crypto'),
lru = require('./lru'),
credentialsCache = lru(1000),
awscred = require('./awscred')
// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html
function hmac(key, string, encoding) {
return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)
}
function hash(string, encoding) {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)
}
// This function assumes the string has already been percent encoded
function encodeRfc3986(urlEncodedString) {
return urlEncodedString.replace(/[!'()*]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
// request: { path | body, [host], [method], [headers], [service], [region] }
// credentials: { accessKeyId, secretAccessKey, [sessionToken] }
function RequestSigner(request, credentials) {
if (typeof request === 'string') request = url.parse(request)
var headers = request.headers = (request.headers || {}),
hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host)
this.request = request
this.credentials = credentials || this.defaultCredentials()
this.service = request.service || hostParts[0] || ''
this.region = request.region || hostParts[1] || 'us-east-1'
// SES uses a different domain from the service name
if (this.service === 'email') this.service = 'ses'
if (!request.method && request.body)
request.method = 'POST'
if (!headers.Host && !headers.host) {
headers.Host = request.hostname || request.host || this.createHost()
// If a port is specified explicitly, use it as is
if (request.port)
headers.Host += ':' + request.port
}
if (!request.hostname && !request.host)
request.hostname = headers.Host || headers.host
this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT'
}
RequestSigner.prototype.matchHost = function(host) {
var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com$/)
var hostParts = (match || []).slice(1, 3)
// ES's hostParts are sometimes the other way round, if the value that is expected
// to be region equals ‘es’ switch them back
// e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com
if (hostParts[1] === 'es')
hostParts = hostParts.reverse()
return hostParts
}
// http://docs.aws.amazon.com/general/latest/gr/rande.html
RequestSigner.prototype.isSingleRegion = function() {
// Special case for S3 and SimpleDB in us-east-1
if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true
return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts']
.indexOf(this.service) >= 0
}
RequestSigner.prototype.createHost = function() {
var region = this.isSingleRegion() ? '' :
(this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region,
service = this.service === 'ses' ? 'email' : this.service
return service + region + '.amazonaws.com'
}
RequestSigner.prototype.prepareRequest = function() {
this.parsePath()
var request = this.request, headers = request.headers, query
if (request.signQuery) {
this.parsedPath.query = query = this.parsedPath.query || {}
if (this.credentials.sessionToken)
query['X-Amz-Security-Token'] = this.credentials.sessionToken
if (this.service === 's3' && !query['X-Amz-Expires'])
query['X-Amz-Expires'] = 86400
if (query['X-Amz-Date'])
this.datetime = query['X-Amz-Date']
else
query['X-Amz-Date'] = this.getDateTime()
query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString()
query['X-Amz-SignedHeaders'] = this.signedHeaders()
} else {
if (!request.doNotModifyHeaders && !this.isCodeCommitGit) {
if (request.body && !headers['Content-Type'] && !headers['content-type'])
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
if (request.body && !headers['Content-Length'] && !headers['content-length'])
headers['Content-Length'] = Buffer.byteLength(request.body)
if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token'])
headers['X-Amz-Security-Token'] = this.credentials.sessionToken
if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256'])
headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex')
if (headers['X-Amz-Date'] || headers['x-amz-date'])
this.datetime = headers['X-Amz-Date'] || headers['x-amz-date']
else
headers['X-Amz-Date'] = this.getDateTime()
}
delete headers.Authorization
delete headers.authorization
}
}
RequestSigner.prototype.sign = function() {
if (!this.parsedPath) this.prepareRequest()
if (this.request.signQuery) {
this.parsedPath.query['X-Amz-Signature'] = this.signature()
} else {
this.request.headers.Authorization = this.authHeader()
}
this.request.path = this.formatPath()
return this.request
}
RequestSigner.prototype.getDateTime = function() {
if (!this.datetime) {
var headers = this.request.headers,
date = new Date(headers.Date || headers.date || new Date)
this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '')
// Remove the trailing 'Z' on the timestamp string for CodeCommit git access
if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1)
}
return this.datetime
}
RequestSigner.prototype.getDate = function() {
return this.getDateTime().substr(0, 8)
}
RequestSigner.prototype.authHeader = function() {
return [
'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(),
'SignedHeaders=' + this.signedHeaders(),
'Signature=' + this.signature(),
].join(', ')
}
RequestSigner.prototype.signature = function() {
var date = this.getDate(),
cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(),
kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey)
if (!kCredentials) {
kDate = hmac('AWS4' + this.credentials.secretAccessKey, date)
kRegion = hmac(kDate, this.region)
kService = hmac(kRegion, this.service)
kCredentials = hmac(kService, 'aws4_request')
credentialsCache.set(cacheKey, kCredentials)
}
return hmac(kCredentials, this.stringToSign(), 'hex')
}
RequestSigner.prototype.stringToSign = function() {
return [
'AWS4-HMAC-SHA256',
this.getDateTime(),
this.credentialString(),
hash(this.canonicalString(), 'hex'),
].join('\n')
}
RequestSigner.prototype.canonicalString = function() {
if (!this.parsedPath) this.prepareRequest()
var pathStr = this.parsedPath.path,
query = this.parsedPath.query,
headers = this.request.headers,
queryStr = '',
normalizePath = this.service !== 's3',
decodePath = this.service === 's3' || this.request.doNotEncodePath,
decodeSlashesInPath = this.service === 's3',
firstValOnly = this.service === 's3',
bodyHash
if (this.service === 's3' && this.request.signQuery) {
bodyHash = 'UNSIGNED-PAYLOAD'
} else if (this.isCodeCommitGit) {
bodyHash = ''
} else {
bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] ||
hash(this.request.body || '', 'hex')
}
if (query) {
queryStr = encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce(function(obj, key) {
if (!key) return obj
obj[key] = !Array.isArray(query[key]) ? query[key] :
(firstValOnly ? query[key][0] : query[key].slice().sort())
return obj
}, {})))
}
if (pathStr !== '/') {
if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/')
pathStr = pathStr.split('/').reduce(function(path, piece) {
if (normalizePath && piece === '..') {
path.pop()
} else if (!normalizePath || piece !== '.') {
if (decodePath) piece = querystring.unescape(piece)
path.push(encodeRfc3986(querystring.escape(piece)))
}
return path
}, []).join('/')
if (pathStr[0] !== '/') pathStr = '/' + pathStr
if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/')
}
return [
this.request.method || 'GET',
pathStr,
queryStr,
this.canonicalHeaders() + '\n',
this.signedHeaders(),
bodyHash,
].join('\n')
}
RequestSigner.prototype.canonicalHeaders = function() {
var headers = this.request.headers
function trimAll(header) {
return header.toString().trim().replace(/\s+/g, ' ')
}
return Object.keys(headers)
.sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 })
.map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) })
.join('\n')
}
RequestSigner.prototype.signedHeaders = function() {
return Object.keys(this.request.headers)
.map(function(key) { return key.toLowerCase() })
.sort()
.join(';')
}
RequestSigner.prototype.credentialString = function() {
return [
this.getDate(),
this.region,
this.service,
'aws4_request',
].join('/')
}
RequestSigner.prototype.defaultCredentials = function() {
var env = process.env
return {
accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
sessionToken: env.AWS_SESSION_TOKEN,
}
}
RequestSigner.prototype.parsePath = function() {
var path = this.request.path || '/',
queryIx = path.indexOf('?'),
query = null
if (queryIx >= 0) {
query = querystring.parse(path.slice(queryIx + 1))
path = path.slice(0, queryIx)
}
// S3 doesn't always encode characters > 127 correctly and
// all services don't encode characters > 255 correctly
// So if there are non-reserved chars (and it's not already all % encoded), just encode them all
if (/[^0-9A-Za-z!'()*\-._~%/]/.test(path)) {
path = path.split('/').map(function(piece) {
return querystring.escape(querystring.unescape(piece))
}).join('/')
}
this.parsedPath = {
path: path,
query: query,
}
}
RequestSigner.prototype.formatPath = function() {
var path = this.parsedPath.path,
query = this.parsedPath.query
if (!query) return path
// Services don't support empty query string keys
if (query[''] != null) delete query['']
return path + '?' + encodeRfc3986(querystring.stringify(query))
}
aws4.RequestSigner = RequestSigner
aws4.sign = function(request, credentials) {
return new RequestSigner(request, credentials).sign()
}
var isInitialized = false;
aws4.init = function() {
return new Promise(function(resolve, reject) {
if (isInitialized) {
return resolve()
}
if (process.env.AWS_ACCESS_KEY_ID) {
isInitialized = true;
return resolve()
} else {
console.error("initializing AWS credentials...")
awscred.load(function(error, result) {
if (error) {
reject(error)
} else {
let cred = result.credentials;
process.env.AWS_ACCESS_KEY_ID = cred.accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = cred.secretAccessKey;
if (cred.sessionToken) {
process.env.AWS_SESSION_TOKEN = cred.sessionToken;
}
resolve()
isInitialized = true
}
})
}
})
}
================================================
FILE: lib/awscred.js
================================================
var fs = require('fs'),
path = require('path'),
http = require('http'),
env = process.env
exports.credentialsCallChain = [
loadCredentialsFromEnv,
loadCredentialsFromIniFile,
loadCredentialsFromEc2Metadata,
loadCredentialsFromEcs,
]
exports.regionCallChain = [
loadRegionFromEnv,
loadRegionFromIniFile,
]
exports.load = exports.loadCredentialsAndRegion = loadCredentialsAndRegion
exports.loadCredentials = loadCredentials
exports.loadRegion = loadRegion
exports.loadRegionSync = loadRegionSync
exports.loadCredentialsFromEnv = loadCredentialsFromEnv
exports.loadRegionFromEnv = loadRegionFromEnv
exports.loadRegionFromEnvSync = loadRegionFromEnvSync
exports.loadCredentialsFromIniFile = loadCredentialsFromIniFile
exports.loadRegionFromIniFile = loadRegionFromIniFile
exports.loadRegionFromIniFileSync = loadRegionFromIniFileSync
exports.loadCredentialsFromEc2Metadata = loadCredentialsFromEc2Metadata
exports.loadProfileFromIniFile = loadProfileFromIniFile
exports.loadProfileFromIniFileSync = loadProfileFromIniFileSync
exports.merge = merge
function loadCredentialsAndRegion(options, cb) {
if (!cb) { cb = options; options = {} }
cb = once(cb)
var out = {}, callsRemaining = 2
function checkDone(propName) {
return function(err, data) {
if (err) return cb(err)
out[propName] = data
if (!--callsRemaining) return cb(null, out)
}
}
loadCredentials(options, checkDone('credentials'))
loadRegion(options, checkDone('region'))
}
function loadCredentials(options, cb) {
if (!cb) { cb = options; options = {} }
var credentialsCallChain = options.credentialsCallChain || exports.credentialsCallChain
function nextCall(i) {
credentialsCallChain[i](options, function(err, credentials) {
if (err) return cb(err)
if (credentials.accessKeyId && credentials.secretAccessKey)
return cb(null, credentials)
if (i >= credentialsCallChain.length - 1)
return cb(null, {})
nextCall(i + 1)
})
}
nextCall(0)
}
function loadRegion(options, cb) {
if (!cb) { cb = options; options = {} }
var regionCallChain = options.regionCallChain || exports.regionCallChain
function nextCall(i) {
regionCallChain[i](options, function(err, region) {
if (err) return cb(err)
if (region)
return cb(null, region)
if (i >= regionCallChain.length - 1)
return cb(null, 'us-east-1')
nextCall(i + 1)
})
}
nextCall(0)
}
function loadRegionSync(options) {
return loadRegionFromEnvSync(options) || loadRegionFromIniFileSync(options)
}
function loadCredentialsFromEnv(options, cb) {
if (!cb) { cb = options; options = {} }
cb(null, {
accessKeyId: env.AWS_ACCESS_KEY_ID || env.AMAZON_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AMAZON_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
sessionToken: env.AWS_SESSION_TOKEN || env.AMAZON_SESSION_TOKEN,
})
}
function loadRegionFromEnv(options, cb) {
if (!cb) { cb = options; options = {} }
cb(null, loadRegionFromEnvSync())
}
function loadRegionFromEnvSync() {
return env.AWS_REGION || env.AMAZON_REGION || env.AWS_DEFAULT_REGION
}
function loadCredentialsFromIniFile(options, cb) {
if (!cb) { cb = options; options = {} }
loadProfileFromIniFile(options, 'credentials', function(err, profile) {
if (err) return cb(err)
cb(null, {
accessKeyId: profile.aws_access_key_id,
secretAccessKey: profile.aws_secret_access_key,
sessionToken: profile.aws_session_token,
})
})
}
function loadRegionFromIniFile(options, cb) {
if (!cb) { cb = options; options = {} }
loadProfileFromIniFile(options, 'config', function(err, profile) {
if (err) return cb(err)
cb(null, profile.region)
})
}
function loadRegionFromIniFileSync(options) {
return loadProfileFromIniFileSync(options || {}, 'config').region
}
var TIMEOUT_CODES = ['ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'Unknown system errno 64']
var ec2Callbacks = []
var ecsCallbacks = []
function loadCredentialsFromEcs(options, cb) {
if (!cb) { cb = options; options = {} }
ecsCallbacks.push(cb)
if (ecsCallbacks.length > 1) return // only want one caller at a time
cb = function(err, credentials) {
ecsCallbacks.forEach(function(cb) { cb(err, credentials) })
ecsCallbacks = []
}
if (options.timeout == null) options.timeout = 5000
options.host = '169.254.170.2'
options.path = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
return request(options, function(err, res, data) {
if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})
if (err) return cb(err)
if (res.statusCode != 200)
return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))
try { data = JSON.parse(data) } catch (e) { }
if (res.statusCode != 200)
return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))
cb(null, {
accessKeyId: data.AccessKeyId,
secretAccessKey: data.SecretAccessKey,
sessionToken: data.Token,
expiration: new Date(data.Expiration),
})
})
}
function loadCredentialsFromEc2Metadata(options, cb) {
if (!cb) { cb = options; options = {} }
ec2Callbacks.push(cb)
if (ec2Callbacks.length > 1) return // only want one caller at a time
cb = function(err, credentials) {
ec2Callbacks.forEach(function(cb) { cb(err, credentials) })
ec2Callbacks = []
}
if (options.timeout == null) options.timeout = 5000
options.host = '169.254.169.254'
options.path = '/latest/meta-data/iam/security-credentials/'
return request(options, function(err, res, data) {
if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})
if (err) return cb(err)
if (res.statusCode != 200)
return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))
options.path += data.split('\n')[0]
request(options, function(err, res, data) {
if (err) return cb(err)
try { data = JSON.parse(data) } catch (e) { }
if (res.statusCode != 200 || data.Code != 'Success')
return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))
cb(null, {
accessKeyId: data.AccessKeyId,
secretAccessKey: data.SecretAccessKey,
sessionToken: data.Token,
expiration: new Date(data.Expiration),
})
})
})
}
function loadProfileFromIniFile(options, defaultFilename, cb) {
var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),
profile = options.profile || resolveProfile()
fs.readFile(filename, 'utf8', function(err, data) {
if (err && err.code == 'ENOENT') return cb(null, {})
if (err) return cb(err)
cb(null, parseAwsIni(data)[profile] || {})
})
}
function loadProfileFromIniFileSync(options, defaultFilename) {
var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),
profile = options.profile || resolveProfile(),
data
try {
data = fs.readFileSync(filename, 'utf8')
} catch (err) {
if (err.code == 'ENOENT') return {}
throw err
}
return parseAwsIni(data)[profile] || {}
}
function merge(obj, options, cb) {
if (!cb) { cb = options; options = {} }
var needRegion = !obj.region
var needCreds = !obj.credentials || !obj.credentials.accessKeyId || !obj.credentials.secretAccessKey
function loadCreds(cb) {
if (needRegion && needCreds) {
return loadCredentialsAndRegion(options, cb)
} else if (needRegion) {
return loadRegion(options, function(err, region) { cb(err, {region: region}) })
} else if (needCreds) {
return loadCredentials(options, function(err, credentials) { cb(err, {credentials: credentials}) })
}
cb(null, {})
}
loadCreds(function(err, creds) {
if (err) return cb(err)
if (creds.region) obj.region = creds.region
if (creds.credentials) {
if (!obj.credentials) {
obj.credentials = creds.credentials
} else {
Object.keys(creds.credentials).forEach(function(key) {
if (!obj.credentials[key]) obj.credentials[key] = creds.credentials[key]
})
}
}
cb()
})
}
function resolveProfile() {
return env.AWS_PROFILE || env.AMAZON_PROFILE || 'default'
}
function resolveHome() {
return env.HOME || env.USERPROFILE || ((env.HOMEDRIVE || 'C:') + env.HOMEPATH)
}
// Fairly strict INI parser – will only deal with alpha keys, must be within sections
function parseAwsIni(ini) {
var section,
out = Object.create(null),
re = /^\[([^\]]+)\]\s*$|^([a-z_]+)\s*=\s*(.+?)\s*$/,
lines = ini.split(/\r?\n/)
lines.forEach(function(line) {
var match = line.match(re)
if (!match) return
if (match[1]) {
section = match[1]
if (out[section] == null) out[section] = Object.create(null)
} else if (section) {
out[section][match[2]] = match[3]
}
})
return out
}
function request(options, cb) {
cb = once(cb)
var req = http.request(options, function(res) {
var data = ''
res.setEncoding('utf8')
res.on('error', cb)
res.on('data', function(chunk) { data += chunk })
res.on('end', function() { cb(null, res, data) })
}).on('error', cb)
if (options.timeout != null) {
req.setTimeout(options.timeout)
req.on('timeout', function() { req.abort() })
}
req.end()
}
function once(cb) {
var called = false
return function() {
if (called) return
called = true
cb.apply(this, arguments)
}
}
================================================
FILE: lib/cloudwatch.js
================================================
'use strict';
const co = require('co');
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const log = require('./log');
const cloudwatch = new AWS.CloudWatch();
const namespace = 'big-mouth';
const async = (process.env.async_metrics || 'false') === 'true';
// the Lambda execution environment defines a number of env variables:
// https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
// and the serverless framework also defines a STAGE env variable too
const dimensions =
[
{ Name: 'Function', Value: process.env.AWS_LAMBDA_FUNCTION_NAME },
{ Name: 'Version', Value: process.env.AWS_LAMBDA_FUNCTION_VERSION },
{ Name: 'Stage', Value: process.env.STAGE }
]
.filter(dim => dim.Value);
let countMetrics = {};
let timeMetrics = {};
function getCountMetricData(name, value) {
return {
MetricName : name,
Dimensions : dimensions,
Unit : 'Count',
Value : value
};
}
function getTimeMetricData(name, statsValues) {
return {
MetricName : name,
Dimensions : dimensions,
Unit : 'Milliseconds',
StatisticValues : statsValues
};
}
function getCountMetricDatum() {
let keys = Object.keys(countMetrics);
if (keys.length === 0) {
return [];
}
let metricDatum = keys.map(key => getCountMetricData(key, countMetrics[key]));
countMetrics = {}; // zero out the recorded count metrics
return metricDatum;
}
function getTimeMetricDatum() {
let keys = Object.keys(timeMetrics);
if (keys.length === 0) {
return [];
}
let metricDatum = keys.map(key => getTimeMetricData(key, timeMetrics[key]));
timeMetrics = {}; // zero out the recorded time metrics
return metricDatum;
}
let flush = co.wrap(function* () {
let countDatum = getCountMetricDatum();
let timeDatum = getTimeMetricDatum();
let allDatum = countDatum.concat(timeDatum);
if (allDatum.length == 0) { return; }
let metricNames = allDatum.map(x => x.MetricName).join(',');
log.debug(`flushing [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);
var params = {
MetricData: allDatum,
Namespace: namespace
};
try {
yield cloudwatch.putMetricData(params).promise();
log.debug(`flushed [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);
} catch (err) {
log.warn(`cloudn't flush [${allDatum.length}] CloudWatch metrics`, null, err);
}
});
function clear() {
countMetrics = {};
timeMetrics = {};
}
function incrCount(metricName, count) {
count = count || 1;
if (async) {
console.log(`MONITORING|${count}|count|${metricName}|${namespace}`);
} else {
if (countMetrics[metricName]) {
countMetrics[metricName] += count;
} else {
countMetrics[metricName] = count;
}
}
}
function recordTimeInMillis(metricName, ms) {
if (!ms) {
return;
}
log.debug(`new execution time for [${metricName}] : ${ms} milliseconds`);
if (async) {
console.log(`MONITORING|${ms}|milliseconds|${metricName}|${namespace}`);
} else {
if (timeMetrics[metricName]) {
let metric = timeMetrics[metricName];
metric.Sum += ms;
metric.Maximum = Math.max(metric.Maximum, ms);
metric.Minimum = Math.min(metric.Minimum, ms);
metric.SampleCount += 1;
} else {
let statsValues = {
Maximum : ms,
Minimum : ms,
SampleCount : 1,
Sum : ms
};
timeMetrics[metricName] = statsValues;
}
}
}
function trackExecTime(metricName, f) {
if (!f || typeof f !== "function") {
throw new Error('cloudWatch.trackExecTime requires a function, eg. () => 42');
}
if (!metricName) {
throw new Error('cloudWatch.trackExecTime requires a metric name, eg. "CloudSearch-latency"');
}
let start = new Date().getTime(), end;
let res = f();
// anything with a 'then' function can be considered a Promise...
// http://stackoverflow.com/a/27746324/55074
if (!res.hasOwnProperty('then')) {
end = new Date().getTime();
recordTimeInMillis(metricName, end-start);
return res;
} else {
return res.then(x => {
end = new Date().getTime();
recordTimeInMillis(metricName, end-start);
return x;
});
}
}
module.exports = {
flush,
clear,
incrCount,
trackExecTime,
recordTimeInMillis
};
================================================
FILE: lib/correlation-ids.js
================================================
'use strict';
let clearAll = () => global.CONTEXT = undefined;
let replaceAllWith = ctx => global.CONTEXT = ctx;
let set = (key, value) => {
if (!key.startsWith("x-correlation-")) {
key = "x-correlation-" + key;
}
if (!global.CONTEXT) {
global.CONTEXT = {};
}
global.CONTEXT[key] = value;
};
let get = () => global.CONTEXT || {};
module.exports = {
clearAll: clearAll,
replaceAllWith: replaceAllWith,
set: set,
get: get
};
================================================
FILE: lib/http.js
================================================
'use strict';
const correlationIds = require('./correlation-ids');
const http = require('superagent-promise')(require('superagent'), Promise);
function getRequest (options) {
let uri = options.uri;
let method = options.method || '';
switch (method.toLowerCase()) {
case '':
case 'get':
return http.get(uri);
case 'head':
return http.head(uri);
case 'post':
return http.post(uri);
case 'put':
return http.put(uri);
case 'delete':
return http.del(uri);
default:
throw new Error(`unsupported method : ${method.toLowerCase()}`);
}
}
function setHeaders (request, headers) {
let headerNames = Object.keys(headers);
headerNames.forEach(h => request = request.set(h, headers[h]));
return request;
}
function setQueryStrings (request, qs) {
if (!qs) {
return request;
}
return request.query(qs);
}
function setBody (request, body) {
if (!body) {
return request;
}
return request.send(body);
}
// options: {
// uri : string
// method : GET (default) | POST | PUT | HEAD
// headers : object
// qs : object
// body : object
// }
let Req = (options) => {
if (!options) {
throw new Error('no HTTP request options is provided');
}
if (!options.uri) {
throw new Error('no HTTP uri is specified');
}
const context = correlationIds.get();
// copy the provided headers last so it overrides the values from the context
let headers = Object.assign({}, context, options.headers);
let request = getRequest(options);
request = setHeaders(request, headers);
request = setQueryStrings(request, options.qs);
request = setBody(request, options.body);
return request
.catch(e => {
if (e.response && e.response.error) {
throw e.response.error;
}
throw e;
});
};
module.exports = Req;
================================================
FILE: lib/kinesis.js
================================================
'use strict';
const _ = require('lodash');
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const Kinesis = new AWS.Kinesis();
const log = require('./log');
const correlationIds = require('./correlation-ids');
function tryJsonParse(data) {
if (!_.isString(data)) {
return null;
}
try {
return JSON.parse(data);
} catch (err) {
log.warn('only JSON string data can be modified to insert correlation IDs');
return null;
}
}
function addCorrelationIds(data) {
// only do with with JSON string data
const payload = tryJsonParse(data);
if (!payload) {
return data;
}
const context = correlationIds.get();
const newData = Object.assign({ __context__: context }, payload);
return JSON.stringify(newData);
}
function putRecord(params, cb) {
const newData = addCorrelationIds(params.Data);
params = Object.assign({}, params, { Data: newData });
return Kinesis.putRecord(params, cb);
};
const client = Object.assign({}, Kinesis, { putRecord });
module.exports = client;
================================================
FILE: lib/log.js
================================================
'use strict';
const correlationIds = require('./correlation-ids');
const LogLevels = {
DEBUG : 0,
INFO : 1,
WARN : 2,
ERROR : 3
};
// most of these are available through the Node.js execution environment for Lambda
// see https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
const DEFAULT_CONTEXT = {
awsRegion: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION,
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
functionMemorySize: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
stage: process.env.ENVIRONMENT || process.env.STAGE
};
function getContext () {
// if there's a global variable for all the current request context then use it
const context = correlationIds.get();
if (context) {
// note: this is a shallow copy, which is ok as we're not going to mutate anything
return Object.assign({}, DEFAULT_CONTEXT, context);
}
return DEFAULT_CONTEXT;
}
// default to debug if not specified
function logLevelName() {
return process.env.log_level || 'DEBUG';
}
function isEnabled (level) {
return level >= LogLevels[logLevelName()];
}
function appendError(params, err) {
if (!err) {
return params;
}
return Object.assign(
{},
params || {},
{ errorName: err.name, errorMessage: err.message, stackTrace: err.stack }
);
}
function log (levelName, message, params) {
if (!isEnabled(LogLevels[levelName])) {
return;
}
let context = getContext();
let logMsg = Object.assign({}, context, params);
logMsg.level = levelName;
logMsg.message = message;
console.log(JSON.stringify(logMsg));
}
module.exports.debug = (msg, params) => log('DEBUG', msg, params);
module.exports.info = (msg, params) => log('INFO', msg, params);
module.exports.warn = (msg, params, error) => log('WARN', msg, appendError(params, error));
module.exports.error = (msg, params, error) => log('ERROR', msg, appendError(params, error));
module.exports.enableDebug = () => {
const oldLevel = process.env.log_level;
process.env.log_level = 'DEBUG';
// return a function to perform the rollback
return () => {
process.env.log_level = oldLevel;
}
};
================================================
FILE: lib/lru.js
================================================
module.exports = function(size) {
return new LruCache(size)
}
function LruCache(size) {
this.capacity = size | 0
this.map = Object.create(null)
this.list = new DoublyLinkedList()
}
LruCache.prototype.get = function(key) {
var node = this.map[key]
if (node == null) return undefined
this.used(node)
return node.val
}
LruCache.prototype.set = function(key, val) {
var node = this.map[key]
if (node != null) {
node.val = val
} else {
if (!this.capacity) this.prune()
if (!this.capacity) return false
node = new DoublyLinkedNode(key, val)
this.map[key] = node
this.capacity--
}
this.used(node)
return true
}
LruCache.prototype.used = function(node) {
this.list.moveToFront(node)
}
LruCache.prototype.prune = function() {
var node = this.list.pop()
if (node != null) {
delete this.map[node.key]
this.capacity++
}
}
function DoublyLinkedList() {
this.firstNode = null
this.lastNode = null
}
DoublyLinkedList.prototype.moveToFront = function(node) {
if (this.firstNode == node) return
this.remove(node)
if (this.firstNode == null) {
this.firstNode = node
this.lastNode = node
node.prev = null
node.next = null
} else {
node.prev = null
node.next = this.firstNode
node.next.prev = node
this.firstNode = node
}
}
DoublyLinkedList.prototype.pop = function() {
var lastNode = this.lastNode
if (lastNode != null) {
this.remove(lastNode)
}
return lastNode
}
DoublyLinkedList.prototype.remove = function(node) {
if (this.firstNode == node) {
this.firstNode = node.next
} else if (node.prev != null) {
node.prev.next = node.next
}
if (this.lastNode == node) {
this.lastNode = node.prev
} else if (node.next != null) {
node.next.prev = node.prev
}
}
function DoublyLinkedNode(key, val) {
this.key = key
this.val = val
this.prev = null
this.next = null
}
================================================
FILE: lib/notify.js
================================================
'use strict';
const _ = require('lodash');
const co = require('co');
const sns = require('./sns');
const kinesis = require('./kinesis');
const chance = require('chance').Chance();
const log = require('./log');
const cloudwatch = require('./cloudwatch');
const streamName = process.env.order_events_stream;
const restaurantTopicArn = process.env.restaurant_notification_topic;
const userTopicArn = process.env.user_notification_topic;
let notifyRestaurantOfOrder = co.wrap(function* (order) {
try {
if (chance.bool({likelihood: 75})) { // 75% chance of failure
throw new Error("boom");
}
let snsReq = {
Message: JSON.stringify(order),
TopicArn: restaurantTopicArn
};
yield cloudwatch.trackExecTime(
"SnsPublishLatency",
() => sns.publish(snsReq).promise()
);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.debug('notified restaurant of new order', logContext);
let data = _.clone(order);
data.eventType = 'restaurant_notified';
let kinesisReq = {
Data: JSON.stringify(data), // the SDK would base64 encode this for us
PartitionKey: order.orderId,
StreamName: streamName
};
yield cloudwatch.trackExecTime(
"KinesisPutRecordLatency",
() => kinesis.putRecord(kinesisReq).promise()
);
log.debug('published event into Kinesis', { eventName: 'restaurant_notified' });
cloudwatch.incrCount('NotifyRestaurantSuccess');
} catch (err) {
cloudwatch.incrCount('NotifyRestaurantFailed');
throw err;
}
});
let notifyUserOfOrderAccepted = co.wrap(function* (order) {
try {
if (chance.bool({likelihood: 75})) { // 75% chance of failure
throw new Error("boom");
}
let snsReq = {
Message: JSON.stringify(order),
TopicArn: userTopicArn
};
yield cloudwatch.trackExecTime(
"SnsPublishLatency",
() => sns.publish(snsReq).promise()
);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.debug('notified user of accepted order', logContext);
let data = _.clone(order);
data.eventType = 'user_notified';
let kinesisReq = {
Data: JSON.stringify(data), // the SDK would base64 encode this for us
PartitionKey: order.orderId,
StreamName: streamName
};
yield cloudwatch.trackExecTime(
"KinesisPutRecordLatency",
() => kinesis.putRecord(kinesisReq).promise()
);
log.debug(`published event into Kinesis`, { eventName: 'user_notified' });
cloudwatch.incrCount('NotifyUserSuccess');
} catch (err) {
cloudwatch.incrCount('NotifyUserFailed');
throw err;
}
});
module.exports = {
restaurantOfOrder: notifyRestaurantOfOrder,
userOfOrderAccepted: notifyUserOfOrderAccepted
};
================================================
FILE: lib/retry.js
================================================
'use strict';
const co = require('co');
const sns = require('./sns');
const log = require('./log');
const cloudwatch = require('./cloudwatch');
const restaurantRetryTopicArn = process.env.restaurant_notification_retry_topic;
const userRetryTopicArn = process.env.user_notification_retry_topic;
let retryRestaurantNotification = co.wrap(function* (order) {
let snsReq = {
Message: JSON.stringify(order),
TopicArn: restaurantRetryTopicArn
};
yield cloudwatch.trackExecTime(
"SnsPublishLatency",
() => sns.publish(snsReq).promise()
);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.debug('queued restaurant notification for retry', logContext);
cloudwatch.incrCount("NotifyRestaurantQueued");
});
let retryUserNotification = co.wrap(function* (order) {
let snsReq = {
Message: JSON.stringify(order),
TopicArn: userRetryTopicArn
};
yield cloudwatch.trackExecTime(
"SnsPublishLatency",
() => sns.publish(snsReq).promise()
);
let logContext = {
orderId: order.orderId,
restaurantName: order.restaurantName,
userEmail: order.userEmail
};
log.debug('queued user notification for retry', logContext);
cloudwatch.incrCount("NotifyUserQueued");
});
module.exports = {
restaurantNotification: retryRestaurantNotification,
userNotification: retryUserNotification
};
================================================
FILE: lib/sns.js
================================================
'use strict';
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const SNS = new AWS.SNS();
const correlationIds = require('./correlation-ids');
function addCorrelationIds(messageAttributes) {
let attributes = {};
let context = correlationIds.get();
for (let key in context) {
attributes[key] = {
DataType: 'String',
StringValue: context[key]
};
}
// use `attribtues` as base so if the user's message attributes would override
// our correlation IDs
return Object.assign(attributes, messageAttributes || {});
}
function publish(params, cb) {
const newMessageAttributes = addCorrelationIds(params.MessageAttributes);
params = Object.assign(params, { MessageAttributes: newMessageAttributes });
return SNS.publish(params, cb);
};
const client = Object.assign({}, SNS, { publish });
module.exports = client;
================================================
FILE: middleware/capture-correlation-ids.js
================================================
'use strict';
const correlationIds = require('../lib/correlation-ids');
const log = require('../lib/log');
function captureHttp(headers, awsRequestId, sampleDebugLogRate) {
if (!headers) {
log.warn(`Request ${awsRequestId} is missing headers`);
return;
}
let context = { awsRequestId };
for (const header in headers) {
if (header.toLowerCase().startsWith('x-correlation-')) {
context[header] = headers[header];
}
}
if (!context['x-correlation-id']) {
context['x-correlation-id'] = awsRequestId;
}
// forward the original User-Agent on
if (headers['User-Agent']) {
context['User-Agent'] = headers['User-Agent'];
}
if (headers['Debug-Log-Enabled']) {
context['Debug-Log-Enabled'] = headers['Debug-Log-Enabled'];
} else {
context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
}
correlationIds.replaceAllWith(context);
}
function parsePayload (record) {
let json = new Buffer(record.kinesis.data, 'base64').toString('utf8');
return JSON.parse(json);
}
function captureKinesis(event, context, sampleDebugLogRate) {
const awsRequestId = context.awsRequestId;
const events = event
.Records
.map(parsePayload)
.map(record => {
// the wrapped kinesis client would put the correlation IDs as part of
// the payload as a special __context__ property
let recordContext = record.__context__ || {};
recordContext.awsRequestId = awsRequestId;
delete record.__context__;
if (!recordContext['x-correlation-id']) {
recordContext['x-correlation-id'] = awsRequestId;
}
if (!recordContext['Debug-Log-Enabled']) {
recordContext['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
}
let debugLog = recordContext['Debug-Log-Enabled'] === 'true';
let oldContext = undefined;
let debugLogRollback = undefined;
// lets you add more correlation IDs for just this record
record.addToScope = (key, value) => {
if (!key.startsWith("x-correlation-")) {
key = "x-correlation-" + key;
}
recordContext[key] = value;
correlationIds.set(key, value);
}
record.scopeToThis = () => {
if (!oldContext) {
oldContext = correlationIds.get();
correlationIds.replaceAllWith(recordContext);
}
if (debugLog) {
debugLogRollback = log.enableDebug();
}
};
record.unscope = () => {
if (oldContext) {
correlationIds.replaceAllWith(oldContext);
}
if (debugLogRollback) {
debugLogRollback();
}
}
return record;
});
context.parsedKinesisEvents = events;
correlationIds.replaceAllWith({ awsRequestId });
}
function captureSns(records, awsRequestId, sampleDebugLogRate) {
let context = { awsRequestId };
const snsRecord = records[0].Sns;
const msgAttributes = snsRecord.MessageAttributes;
for (var msgAttribute in msgAttributes) {
if (msgAttribute.toLowerCase().startsWith('x-correlation-')) {
context[msgAttribute] = msgAttributes[msgAttribute].Value;
}
if (msgAttribute === 'User-Agent') {
context['User-Agent'] = msgAttributes['User-Agent'].Value;
}
if (msgAttribute === 'Debug-Log-Enabled') {
context['Debug-Log-Enabled'] = msgAttributes['Debug-Log-Enabled'].Value;
}
}
if (!context['x-correlation-id']) {
context['x-correlation-id'] = awsRequestId;
}
if (!context['Debug-Log-Enabled']) {
context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
}
correlationIds.replaceAllWith(context);
}
function isApiGatewayEvent(event) {
return event.hasOwnProperty('httpMethod')
}
function isKinesisEvent(event) {
if (!event.hasOwnProperty('Records')) {
return false;
}
if (!Array.isArray(event.Records)) {
return false;
}
return event.Records[0].eventSource === 'aws:kinesis';
}
function isSnsEvent(event) {
if (!event.hasOwnProperty('Records')) {
return false;
}
if (!Array.isArray(event.Records)) {
return false;
}
return event.Records[0].EventSource === 'aws:sns';
}
module.exports = (config) => {
const sampleDebugLogRate = config.sampleDebugLogRate || 0.01;
return {
before: (handler, next) => {
correlationIds.clearAll();
if (isApiGatewayEvent(handler.event)) {
captureHttp(handler.event.headers, handler.context.awsRequestId, sampleDebugLogRate);
} else if (isKinesisEvent(handler.event)) {
captureKinesis(handler.event, handler.context, sampleDebugLogRate);
} else if (isSnsEvent(handler.event)) {
captureSns(handler.event.Records, handler.context.awsRequestId, sampleDebugLogRate);
}
next()
}
};
};
================================================
FILE: middleware/flush-metrics.js
================================================
'use strict';
const log = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
module.exports = {
after: (handler, next) => {
cloudwatch.flush().then(_ => next());
},
onError: (handler, next) => {
cloudwatch.flush().then(_ => next(handler.error));
}
};
================================================
FILE: middleware/function-shield.js
================================================
'use strict';
const FuncShield = require('@puresec/function-shield');
module.exports = () => {
return {
before: (handler, next) => {
FuncShield.configure({
policy: {
outbound_connectivity: "block",
read_write_tmp: "block",
create_child_process: "block"
},
token: process.env.FUNCTION_SHIELD_TOKEN
});
next();
}
};
};
================================================
FILE: middleware/sample-logging.js
================================================
'use strict';
const correlationIds = require('../lib/correlation-ids');
const log = require('../lib/log');
// config should be { sampleRate: double } where sampleRate is between 0.0-1.0
module.exports = (config) => {
let rollback = undefined;
const isDebugEnabled = () => {
const context = correlationIds.get();
if (context['Debug-Log-Enabled'] === 'true') {
return true;
}
return config.sampleRate && Math.random() <= config.sampleRate;
}
return {
before: (handler, next) => {
if (isDebugEnabled()) {
rollback = log.enableDebug();
}
next();
},
after: (handler, next) => {
if (rollback) {
rollback();
}
next();
},
onError: (handler, next) => {
let awsRequestId = handler.context.awsRequestId;
let invocationEvent = JSON.stringify(handler.event);
log.error('invocation failed', { awsRequestId, invocationEvent }, handler.error);
next(handler.error);
}
};
};
================================================
FILE: middleware/wrapper.js
================================================
'use strict';
const middy = require('middy');
const sampleLogging = require('./sample-logging');
const captureCorrelationIds = require('./capture-correlation-ids');
const functionShield = require('./function-shield');
module.exports = f => {
return middy(f)
.use(captureCorrelationIds({ sampleDebugLogRate: 0.01 }))
.use(sampleLogging({ sampleRate: 0.01 }))
.use(functionShield());
};
================================================
FILE: package.json
================================================
{
"name": "big-mouth",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"integration-test": "env TEST_MODE=handler ./node_modules/.bin/mocha tests/test_cases --reporter spec --retries 3 --timeout 10000",
"acceptance-test": "env TEST_MODE=http TEST_ROOT=https://i3c5h755j0.execute-api.us-east-1.amazonaws.com/dev ./node_modules/.bin/mocha tests/test_cases --reporter spec --retries 3 --timeout 10000"
},
"author": "",
"license": "ISC",
"dependencies": {
"@puresec/function-shield": "^1.0.7",
"aws-xray-sdk": "^1.2.0",
"bluebird": "^3.5.1",
"chance": "^1.0.13",
"co": "^4.6.0",
"middy": "^0.17.1",
"mustache": "^2.3.0",
"superagent": "^3.8.1",
"superagent-promise": "^1.1.0"
},
"devDependencies": {
"aws-sdk": "^2.302.0",
"chai": "^4.1.2",
"cheerio": "^1.0.0-rc.2",
"lodash": "^4.17.10",
"mocha": "^4.0.1",
"serverless": "^1.30.1",
"serverless-iam-roles-per-function": "^0.1.5",
"serverless-plugin-aws-alerts": "^1.2.4",
"serverless-plugin-canary-deployments": "^0.4.3",
"serverless-plugin-tracing": "^2.0.0",
"serverless-pseudo-parameters": "^1.2.5",
"serverless-sam": "0.0.3"
}
}
================================================
FILE: seed-restaurants.js
================================================
'use strict';
const co = require('co');
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const dynamodb = new AWS.DynamoDB.DocumentClient();
let restaurants = [
{
name: "Fangtasia",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/fangtasia.png",
themes: ["true blood"]
},
{
name: "Shoney's",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/shoney's.png",
themes: ["cartoon", "rick and morty"]
},
{
name: "Freddy's BBQ Joint",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/freddy's+bbq+joint.png",
themes: ["netflix", "house of cards"]
},
{
name: "Pizza Planet",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/pizza+planet.png",
themes: ["netflix", "toy story"]
},
{
name: "Leaky Cauldron",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/leaky+cauldron.png",
themes: ["movie", "harry potter"]
},
{
name: "Lil' Bits",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/lil+bits.png",
themes: ["cartoon", "rick and morty"]
},
{
name: "Fancy Eats",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/fancy+eats.png",
themes: ["cartoon", "rick and morty"]
},
{
name: "Don Cuco",
image: "https://d2qt42rcwzspd6.cloudfront.net/manning/don%20cuco.png",
themes: ["cartoon", "rick and morty"]
},
];
let putReqs = restaurants.map(x => ({
PutRequest: {
Item: x
}
}));
let req = {
RequestItems: {
'restaurants': putReqs
}
};
dynamodb.batchWrite(req).promise().then(() => console.log("all done"));
================================================
FILE: serverless.yml
================================================
service: big-mouth
plugins:
- serverless-pseudo-parameters
- serverless-sam
- serverless-iam-roles-per-function
- serverless-plugin-tracing
- serverless-plugin-canary-deployments
- serverless-plugin-aws-alerts
custom:
stage: ${opt:stage, self:provider.stage}
region: ${opt:region}
logLevel:
prod: WARN
default: DEBUG
serverless-iam-roles-per-function:
defaultInherit: true
alerts:
stages:
- dev
- staging
- production
dashboards: false
alarms:
- functionThrottles
- functionErrors
provider:
name: aws
runtime: nodejs6.10
tracing: true
environment:
log_level: ${self:custom.logLevel.${self:custom.stage}, self:custom.logLevel.default}
STAGE: ${self:custom.stage}
FUNCTION_SHIELD_TOKEN: ${ssm:/bigmouth/${self:custom.stage}/function_shield_token~true}
iamRoleStatements:
- Effect: Allow
Action: cloudwatch:PutMetricData
Resource: '*'
- Effect: Allow
Action:
- 'xray:PutTraceSegments'
- 'xray:PutTelemetryRecords'
Resource: '*'
- Effect: Allow
Action: codedeploy:*
Resource: '*'
functions:
get-index:
handler: functions/get-index.handler
events:
- http:
path: /
method: get
environment:
async_metrics: true
deploymentSettings:
type: Linear10PercentEvery1Minute
alias: Live
alarms:
- GetDashindexFunctionErrorsAlarm
#iamRoleStatementsInherit: true #optionally inherit shared permissions
iamRoleStatements:
- Effect: Allow
Action: execute-api:Invoke
Resource: arn:aws:execute-api:#{AWS::Region}:#{AWS::AccountId}:*/*/GET/restaurants
- Effect: Allow
Action: ssm:GetParameters*
Resource: arn:aws:ssm:#{AWS::Region}:#{AWS::AccountId}:parameter/bigmouth/${self:custom.stage}/*
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:#{AWS::Region}:#{AWS::AccountId}:secret:/bigmouth/${self:custom.stage}/*
get-restaurants:
handler: functions/get-restaurants.handler
events:
- http:
path: /restaurants/
method: get
authorizer: aws_iam
environment:
restaurants_table: restaurants
async_metrics: true
iamRoleStatements:
- Effect: Allow
Action: dynamodb:scan
Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants
search-restaurants:
handler: functions/search-restaurants.handler
events:
- http:
path: /restaurants/search
method: post
authorizer:
arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB
environment:
restaurants_table: restaurants
async_metrics: true
iamRoleStatements:
- Effect: Allow
Action: dynamodb:scan
Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants
place-order:
handler: functions/place-order.handler
events:
- http:
path: /orders
method: post
authorizer:
arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB
environment:
order_events_stream: order-events
async_metrics: true
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
notify-restaurant:
handler: functions/notify-restaurant.handler
events:
- stream:
arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
environment:
order_events_stream: order-events
restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
restaurant_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
- Effect: Allow
Action: sns:Publish
Resource:
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry
retry-notify-restaurant:
handler: functions/retry-notify-restaurant.handler
events:
- sns: restaurant-notification-retry
environment:
order_events_stream: order-events
restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-dlq
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
- Effect: Allow
Action: sns:Publish
Resource:
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
accept-order:
handler: functions/accept-order.handler
events:
- http:
path: /orders/accept
method: post
environment:
order_events_stream: order-events
async_metrics: true
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
notify-user:
handler: functions/notify-user.handler
events:
- stream:
arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
environment:
order_events_stream: order-events
user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
user_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
- Effect: Allow
Action: sns:Publish
Resource:
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry
retry-notify-user:
handler: functions/retry-notify-user.handler
events:
- sns: user-notification-retry
environment:
order_events_stream: order-events
user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-dlq
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
- Effect: Allow
Action: sns:Publish
Resource:
- arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
fulfill-order:
handler: functions/fulfill-order.handler
events:
- http:
path: /orders/complete
method: post
environment:
order_events_stream: order-events
async_metrics: true
iamRoleStatements:
- Effect: Allow
Action: kinesis:PutRecord
Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
auto-create-api-alarms:
handler: functions/create-alarms.handler
events:
- cloudwatchEvent:
event:
source:
- aws.apigateway
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- apigateway.amazonaws.com
eventName:
- CreateDeployment
environment:
alarm_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe
ok_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe
iamRoleStatements:
- Effect: Allow
Action: apigateway:GET
Resource:
- arn:aws:apigateway:#{AWS::Region}::/restapis/*
- arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}
- Effect: Allow
Action: apigateway:PATCH
Resource: arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}
- Effect: Allow
Action: cloudwatch:PutMetricAlarm
Resource: "*"
resources:
Resources:
restaurantsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: restaurants
AttributeDefinitions:
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: name
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
orderEventsStream:
Type: AWS::Kinesis::Stream
Properties:
Name: order-events
ShardCount: 1
restaurantNotificationTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: restaurant-notification
TopicName: restaurant-notification
userNotificationTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: user-notification
TopicName: user-notification
restaurantNotificationDLQTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: restaurant-notification-dlq
TopicName: restaurant-notification-dlq
userNotificationDLQTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: user-notification-dlq
TopicName: user-notification-dlq
================================================
FILE: static/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Big Mouth</title>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.149.0.min.js"></script>
<script src="https://d2qt42rcwzspd6.cloudfront.net/manning/aws-cognito-sdk.min.js"></script>
<script src="https://d2qt42rcwzspd6.cloudfront.net/manning/amazon-cognito-identity.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
integrity="sha384-Dziy8F2VlJQLMShA6FHWNul/veM9bCkRUaLqr199K94ntO5QUrLJBEbYegdSkkqX"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<style>
.fullscreenDiv {
background-color: #05bafd;
width: 100%;
height: auto;
bottom: 0px;
top: 0px;
left: 0;
position: absolute;
}
.restaurantsDiv {
background-color: #ffffff;
width: 100%;
height: auto;
}
.dayOfWeek {
font-family: Arial, Helvetica, sans-serif;
font-size: 32px;
padding: 10px;
height: auto;
display: flex;
justify-content: center;
}
.column-container {
padding: 0;
margin: 0;
list-style: none;
display: flex;
flex-flow: column;
flex-wrap: wrap;
justify-content: center;
}
.row-container {
padding: 5px;
margin: 5px;
list-style: none;
display: flex;
flex-flow: row;
flex-wrap: wrap;
justify-content: center;
}
.item {
padding: 5px;
height: auto;
margin-top: 10px;
display: flex;
flex-flow: row;
flex-wrap: wrap;
justify-content: center;
}
.restaurant {
background-color: #00a8f7;
border-radius: 10px;
padding: 5px;
height: auto;
width: auto;
margin-left: 40px;
margin-right: 40px;
margin-top: 15px;
margin-bottom: 0px;
display: flex;
justify-content: center;
}
.restaurant-name {
font-size: 24px;
font-family:Arial, Helvetica, sans-serif;
color: #ffffff;
padding: 10px;
margin: 0px;
}
.restaurant-image {
padding-top: 0px;
margin-top: 0px;
}
.row-container-left {
list-style: none;
display: flex;
flex-flow: row;
justify-content: flex-start;
}
.menu-text {
font-family: Arial, Helvetica, sans-serif;
font-size: 24px;
font-weight: bold;
color: white;
}
.text-trail-space {
margin-right: 10px;
}
.hidden {
display: none;
}
lable, button, input {
display:block;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
}
fieldset {
padding:0;
border:0;
margin-top:25px;
}
</style>
<script>
const AWS_REGION = '{{awsRegion}}';
const COGNITO_USER_POOL_ID = '{{cognitoUserPoolId}}';
const CLIENT_ID = '{{cognitoClientId}}';
const SEARCH_URL = '{{& searchUrl}}';
const PLACE_ORDER_URL = '{{& placeOrderUrl}}';
var regDialog, regForm;
var verifyDialog;
var regCompleteDialog;
var signInDialog;
var userPool, cognitoUser;
var idToken;
function toggleSignOut (enable) {
enable === true ? $('#sign-out').show() : $('#sign-out').hide();
}
function toggleSignIn (enable) {
enable === true ? $('#sign-in').show() : $('#sign-in').hide();
}
function toggleRegister (enable) {
enable === true ? $('#register').show() : $('#register').hide();
}
function init() {
AWS.config.region = AWS_REGION;
AWSCognito.config.region = AWS_REGION;
var data = {
UserPoolId : COGNITO_USER_POOL_ID,
ClientId : CLIENT_ID
};
userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(data);
cognitoUser = userPool.getCurrentUser();
if (cognitoUser != null) {
cognitoUser.getSession(function(err, session) {
if (err) {
alert(err);
return;
}
idToken = session.idToken.jwtToken;
console.log('idToken: ' + idToken);
console.log('session validity: ' + session.isValid());
});
toggleSignOut(true);
toggleSignIn(false);
toggleRegister(false);
} else {
toggleSignOut(false);
toggleSignIn(true);
toggleRegister(true);
}
}
function addUser() {
var firstName = $("#first-name")[0].value;
var lastName = $("#last-name")[0].value;
var username = $("#username")[0].value;
var password = $("#password")[0].value;
var email = $("#email")[0].value;
var attributeList = [
new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({
Name : 'email', Value : email
}),
new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({
Name : 'given_name', Value : firstName
}),
new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({
Name : 'family_name', Value : lastName
}),
];
userPool.signUp(username, password, attributeList, null, function(err, result){
if (err) {
alert(err);
return;
}
cognitoUser = result.user;
console.log('user name is ' + cognitoUser.getUsername());
regDialog.dialog("close");
verifyDialog.dialog("open");
});
}
function confirmUser() {
var verificationCode = $("#verification-code")[0].value;
cognitoUser.confirmRegistration(verificationCode, true, function(err, result) {
if (err) {
alert(err);
return;
}
console.log('verification call result: ' + result);
verifyDialog.dialog("close");
regCompleteDialog.dialog("open");
});
}
function authenticateUser() {
var username = $("#sign-in-username")[0].value;
var password = $("#sign-in-password")[0].value;
var authenticationData = {
Username : username,
Password : password,
};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
var userData = {
Username : username,
Pool : userPool
};
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
console.log('access token : ' + result.getAccessToken().getJwtToken());
/*Use the idToken for Logins Map when Federating User Pools with Cognito Identity or when passing through an Authorization Header to an API Gateway Authorizer*/
idToken = result.idToken.jwtToken;
console.log('idToken : ' + idToken);
signInDialog.dialog("close");
toggleRegister(false);
toggleSignIn(false);
toggleSignOut(true);
},
onFailure: function(err) {
alert(err);
}
});
}
function signOut() {
if (cognitoUser != null) {
cognitoUser.signOut();
toggleRegister(true);
toggleSignIn(true);
toggleSignOut(false);
}
}
function searchRestaurants() {
var theme = $("#theme")[0].value;
var xhr = new XMLHttpRequest();
xhr.open('POST', SEARCH_URL, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", idToken);
xhr.send(JSON.stringify({ theme }));
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4 && xhr.status === 200) {
var restaurants = JSON.parse(xhr.responseText);
var restaurantsList = $("#restaurantsUl");
restaurantsList.empty();
for (var restaurant of restaurants) {
restaurantsList.append(`
<li class="restaurant">
<ul class="column-container" onclick='placeOrder("${restaurant.name}")'>
<li class="item restaurant-name">${restaurant.name}</li>
<li class="item restaurant-image">
<img src="${restaurant.image}">
</li>
</ul>
</li>
`);
}
} else if (xhr.readyState === 4) {
alert(xhr.responseText);
}
};
}
function placeOrder(restaurantName) {
var xhr = new XMLHttpRequest();
xhr.open('POST', PLACE_ORDER_URL, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", idToken);
xhr.send(JSON.stringify({ restaurantName }));
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4 && xhr.status === 200) {
alert("your order has been placed, we'll let you know once it's been accepted by the restaurant!");
} else if (xhr.readyState === 4) {
alert(xhr.responseText);
}
};
}
$(document).ready(function() {
regDialog = $("#reg-dialog-form").dialog({
autoOpen: false,
modal: true,
buttons: {
"Create an account": addUser,
Cancel: function() {
regDialog.dialog("close");
}
},
close: function() {
regForm[0].reset();
}
});
regForm = regDialog.find("form").on("submit", function(event) {
event.preventDefault();
addUser();
});
$("#register").on("click", function() {
regDialog.dialog("open");
});
verifyDialog = $("#verify-dialog-form").dialog({
autoOpen: false,
modal: true,
buttons: {
"Confirm registration": confirmUser,
Cancel: function() {
verifyDialog.dialog("close");
}
},
close: function() {
$(this).dialog("close");
}
});
regCompleteDialog = $("#registered-message").dialog({
autoOpen: false,
modal: true,
buttons: {
Ok: function() {
$(this).dialog("close");
}
}
});
signInDialog = $("#sign-in-form").dialog({
autoOpen: false,
modal: true,
buttons: {
"Sign in": authenticateUser,
Cancel: function() {
signInDialog.dialog("close");
}
},
close: function() {
$(this).dialog("close");
}
});
$("#sign-in").on("click", function() {
signInDialog.dialog("open");
});
$("#sign-out").on("click", function() {
signOut();
})
init();
});
</script>
</head>
<body>
<div class="fullscreenDiv">
<ul class="column-container">
<li>
<ul class="row-container-left">
<li id="register" class="item text-trail-space hidden">
<a class="menu-text" href="#">Register</a>
</li>
<li id="sign-in" class="item menu-text text-trail-space hidden">
<a class="menu-text" href="#">Sign in</a>
</li>
<li id="sign-out" class="item menu-text text-trail-space hidden">
<a class="menu-text" href="#">Sign out</a>
</li>
</ul>
</li>
<li class="item">
<img id="logo" src="https://d2qt42rcwzspd6.cloudfront.net/manning/big-mouth.png">
</li>
<li class="item">
<input id="theme" type="text" size="50" placeholder="enter a theme, eg. rick and morty"/>
<button onclick="searchRestaurants()">Find Restaurants</button>
</li>
<li>
<div class="restaurantsDiv column-container">
<b class="dayOfWeek">{{dayOfWeek}}</b>
<ul id="restaurantsUl" class="row-container">
{{#restaurants}}
<li class="restaurant">
<ul class="column-container" onclick='placeOrder("{{name}}")'>
<li class="item restaurant-name">{{name}}</li>
<li class="item restaurant-image">
<img src="{{image}}">
</li>
</ul>
</li>
{{/restaurants}}
</ul>
</div>
</li>
</ul>
</div>
<div id="reg-dialog-form" title="Register">
<form>
<fieldset>
<label for="first-name">First Name</label>
<input type="text" id="first-name" class="text ui-widget-content ui-corner-all">
<label for="last-name">Last Name</label>
<input type="text" id="last-name" class="text ui-widget-content ui-corner-all">
<label for="email">Email</label>
<input type="text" name="email" id="email" class="text ui-widget-content ui-corner-all">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="text ui-widget-content ui-corner-all">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="text ui-widget-content ui-corner-all">
</fieldset>
</form>
</div>
<div id="verify-dialog-form" title="Verify">
<form>
<fieldset>
<label for="verification-code">Verification Code</label>
<input type="text" id="verification-code" class="text ui-widget-content ui-corner-all">
</fieldset>
</form>
</div>
<div id="registered-message" title="Registration complete!">
<p>
<span class="ui-icon ui-icon-circle-check" style="float:left; margin:0 7px 50px 0;"></span>
You are now registered!
</p>
</div>
<div id="sign-in-form" title="Sign in">
<form>
<fieldset>
<label for="sign-in-username">Username</label>
<input type="text" id="sign-in-username" class="text ui-widget-content ui-corner-all">
<label for="sign-in-password">Password</label>
<input type="password" id="sign-in-password" class="text ui-widget-content ui-corner-all">
</fieldset>
</form>
</div>
</body>
</html>
================================================
FILE: template.yml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'SAM template for Serverless framework service: '
Resources:
restaurantsTable:
Type: 'AWS::DynamoDB::Table'
Properties:
TableName: restaurants
AttributeDefinitions:
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: name
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
GetIndex:
Type: 'AWS::Serverless::Function'
Properties:
Handler: functions/get-index.handler
Runtime: nodejs6.10
CodeUri: >-
/Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
MemorySize: 128
Timeout: 3
Policies:
- Effect: Allow
Action: 'dynamodb:scan'
Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
Environment:
Variables:
restaurants_api: https://i3c5h755j0.execute-api.us-east-1.amazonaws.com/dev/restaurants
cognito_user_pool_id: us-east-1_DfuAwa0vB
cognito_client_id: 1tf8pb1s1n53p7u608e7ic5ih8
Events:
Event1:
Type: Api
Properties:
Path: /
Method: get
GetRestaurants:
Type: 'AWS::Serverless::Function'
Properties:
Handler: functions/get-restaurants.handler
Runtime: nodejs6.10
CodeUri: >-
/Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
MemorySize: 128
Timeout: 3
Policies:
- Effect: Allow
Action: 'dynamodb:scan'
Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
Environment:
Variables:
restaurants_table: restaurants
Events:
Event1:
Type: Api
Properties:
Path: /restaurants/
Method: get
Aws_iamResourcePolicy:
Type: 'AWS::Lambda::Permission'
Properties:
Action: 'lambda:InvokeFunction'
FunctionName:
'Fn::GetAtt':
- Aws_iam
- Arn
Principal: apigateway.amazonaws.com
SourceAccount:
Ref: 'AWS::AccountId'
SearchRestaurants:
Type: 'AWS::Serverless::Function'
Properties:
Handler: functions/search-restaurants.handler
Runtime: nodejs6.10
CodeUri: >-
/Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
MemorySize: 128
Timeout: 3
Policies:
- Effect: Allow
Action: 'dynamodb:scan'
Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
Environment:
Variables:
restaurants_table: restaurants
Events:
Event1:
Type: Api
Properties:
Path: /restaurants/search
Method: post
================================================
FILE: tests/steps/given.js
================================================
'use strict';
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const cognito = new AWS.CognitoIdentityServiceProvider();
const chance = require('chance').Chance();
let random_password = () => {
// needs number, special char, upper and lower case
return `${chance.string({ length: 8})}B!gM0uth`;
}
let an_authenticated_user = function* () {
let userpoolId = process.env.cognito_user_pool_id;
let clientId = process.env.cognito_server_client_id;
let firstName = chance.first();
let lastName = chance.last();
let username = `test-${firstName}-${lastName}-${chance.string({length: 8})}`;
let password = random_password();
let email = `${firstName}-${lastName}@big-mouth.com`;
let createReq = {
UserPoolId : userpoolId,
Username : username,
MessageAction : 'SUPPRESS',
TemporaryPassword : password,
UserAttributes : [
{ Name: "given_name", Value: firstName },
{ Name: "family_name", Value: lastName },
{ Name: "email", Value: email }
]
};
yield cognito.adminCreateUser(createReq).promise();
console.log(`[${username}] - user is created`);
let req = {
AuthFlow : 'ADMIN_NO_SRP_AUTH',
UserPoolId : userpoolId,
ClientId : clientId,
AuthParameters : {
USERNAME: username,
PASSWORD: password
}
};
let resp = yield cognito.adminInitiateAuth(req).promise();
console.log(`[${username}] - initialised auth flow`);
let challengeReq = {
UserPoolId : userpoolId,
ClientId : clientId,
ChallengeName : resp.ChallengeName,
Session : resp.Session,
ChallengeResponses : {
USERNAME: username,
NEW_PASSWORD: random_password()
}
};
let challengeResp = yield cognito.adminRespondToAuthChallenge(challengeReq).promise();
console.log(`[${username}] - responded to auth challenge`);
return {
username,
firstName,
lastName,
idToken: challengeResp.AuthenticationResult.IdToken
};
};
module.exports = {
an_authenticated_user
};
================================================
FILE: tests/steps/init.js
================================================
'use strict';
const _ = require('lodash');
const co = require('co');
const Promise = require('bluebird');
const aws4 = require('../../lib/aws4');
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const SSM = new AWS.SSM();
let initialized = false;
const getParameters = co.wrap(function* (keys) {
const prefix = '/bigmouth/dev/';
const req = {
Names: keys.map(key => `${prefix}${key}`)
}
const resp = yield SSM.getParameters(req).promise();
return _.reduce(resp.Parameters, function(obj, param) {
obj[param.Name.substr(prefix.length)] = param.Value
return obj;
}, {})
});
let init = co.wrap(function* () {
if (initialized) {
return;
}
const params = yield getParameters([
'cognito_client_id',
'cognito_user_pool_id',
'restaurants_api'
]);
process.env.restaurants_api = params.restaurants_api;
process.env.restaurants_table = "restaurants";
process.env.AWS_REGION = "us-east-1";
process.env.cognito_client_id = params.cognito_client_id;
process.env.cognito_user_pool_id = params.cognito_user_pool_id;
process.env.cognito_server_client_id = "niv7esuaibla0tj5q36b6mvnr";
process.env.AWS_XRAY_CONTEXT_MISSING = "LOG_ERROR";
process.env.STAGE = 'dev';
yield aws4.init();
initialized = true;
});
module.exports.init = init;
================================================
FILE: tests/steps/tearDown.js
================================================
'use strict';
const co = require('co');
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const cognito = new AWS.CognitoIdentityServiceProvider();
let an_authenticated_user = function* (user) {
let req = {
UserPoolId: process.env.cognito_user_pool_id,
Username: user.username
};
yield cognito.adminDeleteUser(req).promise();
console.log(`[${user.username}] - user deleted`);
};
module.exports = {
an_authenticated_user
};
================================================
FILE: tests/steps/when.js
================================================
'use strict';
const APP_ROOT = '../../';
const _ = require('lodash');
const co = require('co');
const Promise = require("bluebird");
const http = require('superagent-promise')(require('superagent'), Promise);
const aws4 = require('../../lib/aws4');
const URL = require('url');
const mode = process.env.TEST_MODE;
let respondFrom = function (httpRes) {
let contentType = _.get(httpRes, 'headers.content-type', 'application/json');
let body =
contentType === 'application/json'
? httpRes.body
: httpRes.text;
return {
statusCode: httpRes.status,
body: body,
headers: httpRes.headers
};
}
let signHttpRequest = (url, httpReq) => {
let urlData = URL.parse(url);
let opts = {
host: urlData.hostname,
path: urlData.pathname
};
aws4.sign(opts);
httpReq
.set('Host', opts.headers['Host'])
.set('X-Amz-Date', opts.headers['X-Amz-Date'])
.set('Authorization', opts.headers['Authorization']);
if (opts.headers['X-Amz-Security-Token']) {
httpReq.set('X-Amz-Security-Token', opts.headers['X-Amz-Security-Token']);
}
}
let viaHttp = co.wrap(function* (relPath, method, opts) {
let root = process.env.TEST_ROOT;
let url = `${root}/${relPath}`;
console.log(`invoking via HTTP ${method} ${url}`);
try {
let httpReq = http(method, url);
let body = _.get(opts, "body");
if (body) {
httpReq.send(body);
}
if (_.get(opts, "iam_auth", false) === true) {
signHttpRequest(url, httpReq);
}
let authHeader = _.get(opts, "auth");
if (authHeader) {
httpReq.set('Authorization', authHeader);
}
let res = yield httpReq;
return respondFrom(res);
} catch (err) {
if (err.status) {
return {
statusCode: err.status,
headers: err.response.headers
};
} else {
throw err;
}
}
})
let viaHandler = (event, functionName) => {
let handler = require(`${APP_ROOT}/functions/${functionName}`).handler;
console.log(`invoking via handler function ${functionName}`);
return new Promise((resolve, reject) => {
let context = {};
let callback = function (err, response) {
if (err) {
reject(err);
} else {
let contentType = _.get(response, 'headers.content-type', 'application/json');
if (response.body && contentType === 'application/json') {
response.body = JSON.parse(response.body);
}
resolve(response);
}
};
handler(event, context, callback);
});
}
let we_invoke_get_index = co.wrap(function* () {
let res =
mode === 'handler'
? yield viaHandler({}, 'get-index')
: yield viaHttp('', 'GET');
return res;
});
let we_invoke_get_restaurants = co.wrap(function* () {
let res =
mode === 'handler'
? yield viaHandler({}, 'get-restaurants')
: yield viaHttp('restaurants', 'GET', { iam_auth: true });
return res;
});
let we_invoke_search_restaurants = co.wrap(function* (user, theme) {
let body = JSON.stringify({ theme });
let auth = user.idToken;
let res =
mode === 'handler'
? viaHandler({ body }, 'search-restaurants')
: viaHttp('restaurants/search', 'POST', { body, auth })
return res;
});
module.exports = {
we_invoke_get_index,
we_invoke_get_restaurants,
we_invoke_search_restaurants
};
================================================
FILE: tests/test_cases/get-index.js
================================================
'use strict';
const co = require('co');
const expect = require('chai').expect;
const when = require('../steps/when');
const init = require('../steps/init').init;
const cheerio = require('cheerio');
describe(`When we invoke the GET / endpoint`, co.wrap(function* () {
before(co.wrap(function* () {
yield init();
}));
it(`Should return the index page with 8 restaurants`, co.wrap(function* () {
let res = yield when.we_invoke_get_index();
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/html; charset=UTF-8');
expect(res.body).to.not.be.null;
let $ = cheerio.load(res.body);
let restaurants = $('.restaurant', '#restaurantsUl');
expect(restaurants.length).to.equal(8);
}));
}));
================================================
FILE: tests/test_cases/get-restaurants.js
================================================
'use strict';
const co = require('co');
const expect = require('chai').expect;
const init = require('../steps/init').init;
const when = require('../steps/when');
describe(`When we invoke the GET /restaurants endpoint`, co.wrap(function* () {
before(co.wrap(function* () {
yield init();
}));
it(`Should return an array of 8 restaurants`, co.wrap(function* () {
let res = yield when.we_invoke_get_restaurants();
expect(res.statusCode).to.equal(200);
expect(res.body).to.have.lengthOf(8);
for (let restaurant of res.body) {
expect(restaurant).to.have.property('name');
expect(restaurant).to.have.property('image');
}
}));
}));
================================================
FILE: tests/test_cases/search-restaurants.js
================================================
'use strict';
const co = require('co');
const expect = require('chai').expect;
const init = require('../steps/init').init;
const when = require('../steps/when');
const given = require('../steps/given');
const tearDown = require('../steps/tearDown');
describe(`Given an authenticated user`, co.wrap(function* () {
let user;
before(co.wrap(function* () {
yield init();
user = yield given.an_authenticated_user();
}));
after(co.wrap(function* () {
yield tearDown.an_authenticated_user(user);
}));
describe(`When we invoke the POST /restaurants/search endpoint with theme 'cartoon'`, co.wrap(function* () {
it(`Should return an array of 4 restaurants`, co.wrap(function* () {
let res = yield when.we_invoke_search_restaurants(user, 'cartoon');
expect(res.statusCode).to.equal(200);
expect(res.body).to.have.lengthOf(4);
for (let restaurant of res.body) {
expect(restaurant).to.have.property('name');
expect(restaurant).to.have.property('image');
}
}));
}));
}));
gitextract_0qv7lc5k/
├── .gitignore
├── .vscode/
│ └── launch.json
├── README.md
├── build.sh
├── buildspec.yml
├── examples/
│ ├── create-alarm.json
│ ├── get-index.json
│ ├── notify-restaurant.json
│ ├── place-order.json
│ ├── retry-notify-restaurant.json
│ └── search-restaurants.json
├── functions/
│ ├── accept-order.js
│ ├── create-alarms.js
│ ├── fulfill-order.js
│ ├── get-index.js
│ ├── get-restaurants.js
│ ├── notify-restaurant.js
│ ├── notify-user.js
│ ├── place-order.js
│ ├── retry-notify-restaurant.js
│ ├── retry-notify-user.js
│ └── search-restaurants.js
├── lib/
│ ├── aws4.js
│ ├── awscred.js
│ ├── cloudwatch.js
│ ├── correlation-ids.js
│ ├── http.js
│ ├── kinesis.js
│ ├── log.js
│ ├── lru.js
│ ├── notify.js
│ ├── retry.js
│ └── sns.js
├── middleware/
│ ├── capture-correlation-ids.js
│ ├── flush-metrics.js
│ ├── function-shield.js
│ ├── sample-logging.js
│ └── wrapper.js
├── package.json
├── seed-restaurants.js
├── serverless.yml
├── static/
│ └── index.html
├── template.yml
└── tests/
├── steps/
│ ├── given.js
│ ├── init.js
│ ├── tearDown.js
│ └── when.js
└── test_cases/
├── get-index.js
├── get-restaurants.js
└── search-restaurants.js
SYMBOL INDEX (75 symbols across 19 files)
FILE: functions/create-alarms.js
constant AWS (line 5) | const AWS = require('aws-sdk');
FILE: functions/get-index.js
constant URL (line 8) | const URL = require('url');
constant STAGE (line 16) | const STAGE = process.env.STAGE;
FILE: functions/get-restaurants.js
constant AWS (line 5) | const AWS = AWSXRay.captureAWS(require('aws-sdk'));
FILE: functions/place-order.js
constant UNAUTHORIZED (line 14) | const UNAUTHORIZED = {
FILE: functions/search-restaurants.js
constant AWS (line 5) | const AWS = AWSXRay.captureAWS(require('aws-sdk'));
FILE: lib/aws4.js
function hmac (line 11) | function hmac(key, string, encoding) {
function hash (line 15) | function hash(string, encoding) {
function encodeRfc3986 (line 20) | function encodeRfc3986(urlEncodedString) {
function RequestSigner (line 28) | function RequestSigner(request, credentials) {
function trimAll (line 258) | function trimAll(header) {
FILE: lib/awscred.js
function loadCredentialsAndRegion (line 33) | function loadCredentialsAndRegion(options, cb) {
function loadCredentials (line 52) | function loadCredentials(options, cb) {
function loadRegion (line 72) | function loadRegion(options, cb) {
function loadRegionSync (line 92) | function loadRegionSync(options) {
function loadCredentialsFromEnv (line 96) | function loadCredentialsFromEnv(options, cb) {
function loadRegionFromEnv (line 106) | function loadRegionFromEnv(options, cb) {
function loadRegionFromEnvSync (line 112) | function loadRegionFromEnvSync() {
function loadCredentialsFromIniFile (line 116) | function loadCredentialsFromIniFile(options, cb) {
function loadRegionFromIniFile (line 129) | function loadRegionFromIniFile(options, cb) {
function loadRegionFromIniFileSync (line 138) | function loadRegionFromIniFileSync(options) {
function loadCredentialsFromEcs (line 146) | function loadCredentialsFromEcs(options, cb) {
function loadCredentialsFromEc2Metadata (line 182) | function loadCredentialsFromEc2Metadata(options, cb) {
function loadProfileFromIniFile (line 223) | function loadProfileFromIniFile(options, defaultFilename, cb) {
function loadProfileFromIniFileSync (line 234) | function loadProfileFromIniFileSync(options, defaultFilename) {
function merge (line 249) | function merge(obj, options, cb) {
function resolveProfile (line 284) | function resolveProfile() {
function resolveHome (line 288) | function resolveHome() {
function parseAwsIni (line 293) | function parseAwsIni(ini) {
function request (line 313) | function request(options, cb) {
function once (line 332) | function once(cb) {
FILE: lib/cloudwatch.js
constant AWS (line 5) | const AWS = AWSXRay.captureAWS(require('aws-sdk'));
function getCountMetricData (line 26) | function getCountMetricData(name, value) {
function getTimeMetricData (line 35) | function getTimeMetricData(name, statsValues) {
function getCountMetricDatum (line 44) | function getCountMetricDatum() {
function getTimeMetricDatum (line 55) | function getTimeMetricDatum() {
function clear (line 89) | function clear() {
function incrCount (line 94) | function incrCount(metricName, count) {
function recordTimeInMillis (line 108) | function recordTimeInMillis(metricName, ms) {
function trackExecTime (line 136) | function trackExecTime(metricName, f) {
FILE: lib/http.js
function getRequest (line 6) | function getRequest (options) {
function setHeaders (line 27) | function setHeaders (request, headers) {
function setQueryStrings (line 34) | function setQueryStrings (request, qs) {
function setBody (line 42) | function setBody (request, body) {
FILE: lib/kinesis.js
constant AWS (line 5) | const AWS = AWSXRay.captureAWS(require('aws-sdk'));
function tryJsonParse (line 10) | function tryJsonParse(data) {
function addCorrelationIds (line 23) | function addCorrelationIds(data) {
function putRecord (line 35) | function putRecord(params, cb) {
FILE: lib/log.js
constant DEFAULT_CONTEXT (line 14) | const DEFAULT_CONTEXT = {
function getContext (line 22) | function getContext () {
function logLevelName (line 34) | function logLevelName() {
function isEnabled (line 38) | function isEnabled (level) {
function appendError (line 42) | function appendError(params, err) {
function log (line 54) | function log (levelName, message, params) {
FILE: lib/lru.js
function LruCache (line 5) | function LruCache(size) {
function DoublyLinkedList (line 46) | function DoublyLinkedList() {
function DoublyLinkedNode (line 91) | function DoublyLinkedNode(key, val) {
FILE: lib/sns.js
constant AWS (line 4) | const AWS = AWSXRay.captureAWS(require('aws-sdk'));
constant SNS (line 5) | const SNS = new AWS.SNS();
function addCorrelationIds (line 8) | function addCorrelationIds(messageAttributes) {
function publish (line 23) | function publish(params, cb) {
FILE: middleware/capture-correlation-ids.js
function captureHttp (line 6) | function captureHttp(headers, awsRequestId, sampleDebugLogRate) {
function parsePayload (line 37) | function parsePayload (record) {
function captureKinesis (line 42) | function captureKinesis(event, context, sampleDebugLogRate) {
function captureSns (line 107) | function captureSns(records, awsRequestId, sampleDebugLogRate) {
function isApiGatewayEvent (line 138) | function isApiGatewayEvent(event) {
function isKinesisEvent (line 142) | function isKinesisEvent(event) {
function isSnsEvent (line 154) | function isSnsEvent(event) {
FILE: seed-restaurants.js
constant AWS (line 4) | const AWS = require('aws-sdk');
FILE: tests/steps/given.js
constant AWS (line 3) | const AWS = require('aws-sdk');
FILE: tests/steps/init.js
constant AWS (line 7) | const AWS = require('aws-sdk');
constant SSM (line 9) | const SSM = new AWS.SSM();
FILE: tests/steps/tearDown.js
constant AWS (line 4) | const AWS = require('aws-sdk');
FILE: tests/steps/when.js
constant APP_ROOT (line 3) | const APP_ROOT = '../../';
constant URL (line 10) | const URL = require('url');
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
{
"path": ".gitignore",
"chars": 86,
"preview": "# package directories\nnode_modules\njspm_packages\n\n# Serverless directories\n.serverless"
},
{
"path": ".vscode/launch.json",
"chars": 3745,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n //"
},
{
"path": "README.md",
"chars": 139,
"preview": "# manning-aws-lambda-operational-patterns-and-practices\nCode for the Manning video course \"AWS Lambda: Operational Patte"
},
{
"path": "build.sh",
"chars": 597,
"preview": "#!/bin/bash\nset -e\nset -o pipefail\n\ninstruction()\n{\n echo \"usage: ./build.sh deploy <env>\"\n echo \"\"\n echo \"env: eg. i"
},
{
"path": "buildspec.yml",
"chars": 163,
"preview": "version: 0.2\n\nphases:\n build:\n commands:\n - chmod +x build.sh\n - ./build.sh int-test\n - ./build.sh de"
},
{
"path": "examples/create-alarm.json",
"chars": 2287,
"preview": "{\n \"version\": \"0\",\n \"id\": \"dee9a69c-8166-1ad7-41d4-1dad201e29f6\",\n \"detail-type\": \"AWS API Call via CloudTrail\",\n \"s"
},
{
"path": "examples/get-index.json",
"chars": 2354,
"preview": "{\n \"body\": null,\n \"resource\": \"/\",\n \"requestContext\": {\n \"resourceId\": \"123456\",\n \"apiId\": \"1234567890\",\n \"r"
},
{
"path": "examples/notify-restaurant.json",
"chars": 943,
"preview": "{\n \"Records\":[\n {\n \"kinesis\": {\n \"kinesisSchemaVersion\":\"1.0\",\n \"partitionKey\":\"e4a5eae6-19f7-528"
},
{
"path": "examples/place-order.json",
"chars": 3979,
"preview": "{\n \"resource\": \"\\/orders\",\n \"path\": \"\\/orders\",\n \"httpMethod\": \"POST\",\n \"headers\": {\n \"Accept\": \"*\\/*\",\n \"Acce"
},
{
"path": "examples/retry-notify-restaurant.json",
"chars": 732,
"preview": "{\n \"Records\": [\n {\n \"EventVersion\": \"1.0\",\n \"EventSubscriptionArn\": \"arn:aws:sns:EXAMPLE\",\n \"EventSou"
},
{
"path": "examples/search-restaurants.json",
"chars": 2440,
"preview": "{\n \"body\": \"{\\\"theme\\\":\\\"cartoon\\\"}\",\n \"resource\": \"/{proxy+}\",\n \"requestContext\": {\n \"resourceId\": \"123456\",\n "
},
{
"path": "functions/accept-order.js",
"chars": 1404,
"preview": "'use strict';\n\nconst co = require('co');\nconst kinesis = require('../lib/kinesis');\nconst log = requir"
},
{
"path": "functions/create-alarms.js",
"chars": 3492,
"preview": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst AWS = require('aws-s"
},
{
"path": "functions/fulfill-order.js",
"chars": 1416,
"preview": "'use strict';\n\nconst co = require('co');\nconst kinesis = require('../lib/kinesis');\nconst log = requir"
},
{
"path": "functions/get-index.js",
"chars": 3269,
"preview": "'use strict';\n\nconst co = require(\"co\");\nconst Promise = require(\"bluebird\");\nconst fs = Promise.prom"
},
{
"path": "functions/get-restaurants.js",
"chars": 1092,
"preview": "'use strict';\n\nconst co = require('co');\nconst AWSXRay = require('aws-xray-sdk');\nconst AWS = AWSXRay."
},
{
"path": "functions/notify-restaurant.js",
"chars": 1054,
"preview": "'use strict';\n\nconst co = require('co');\nconst notify = require('../lib/notify');\nconst retry = require('../lib/ret"
},
{
"path": "functions/notify-user.js",
"chars": 1060,
"preview": "'use strict';\n\nconst co = require('co');\nconst notify = require('../lib/notify');\nconst retry = require('../lib/ret"
},
{
"path": "functions/place-order.js",
"chars": 1806,
"preview": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst kinesis = re"
},
{
"path": "functions/retry-notify-restaurant.js",
"chars": 912,
"preview": "'use strict';\n\nconst co = require('co');\nconst notify = require('../lib/notify');\nconst log = require"
},
{
"path": "functions/retry-notify-user.js",
"chars": 902,
"preview": "'use strict';\n\nconst co = require('co');\nconst notify = require('../lib/notify');\nconst log = require"
},
{
"path": "functions/search-restaurants.js",
"chars": 1334,
"preview": "'use strict';\n\nconst co = require('co');\nconst AWSXRay = require('aws-xray-sdk');\nconst AWS = AWSXRay."
},
{
"path": "lib/aws4.js",
"chars": 11598,
"preview": "var aws4 = exports, \n url = require('url'),\n querystring = require('querystring'),\n crypto = require('crypto"
},
{
"path": "lib/awscred.js",
"chars": 9576,
"preview": "var fs = require('fs'),\n path = require('path'),\n http = require('http'),\n env = process.env\n\nexports.credentia"
},
{
"path": "lib/cloudwatch.js",
"chars": 4403,
"preview": "'use strict';\n\nconst co = require('co');\nconst AWSXRay = require('aws-xray-sdk');\nconst AWS = AWSXRay."
},
{
"path": "lib/correlation-ids.js",
"chars": 455,
"preview": "'use strict';\n\nlet clearAll = () => global.CONTEXT = undefined;\n\nlet replaceAllWith = ctx => global.CONTEXT = ctx;\n\nlet "
},
{
"path": "lib/http.js",
"chars": 1878,
"preview": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\nconst http = require('superagent-promise')(require('"
},
{
"path": "lib/kinesis.js",
"chars": 1112,
"preview": "'use strict';\n\nconst _ = require('lodash');\nconst AWSXRay = require('aws-xray-sdk');\nconst AWS "
},
{
"path": "lib/log.js",
"chars": 2215,
"preview": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\n\nconst LogLevels = {\n DEBUG : 0,\n INFO : 1,\n WAR"
},
{
"path": "lib/lru.js",
"chars": 1913,
"preview": "module.exports = function(size) {\n return new LruCache(size)\n}\n\nfunction LruCache(size) {\n this.capacity = size | 0\n "
},
{
"path": "lib/notify.js",
"chars": 2969,
"preview": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst sns = require('./sns"
},
{
"path": "lib/retry.js",
"chars": 1449,
"preview": "'use strict';\n\nconst co = require('co');\nconst sns = require('./sns');\nconst log = require('./log'"
},
{
"path": "lib/sns.js",
"chars": 922,
"preview": "'use strict';\n\nconst AWSXRay = require('aws-xray-sdk');\nconst AWS = AWSXRay.captureAWS(require('aws-sd"
},
{
"path": "middleware/capture-correlation-ids.js",
"chars": 4855,
"preview": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\nfunction ca"
},
{
"path": "middleware/flush-metrics.js",
"chars": 296,
"preview": "'use strict';\n\nconst log = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\n\nmodule.export"
},
{
"path": "middleware/function-shield.js",
"chars": 404,
"preview": "'use strict';\n\nconst FuncShield = require('@puresec/function-shield');\n\nmodule.exports = () => {\n return {\n before: "
},
{
"path": "middleware/sample-logging.js",
"chars": 996,
"preview": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\n// config s"
},
{
"path": "middleware/wrapper.js",
"chars": 400,
"preview": "'use strict';\n\nconst middy = require('middy');\nconst sampleLogging = require('./sample-logging');\nconst captureCorrelati"
},
{
"path": "package.json",
"chars": 1335,
"preview": "{\n \"name\": \"big-mouth\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n "
},
{
"path": "seed-restaurants.js",
"chars": 1619,
"preview": "'use strict';\n\nconst co = require('co');\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst dynamodb"
},
{
"path": "serverless.yml",
"chars": 9543,
"preview": "service: big-mouth\n\nplugins:\n - serverless-pseudo-parameters\n - serverless-sam\n - serverless-iam-roles-per-function\n "
},
{
"path": "static/index.html",
"chars": 15202,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>Big Mouth</title>\n\n <script src=\"https://sdk.am"
},
{
"path": "template.yml",
"chars": 3026,
"preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: 'AWS::Serverless-2016-10-31'\nDescription: 'SAM template for Serverless"
},
{
"path": "tests/steps/given.js",
"chars": 2106,
"preview": "'use strict';\n\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst cognito = new AWS.CognitoIdent"
},
{
"path": "tests/steps/init.js",
"chars": 1309,
"preview": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst Promise = require('bluebird');\nconst aws4 = "
},
{
"path": "tests/steps/tearDown.js",
"chars": 470,
"preview": "'use strict';\n\nconst co = require('co');\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst"
},
{
"path": "tests/steps/when.js",
"chars": 3363,
"preview": "'use strict';\n\nconst APP_ROOT = '../../';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst Promi"
},
{
"path": "tests/test_cases/get-index.js",
"chars": 770,
"preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst when = require('../steps/when');\nc"
},
{
"path": "tests/test_cases/get-restaurants.js",
"chars": 672,
"preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').in"
},
{
"path": "tests/test_cases/search-restaurants.js",
"chars": 1052,
"preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').in"
}
]
About this extraction
This page contains the full source code of the theburningmonk/manning-aws-lambda-in-motion GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (118.3 KB), approximately 33.0k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.