diff --git a/.gitignore b/.gitignore index 58ad859aea..ce7e2e2eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ site # Generated API documentation (from TypeDoc) /api + +# SAM Example copies files +/examples/sam/src/handlers/* +!/examples/sam/src/handlers/COPY_LAMBDA_FUNCTIONS_HERE \ No newline at end of file diff --git a/examples/lambda-functions/get-all-items.ts b/examples/lambda-functions/get-all-items.ts new file mode 100644 index 0000000000..729c04ae62 --- /dev/null +++ b/examples/lambda-functions/get-all-items.ts @@ -0,0 +1,94 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { Metrics } from '@aws-lambda-powertools/metrics'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { DocumentClient } from 'aws-sdk/clients/dynamodb'; + +// Create the PowerTools clients +const metrics = new Metrics(); +const logger = new Logger(); +const tracer = new Tracer(); + +// Create DynamoDB DocumentClient and patch it for tracing +const docClient = tracer.captureAWSClient(new DocumentClient()); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.SAMPLE_TABLE; + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} event - API Gateway Lambda Proxy Input Format + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ +export const getAllItemsHandler = async (event: APIGatewayProxyEvent, context: Context): Promise => { + if (event.httpMethod !== 'GET') { + throw new Error(`getAllItems only accepts GET method, you tried: ${event.httpMethod}`); + } + + // Tracer: Get facade segment created by AWS Lambda + const segment = tracer.getSegment(); + + // Tracer: Create subsegment for the function & set it as active + const handlerSegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); + tracer.setSegment(handlerSegment); + + // Tracer: Annotate the subsegment with the cold start & serviceName + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + // Tracer: Add annotation for the awsRequestId + tracer.putAnnotation('awsRequestId', context.awsRequestId); + + // Metrics: Capture cold start metrics + metrics.captureColdStartMetric(); + + // Logger: Add persistent attributes to each log statement + logger.addPersistentLogAttributes({ + awsRequestId: context.awsRequestId, + }); + + // get all items from the table (only first 1MB data, you can use `LastEvaluatedKey` to get the rest of data) + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#scan-property + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html + let response; + try { + if (!tableName) { + throw new Error('SAMPLE_TABLE environment variable is not set'); + } + + const data = await docClient.scan({ + TableName: tableName + }).promise(); + const items = data.Items; + + // Logger: All log statements are written to CloudWatch + logger.debug(`retrieved items: ${items?.length || 0}`); + + response = { + statusCode: 200, + body: JSON.stringify(items) + }; + } catch (err) { + tracer.addErrorAsMetadata(err as Error); + logger.error('Error reading from table. ' + err); + response = { + statusCode: 500, + body: JSON.stringify({ 'error': 'Error reading from table.' }) + }; + } + + // Tracer: Close subsegment (the AWS Lambda one is closed automatically) + handlerSegment.close(); // (## index.handler) + + // Tracer: Set the facade segment as active again (the one created by AWS Lambda) + tracer.setSegment(segment); + + // All log statements are written to CloudWatch + logger.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`); + + return response; +}; \ No newline at end of file diff --git a/examples/lambda-functions/get-by-id.ts b/examples/lambda-functions/get-by-id.ts new file mode 100644 index 0000000000..8e498fd701 --- /dev/null +++ b/examples/lambda-functions/get-by-id.ts @@ -0,0 +1,96 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { Metrics } from '@aws-lambda-powertools/metrics'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { DocumentClient } from 'aws-sdk/clients/dynamodb'; + +// Create the PowerTools clients +const metrics = new Metrics(); +const logger = new Logger(); +const tracer = new Tracer(); + +// Create DynamoDB DocumentClient and patch it for tracing +const docClient = tracer.captureAWSClient(new DocumentClient()); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.SAMPLE_TABLE; + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} event - API Gateway Lambda Proxy Input Format + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ + +export const getByIdHandler = async (event: APIGatewayProxyEvent, context: Context): Promise => { + if (event.httpMethod !== 'GET') { + throw new Error(`getById only accepts GET method, you tried: ${event.httpMethod}`); + } + // Tracer: Get facade segment created by AWS Lambda + const segment = tracer.getSegment(); + + // Tracer: Create subsegment for the function & set it as active + const handlerSegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); + tracer.setSegment(handlerSegment); + + // Tracer: Annotate the subsegment with the cold start & serviceName + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + // Tracer: Add annotation for the awsRequestId + tracer.putAnnotation('awsRequestId', context.awsRequestId); + + // Metrics: Capture cold start metrics + metrics.captureColdStartMetric(); + + // Logger: Add persistent attributes to each log statement + logger.addPersistentLogAttributes({ + awsRequestId: context.awsRequestId, + }); + + // Get the item from the table + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#get-property + let response; + try { + if (!tableName) { + throw new Error('SAMPLE_TABLE environment variable is not set'); + } + if (!event.pathParameters) { + throw new Error('event does not contain pathParameters') + } + if (!event.pathParameters.id) { + throw new Error('PathParameter id is missing') + } + + const data = await docClient.get({ + TableName: tableName, + Key: { id: event.pathParameters.id }, + }).promise(); + const item = data.Item; + response = { + statusCode: 200, + body: JSON.stringify(item) + }; + } catch (err) { + tracer.addErrorAsMetadata(err as Error); + logger.error('Error reading from table. ' + err); + response = { + statusCode: 500, + body: JSON.stringify({ 'error': 'Error reading from table.' }) + }; + } + + // Tracer: Close subsegment (the AWS Lambda one is closed automatically) + handlerSegment.close(); // (## index.handler) + + // Tracer: Set the facade segment as active again (the one created by AWS Lambda) + tracer.setSegment(segment); + + // All log statements are written to CloudWatch + logger.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`); + + return response; +}; diff --git a/examples/lambda-functions/put-item.ts b/examples/lambda-functions/put-item.ts new file mode 100644 index 0000000000..392689815d --- /dev/null +++ b/examples/lambda-functions/put-item.ts @@ -0,0 +1,97 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { Metrics } from '@aws-lambda-powertools/metrics'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { DocumentClient } from 'aws-sdk/clients/dynamodb'; + +// Create the PowerTools clients +const metrics = new Metrics(); +const logger = new Logger(); +const tracer = new Tracer(); + +// Create DynamoDB DocumentClient and patch it for tracing +const docClient = tracer.captureAWSClient(new DocumentClient()); + +// Get the DynamoDB table name from environment variables +const tableName = process.env.SAMPLE_TABLE; + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} event - API Gateway Lambda Proxy Input Format + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ + +export const putItemHandler = async (event: APIGatewayProxyEvent, context: Context): Promise => { + if (event.httpMethod !== 'POST') { + throw new Error(`putItem only accepts POST method, you tried: ${event.httpMethod}`); + } + // Tracer: Get facade segment created by AWS Lambda + const segment = tracer.getSegment(); + + // Tracer: Create subsegment for the function & set it as active + const handlerSegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`); + tracer.setSegment(handlerSegment); + + // Tracer: Annotate the subsegment with the cold start & serviceName + tracer.annotateColdStart(); + tracer.addServiceNameAnnotation(); + + // Tracer: Add annotation for the awsRequestId + tracer.putAnnotation('awsRequestId', context.awsRequestId); + + // Metrics: Capture cold start metrics + metrics.captureColdStartMetric(); + + // Logger: Add persistent attributes to each log statement + logger.addPersistentLogAttributes({ + awsRequestId: context.awsRequestId, + }); + + // Creates a new item, or replaces an old item with a new item + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#put-property + let response; + try { + if (!tableName) { + throw new Error('SAMPLE_TABLE environment variable is not set'); + } + if (!event.body) { + throw new Error('Event does not contain body') + } + + // Get id and name from the body of the request + const body = JSON.parse(event.body); + const id = body.id; + const name = body.name; + + await docClient.put({ + TableName: tableName, + Item: { id: id, name: name } + }).promise(); + response = { + statusCode: 200, + body: JSON.stringify(body) + }; + } catch (err) { + tracer.addErrorAsMetadata(err as Error); + logger.error('Error writing data to table. ' + err); + response = { + statusCode: 500, + body: JSON.stringify({ 'error': 'Error writing data to table.' }) + }; + } + + // Tracer: Close subsegment (the AWS Lambda one is closed automatically) + handlerSegment.close(); // (## index.handler) + + // Tracer: Set the facade segment as active again (the one created by AWS Lambda) + tracer.setSegment(segment); + + // All log statements are written to CloudWatch + logger.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`); + + return response; +}; diff --git a/examples/lambda-functions/tsconfig.json b/examples/lambda-functions/tsconfig.json new file mode 100644 index 0000000000..6fe6024ff3 --- /dev/null +++ b/examples/lambda-functions/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitAny": true, + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "removeComments": false, + "strict": true, + "inlineSourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "pretty": true, + "esModuleInterop": true + }, + "exclude": [ "./node_modules"], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority" + }, + "lib": [ "es2020" ], + "types": [ + "node" + ] +} \ No newline at end of file diff --git a/examples/sam/.gitignore b/examples/sam/.gitignore new file mode 100644 index 0000000000..41b1156d36 --- /dev/null +++ b/examples/sam/.gitignore @@ -0,0 +1,208 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/osx,node,linux,windows,sam +# Edit at https://www.toptal.com/developers/gitignore?templates=osx,node,linux,windows,sam + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env*.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# rollup.js default build output +dist/ + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + +### OSX ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SAM ### +# Ignore build directories for the AWS Serverless Application Model (SAM) +# Info: https://aws.amazon.com/serverless/sam/ +# Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html + +**/.aws-sam + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/osx,node,linux,windows,sam diff --git a/examples/sam/README.md b/examples/sam/README.md new file mode 100644 index 0000000000..a96692f561 --- /dev/null +++ b/examples/sam/README.md @@ -0,0 +1,166 @@ +# AWS Lambda Powertools for TypeScript examples in SAM + +This project contains source code and supporting files for a serverless application that you can deploy with the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html). The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. + +This project includes the following files and folders: + +- `src/handlers` - Code for the application's Lambda function written in TypeScript. See "Prepare the project" below for instructions on how to copy the Lambda handler code here. +- `events` - Invocation events that you can use to invoke the function. +- `template.yaml` - A template that defines the application's AWS resources. + +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. + +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. + +* [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) + +You will need to have a valid AWS Account in order to deploy these resources. These resources may incur costs to your AWS Account. The cost from **some services** are covered by the [AWS Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all) but not all of them. If you don't have an AWS Account follow [these instructions to create one](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). + +## Prepare the project + +All the following commands in this file must be executed inside the folder `examples/sam` + +Before deploying this example install the npm dependencies: + +```bash +npm i +``` + +In addition to the [recommended setup for this project](https://github.com/awslabs/aws-lambda-powertools-typescript/blob/main/CONTRIBUTING.md#setup), you'll need the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). + +## Deploy the sample application + +To build and deploy your application for the first time, run the following in your shell: + +```bash +sam build --beta-features +sam deploy --guided +``` + +The first command will build the source of your application. Using esbuild for bundling Node.js and TypeScript is a beta feature, therefore we add the `--beta-features` parameter. The second command will package and deploy your application to AWS, with a series of prompts: + +* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. +* **AWS Region**: The AWS region you want to deploy your app to. +* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. +* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. +* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. + +You can find your API Gateway Endpoint URL in the output values displayed after deployment. + +## Execute the functions via API Gateway + +Use the API Gateway Endpoint URL from the output values to execute the functions. First, let's add two items to the DynamoDB Table by running: + +```bash +curl -XPOST --header 'Content-Type: application/json' --data '{"id":"myfirstitem","name":"Some Name for the first item"}' https://randomid12345.execute-api.eu-central-1.amazonaws.com/Prod/ +curl -XPOST --header 'Content-Type: application/json' --data '{"id":"myseconditem","name":"Some Name for the second item"}' https://randomid1245.execute-api.eu-central-1.amazonaws.com/Prod/ +```` + +Now, let's retrieve all items by running: + +```bash +curl -XGET https://randomid12345.execute-api.eu-central-1.amazonaws.com/Prod/ +```` + +And finally, let's retrieve a specific item by running: +```bash +https://randomid12345.execute-api.eu-central-1.amazonaws.com/Prod/myseconditem/ +``` + +## Observe the outputs in AWS CloudWatch & X-Ray +### CloudWatch + +If we check the logs in CloudWatch, we can see that the logs are structured like this +``` +2022-04-26T17:00:23.808Z e8a51294-6c6a-414c-9777-6b0f24d8739b DEBUG +{ + "level": "DEBUG", + "message": "retrieved items: 0", + "service": "getAllItems", + "timestamp": "2022-04-26T17:00:23.808Z", + "awsRequestId": "e8a51294-6c6a-414c-9777-6b0f24d8739b" +} +``` + +By having structured logs like this, we can easily search and analyse them in [CloudWatch Logs Insight](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html). Run the following query to get all messages for a specific `awsRequestId`: + +```` +filter awsRequestId="bcd50969-3a55-49b6-a997-91798b3f133a" + | fields timestamp, message +```` +### AWS X-Ray +As we have enabled tracing for our Lambda-Funtions, we can visit [AWS X-Ray Console](https://console.aws.amazon.com/xray/home#/traces/) and see [traces](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-traces) and a [service map](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-using-xray-maps.html) for our application. + +## Use the SAM CLI to build and test locally + +Build your application with the `sam build` command. + +```bash +sam build --beta-features +``` + +The SAM CLI installs dependencies defined in `src/handlers/package.json`, compiles TypeScript with esbuild, creates a deployment package, and saves it in the `.aws-sam/build` folder. + +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. + +Run functions locally and invoke them with the `sam local invoke` command. + +```bash +sam local invoke getAllItemsFunction --event events/event-get-all-items.json +``` + +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. + +```bash +sam local start-api +curl http://localhost:3000/ +``` + +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. + +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: / + Method: get +``` + +## Fetch, tail, and filter Lambda function logs + +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. + +```bash +sam logs -n getAllItemsFunction --stack-name powertools-example --tail +``` + +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). + + +## Cleanup + +To delete the sample application that you created, run the command below while in the `examples/sam` directory: + +```bash +sam delete +``` + +## Resources + +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. + +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/examples/sam/events/event-get-all-items.json b/examples/sam/events/event-get-all-items.json new file mode 100644 index 0000000000..3a0cb5f77b --- /dev/null +++ b/examples/sam/events/event-get-all-items.json @@ -0,0 +1,3 @@ +{ + "httpMethod": "GET" +} \ No newline at end of file diff --git a/examples/sam/events/event-get-by-id.json b/examples/sam/events/event-get-by-id.json new file mode 100644 index 0000000000..63a64fb458 --- /dev/null +++ b/examples/sam/events/event-get-by-id.json @@ -0,0 +1,6 @@ +{ + "httpMethod": "GET", + "pathParameters": { + "id": "id1" + } +} \ No newline at end of file diff --git a/examples/sam/events/event-post-item.json b/examples/sam/events/event-post-item.json new file mode 100644 index 0000000000..6367003e54 --- /dev/null +++ b/examples/sam/events/event-post-item.json @@ -0,0 +1,4 @@ +{ + "httpMethod": "POST", + "body": "{\"id\": \"id1\",\"name\": \"name1\"}" +} \ No newline at end of file diff --git a/examples/sam/package.json b/examples/sam/package.json new file mode 100644 index 0000000000..6ac7860887 --- /dev/null +++ b/examples/sam/package.json @@ -0,0 +1,26 @@ +{ + "name": "powertools-typescript-sam-example", + "version": "1.0.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "description": "This project contains source code and supporting files for a serverless application that you can deploy with the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html). The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.", + "license": "MIT", + "devDependencies": { + "@types/aws-lambda": "^8.10.86", + "@types/node": "17.0.10", + "@typescript-eslint/parser": "^5.12.1", + "esbuild": "^0.14.23", + "eslint": "^8.4.0", + "ts-node": "^10.0.0", + "typescript": "^4.1.3" + }, + "dependencies": { + "@aws-lambda-powertools/logger": "^0.7.0", + "@aws-lambda-powertools/metrics": "^0.7.0", + "@aws-lambda-powertools/tracer": "^0.7.0", + "aws-sdk": "^2.1122.0" + } + } + \ No newline at end of file diff --git a/examples/sam/src/handlers b/examples/sam/src/handlers new file mode 120000 index 0000000000..4407535789 --- /dev/null +++ b/examples/sam/src/handlers @@ -0,0 +1 @@ +../../lambda-functions/ \ No newline at end of file diff --git a/examples/sam/template.yaml b/examples/sam/template.yaml new file mode 100644 index 0000000000..c71a8c6ae4 --- /dev/null +++ b/examples/sam/template.yaml @@ -0,0 +1,143 @@ +# This is the SAM template that represents the architecture of your serverless application +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-basics.html + +# The AWSTemplateFormatVersion identifies the capabilities of the template +# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/format-version-structure.html +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + An example application with PowerTools for TypeScript instrumented. Its purpose is to demonstrate how to use the PowerTools with AWS SAM. The application an API with contains 3 endpoints (get all items, get an item by ids, put an item). Each Lambda function utilises Logger, Metrics, and Tracers. + +# Transform section specifies one or more macros that AWS CloudFormation uses to process your template +# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html +Transform: AWS::Serverless-2016-10-31 + +# Global configuration that all Functions inherit +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html +Globals: + Function: + Runtime: nodejs14.x + Architectures: + - x86_64 + MemorySize: 128 + Timeout: 100 + +# Resources declares the AWS resources that you want to include in the stack +# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html +Resources: + # Each Lambda function is defined by properties: + # https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + + # This is a Lambda function config associated with the source code: get-all-items.js + getAllItemsFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/get-all-items.getAllItemsHandler + Description: A simple example includes a HTTP get method to get all items from a DynamoDB table. + Policies: + # Give Create/Read/Update/Delete Permissions to the SampleTable + - DynamoDBReadPolicy: + TableName: !Ref SampleTable + Tracing: Active + Environment: + Variables: + # Make table name accessible as environment variable from function code during execution + SAMPLE_TABLE: !Ref SampleTable + POWERTOOLS_SERVICE_NAME: getAllItems + POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample + LOG_LEVEL: Debug + Events: + Api: + Type: Api + Properties: + Path: / + Method: GET + Metadata: + # Manage esbuild properties + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Sourcemap: true, + EntryPoints: + - src/handlers/get-all-items.ts + + # This is a Lambda function config associated with the source code: get-by-id.js + getByIdFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/get-by-id.getByIdHandler + Description: A simple example includes a HTTP get method to get one item by id from a DynamoDB table. + Policies: + # Give Create/Read/Update/Delete Permissions to the SampleTable + - DynamoDBReadPolicy: + TableName: !Ref SampleTable + Tracing: Active + Environment: + Variables: + # Make table name accessible as environment variable from function code during execution + SAMPLE_TABLE: !Ref SampleTable + POWERTOOLS_SERVICE_NAME: getById + POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample + LOG_LEVEL: Debug + Events: + Api: + Type: Api + Properties: + Path: /{id} + Method: GET + Metadata: + # Manage esbuild properties + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Sourcemap: true, + EntryPoints: + - src/handlers/get-by-id.ts + + # This is a Lambda function config associated with the source code: put-item.js + putItemFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/put-item.putItemHandler + Description: A simple example includes a HTTP post method to add one item to a DynamoDB table. + Policies: + # Give Create/Read/Update/Delete Permissions to the SampleTable + - DynamoDBCrudPolicy: + TableName: !Ref SampleTable + Tracing: Active + Environment: + Variables: + # Make table name accessible as environment variable from function code during execution + SAMPLE_TABLE: !Ref SampleTable + POWERTOOLS_SERVICE_NAME: getById + POWERTOOLS_METRICS_NAMESPACE: PowertoolsSAMExample + LOG_LEVEL: Debug + Events: + Api: + Type: Api + Properties: + Path: / + Method: POST + Metadata: + # Manage esbuild properties + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Sourcemap: true, + EntryPoints: + - src/handlers/put-item.ts + + # DynamoDB table to store item: {id: , name: } + SampleTable: + Type: AWS::Serverless::SimpleTable + Properties: + PrimaryKey: + Name: id + Type: String + +Outputs: + WebEndpoint: + Description: "API Gateway endpoint URL for Prod stage" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"