Skip to content

docs: update CDK example to nodejs18 #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions examples/cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

This is a deployable CDK app that deploys AWS Lambda functions as part of a CloudFormation stack. These Lambda functions use the utilities made available as part of AWS Lambda Powertools for TypeScript to demonstrate their usage.

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/).
> **Note**
> 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/).

The example functions, located in the `src` folder, are invoked automatically, twice, when deployed using the CDK construct defined in `src/example-function.ts`. The first invocation demonstrates the effect on logs/metrics/annotations when the Lambda function has a cold start, and the latter without a cold start.
The example functions, located in the `functions` folder, are frontend by a REST API that is deployed using AWS API Gateway.

The API has three endpoints:

* `POST /` - Adds an item to the DynamoDB table
* `GET /` - Retrieves all items from the DynamoDB table
* `GET /{id}` - Retrieves a specific item from the DynamoDB table

## Deploying the stack

Expand All @@ -14,10 +21,59 @@ The example functions, located in the `src` folder, are invoked automatically, t

Note: Prior to deploying you may need to run `cdk bootstrap aws://<YOU_AWS_ACCOUNT_ID>/<AWS_REGION> --profile <YOUR_AWS_PROFILE>` if you have not already bootstrapped your account for CDK.

## Viewing Utility Outputs
> **Note**
> 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:

```sh
curl -XGET https://randomid12345.execute-api.eu-central-1.amazonaws.com/prod/
```

And finally, let's retrieve a specific item by running:
```bash
curl -XGET 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, you can visit [AWS CloudWatch Console](https://console.aws.amazon.com/cloudwatch/) 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.

## Cleanup

All utility outputs can be viewed in the Amazon CloudWatch console.
To delete the sample application that you created, run the command below while in the `examples/sam` directory:

* `Logger` output can be found in Logs > Log groups
* `Metrics` output can be found in Metrics > All metrics > CdkExample
* `Tracer` output can be found in X-Ray traces > Traces
```bash
cdk delete
```
6 changes: 6 additions & 0 deletions examples/cdk/functions/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Get the DynamoDB table name from environment variables
const tableName = process.env.SAMPLE_TABLE;

export {
tableName
};
29 changes: 29 additions & 0 deletions examples/cdk/functions/common/dynamodb-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { tracer } from './powertools';

// Create DynamoDB Client and patch it for tracing
const ddbClient = tracer.captureAWSv3Client(new DynamoDBClient({}));

const marshallOptions = {
// Whether to automatically convert empty strings, blobs, and sets to `null`.
convertEmptyValues: false, // false, by default.
// Whether to remove undefined values while marshalling.
removeUndefinedValues: false, // false, by default.
// Whether to convert typeof object to map attribute.
convertClassInstanceToMap: false, // false, by default.
};

const unmarshallOptions = {
// Whether to return numbers as a string instead of converting them to native JavaScript numbers.
wrapNumbers: false, // false, by default.
};

const translateConfig = { marshallOptions, unmarshallOptions };

// Create the DynamoDB Document client.
const docClient = DynamoDBDocumentClient.from(ddbClient, translateConfig);

export {
docClient
};
32 changes: 32 additions & 0 deletions examples/cdk/functions/common/powertools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';

const awsLambdaPowertoolsVersion = '1.5.0';

const defaultValues = {
region: process.env.AWS_REGION || 'N/A',
executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A'
};

const logger = new Logger({
persistentLogAttributes: {
...defaultValues,
logger: {
name: '@aws-lambda-powertools/logger',
version: awsLambdaPowertoolsVersion,
}
},
});

const metrics = new Metrics({
defaultDimensions: defaultValues
});

const tracer = new Tracer();

export {
logger,
metrics,
tracer
};
98 changes: 98 additions & 0 deletions examples/cdk/functions/get-all-items.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import middy from '@middy/core';
import { tableName } from './common/constants';
import { logger, tracer, metrics } from './common/powertools';
import { logMetrics } from '@aws-lambda-powertools/metrics';
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer';
import { docClient } from './common/dynamodb-client';
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
import { default as request } from 'phin';

/*
*
* This example uses the Middy middleware instrumentation.
* It is the best choice if your existing code base relies on the Middy middleware engine.
* Powertools offers compatible Middy middleware to make this integration seamless.
* Find more Information in the docs: https://awslabs.github.io/aws-lambda-powertools-typescript/
*
* 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
*
*/
const getAllItemsHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
if (event.httpMethod !== 'GET') {
throw new Error(`getAllItems only accepts GET method, you tried: ${event.httpMethod}`);
}

// Tracer: Add awsRequestId as annotation
tracer.putAnnotation('awsRequestId', context.awsRequestId);

// Logger: Append awsRequestId to each log statement
logger.appendKeys({
awsRequestId: context.awsRequestId,
});

// Request a sample random uuid from a webservice
const res = await request<{ uuid: string }>({
url: 'https://httpbin.org/uuid',
parse: 'json',
});
const { uuid } = res.body;

// Logger: Append uuid to each log statement
logger.appendKeys({ uuid });

// Tracer: Add uuid as annotation
tracer.putAnnotation('uuid', uuid);

// Metrics: Add uuid as metadata
metrics.addMetadata('uuid', uuid);

// 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
try {
if (!tableName) {
throw new Error('SAMPLE_TABLE environment variable is not set');
}

const data = await docClient.send(new ScanCommand({
TableName: tableName
}));
const { Items: items } = data;

// Logger: All log statements are written to CloudWatch
logger.debug(`retrieved items: ${items?.length || 0}`);

logger.info(`Response ${event.path}`, {
statusCode: 200,
body: items,
});

return {
statusCode: 200,
body: JSON.stringify(items)
};
} catch (err) {
tracer.addErrorAsMetadata(err as Error);
logger.error('Error reading from table. ' + err);

return {
statusCode: 500,
body: JSON.stringify({ 'error': 'Error reading from table.' })
};
}
};

// Wrap the handler with middy
export const handler = middy(getAllItemsHandler)
// Use the middleware by passing the Metrics instance as a parameter
.use(logMetrics(metrics))
// Use the middleware by passing the Logger instance as a parameter
.use(injectLambdaContext(logger, { logEvent: true }))
// Use the middleware by passing the Tracer instance as a parameter
.use(captureLambdaHandler(tracer, { captureResponse: false })); // by default the tracer would add the response as metadata on the segment, but there is a chance to hit the 64kb segment size limit. Therefore set captureResponse: false
110 changes: 110 additions & 0 deletions examples/cdk/functions/get-by-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { tableName } from './common/constants';
import { logger, tracer, metrics } from './common/powertools';
import { LambdaInterface } from '@aws-lambda-powertools/commons';
import { docClient } from './common/dynamodb-client';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
import { default as request } from 'phin';

/*
*
* This example uses the Method decorator instrumentation.
* Use TypeScript method decorators if you prefer writing your business logic using TypeScript Classes.
* If you aren’t using Classes, this requires the most significant refactoring.
* Find more Information in the docs: https://awslabs.github.io/aws-lambda-powertools-typescript/
*
* 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 {APIGatewayProxyEvent} event - API Gateway Lambda Proxy Input Format
*
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
* @returns {Promise<APIGatewayProxyResult>} object - API Gateway Lambda Proxy Output Format
*
*/

class Lambda implements LambdaInterface {

@tracer.captureMethod()
public async getUuid(): Promise<string> {
// Request a sample random uuid from a webservice
const res = await request<{ uuid: string }>({
url: 'https://httpbin.org/uuid',
parse: 'json',
});
const { uuid } = res.body;

return uuid;
}

@tracer.captureLambdaHandler({ captureResponse: false }) // by default the tracer would add the response as metadata on the segment, but there is a chance to hit the 64kb segment size limit. Therefore set captureResponse: false
@logger.injectLambdaContext({ logEvent: true })
@metrics.logMetrics({ throwOnEmptyMetrics: false, captureColdStartMetric: true })
public async handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> {

if (event.httpMethod !== 'GET') {
throw new Error(`getById only accepts GET method, you tried: ${event.httpMethod}`);
}

// Tracer: Add awsRequestId as annotation
tracer.putAnnotation('awsRequestId', context.awsRequestId);

// Logger: Append awsRequestId to each log statement
logger.appendKeys({
awsRequestId: context.awsRequestId,
});

// Call the getUuid function
const uuid = await this.getUuid();

// Logger: Append uuid to each log statement
logger.appendKeys({ uuid });

// Tracer: Add uuid as annotation
tracer.putAnnotation('uuid', uuid);

// Metrics: Add uuid as metadata
metrics.addMetadata('uuid', uuid);

// Get the item from the table
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#get-property
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.send(new GetCommand({
TableName: tableName,
Key: {
id: event.pathParameters.id
}
}));
const item = data.Item;

logger.info(`Response ${event.path}`, {
statusCode: 200,
body: item,
});

return {
statusCode: 200,
body: JSON.stringify(item)
};
} catch (err) {
tracer.addErrorAsMetadata(err as Error);
logger.error('Error reading from table. ' + err);

return {
statusCode: 500,
body: JSON.stringify({ 'error': 'Error reading from table.' })
};
}
}

}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass);
Loading