Skip to content

Commit b4cb3d9

Browse files
authored
docs: update CDK example to nodejs18 (#1197)
* docs: update cdk examples * tests: update unit tests
1 parent a4134c4 commit b4cb3d9

27 files changed

+6109
-7151
lines changed

Diff for: examples/cdk/README.md

+63-7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
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.
44

5-
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/).
5+
> **Note**
6+
> 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/).
67
7-
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.
8+
The example functions, located in the `functions` folder, are frontend by a REST API that is deployed using AWS API Gateway.
9+
10+
The API has three endpoints:
11+
12+
* `POST /` - Adds an item to the DynamoDB table
13+
* `GET /` - Retrieves all items from the DynamoDB table
14+
* `GET /{id}` - Retrieves a specific item from the DynamoDB table
815

916
## Deploying the stack
1017

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

1522
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.
1623

17-
## Viewing Utility Outputs
24+
> **Note**
25+
> You can find your API Gateway Endpoint URL in the output values displayed after deployment.
26+
27+
## Execute the functions via API Gateway
28+
29+
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:
30+
31+
```bash
32+
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/
33+
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/
34+
````
35+
36+
Now, let's retrieve all items by running:
37+
38+
```sh
39+
curl -XGET https://randomid12345.execute-api.eu-central-1.amazonaws.com/prod/
40+
```
41+
42+
And finally, let's retrieve a specific item by running:
43+
```bash
44+
curl -XGET https://randomid12345.execute-api.eu-central-1.amazonaws.com/prod/myseconditem/
45+
```
46+
47+
## Observe the outputs in AWS CloudWatch & X-Ray
48+
49+
### CloudWatch
50+
51+
If we check the logs in CloudWatch, we can see that the logs are structured like this
52+
```
53+
2022-04-26T17:00:23.808Z e8a51294-6c6a-414c-9777-6b0f24d8739b DEBUG
54+
{
55+
"level": "DEBUG",
56+
"message": "retrieved items: 0",
57+
"service": "getAllItems",
58+
"timestamp": "2022-04-26T17:00:23.808Z",
59+
"awsRequestId": "e8a51294-6c6a-414c-9777-6b0f24d8739b"
60+
}
61+
```
62+
63+
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`:
64+
65+
````
66+
filter awsRequestId="bcd50969-3a55-49b6-a997-91798b3f133a"
67+
| fields timestamp, message
68+
````
69+
### AWS X-Ray
70+
71+
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.
72+
73+
## Cleanup
1874
19-
All utility outputs can be viewed in the Amazon CloudWatch console.
75+
To delete the sample application that you created, run the command below while in the `examples/sam` directory:
2076
21-
* `Logger` output can be found in Logs > Log groups
22-
* `Metrics` output can be found in Metrics > All metrics > CdkExample
23-
* `Tracer` output can be found in X-Ray traces > Traces
77+
```bash
78+
cdk delete
79+
```

Diff for: examples/cdk/functions/common/constants.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Get the DynamoDB table name from environment variables
2+
const tableName = process.env.SAMPLE_TABLE;
3+
4+
export {
5+
tableName
6+
};

Diff for: examples/cdk/functions/common/dynamodb-client.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
3+
import { tracer } from './powertools';
4+
5+
// Create DynamoDB Client and patch it for tracing
6+
const ddbClient = tracer.captureAWSv3Client(new DynamoDBClient({}));
7+
8+
const marshallOptions = {
9+
// Whether to automatically convert empty strings, blobs, and sets to `null`.
10+
convertEmptyValues: false, // false, by default.
11+
// Whether to remove undefined values while marshalling.
12+
removeUndefinedValues: false, // false, by default.
13+
// Whether to convert typeof object to map attribute.
14+
convertClassInstanceToMap: false, // false, by default.
15+
};
16+
17+
const unmarshallOptions = {
18+
// Whether to return numbers as a string instead of converting them to native JavaScript numbers.
19+
wrapNumbers: false, // false, by default.
20+
};
21+
22+
const translateConfig = { marshallOptions, unmarshallOptions };
23+
24+
// Create the DynamoDB Document client.
25+
const docClient = DynamoDBDocumentClient.from(ddbClient, translateConfig);
26+
27+
export {
28+
docClient
29+
};

Diff for: examples/cdk/functions/common/powertools.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Logger } from '@aws-lambda-powertools/logger';
2+
import { Metrics } from '@aws-lambda-powertools/metrics';
3+
import { Tracer } from '@aws-lambda-powertools/tracer';
4+
5+
const awsLambdaPowertoolsVersion = '1.5.0';
6+
7+
const defaultValues = {
8+
region: process.env.AWS_REGION || 'N/A',
9+
executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A'
10+
};
11+
12+
const logger = new Logger({
13+
persistentLogAttributes: {
14+
...defaultValues,
15+
logger: {
16+
name: '@aws-lambda-powertools/logger',
17+
version: awsLambdaPowertoolsVersion,
18+
}
19+
},
20+
});
21+
22+
const metrics = new Metrics({
23+
defaultDimensions: defaultValues
24+
});
25+
26+
const tracer = new Tracer();
27+
28+
export {
29+
logger,
30+
metrics,
31+
tracer
32+
};

Diff for: examples/cdk/functions/get-all-items.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
2+
import middy from '@middy/core';
3+
import { tableName } from './common/constants';
4+
import { logger, tracer, metrics } from './common/powertools';
5+
import { logMetrics } from '@aws-lambda-powertools/metrics';
6+
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
7+
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer';
8+
import { docClient } from './common/dynamodb-client';
9+
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
10+
import { default as request } from 'phin';
11+
12+
/*
13+
*
14+
* This example uses the Middy middleware instrumentation.
15+
* It is the best choice if your existing code base relies on the Middy middleware engine.
16+
* Powertools offers compatible Middy middleware to make this integration seamless.
17+
* Find more Information in the docs: https://awslabs.github.io/aws-lambda-powertools-typescript/
18+
*
19+
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
20+
* @param {Object} event - API Gateway Lambda Proxy Input Format
21+
*
22+
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
23+
* @returns {Object} object - API Gateway Lambda Proxy Output Format
24+
*
25+
*/
26+
const getAllItemsHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
27+
if (event.httpMethod !== 'GET') {
28+
throw new Error(`getAllItems only accepts GET method, you tried: ${event.httpMethod}`);
29+
}
30+
31+
// Tracer: Add awsRequestId as annotation
32+
tracer.putAnnotation('awsRequestId', context.awsRequestId);
33+
34+
// Logger: Append awsRequestId to each log statement
35+
logger.appendKeys({
36+
awsRequestId: context.awsRequestId,
37+
});
38+
39+
// Request a sample random uuid from a webservice
40+
const res = await request<{ uuid: string }>({
41+
url: 'https://httpbin.org/uuid',
42+
parse: 'json',
43+
});
44+
const { uuid } = res.body;
45+
46+
// Logger: Append uuid to each log statement
47+
logger.appendKeys({ uuid });
48+
49+
// Tracer: Add uuid as annotation
50+
tracer.putAnnotation('uuid', uuid);
51+
52+
// Metrics: Add uuid as metadata
53+
metrics.addMetadata('uuid', uuid);
54+
55+
// get all items from the table (only first 1MB data, you can use `LastEvaluatedKey` to get the rest of data)
56+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#scan-property
57+
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html
58+
try {
59+
if (!tableName) {
60+
throw new Error('SAMPLE_TABLE environment variable is not set');
61+
}
62+
63+
const data = await docClient.send(new ScanCommand({
64+
TableName: tableName
65+
}));
66+
const { Items: items } = data;
67+
68+
// Logger: All log statements are written to CloudWatch
69+
logger.debug(`retrieved items: ${items?.length || 0}`);
70+
71+
logger.info(`Response ${event.path}`, {
72+
statusCode: 200,
73+
body: items,
74+
});
75+
76+
return {
77+
statusCode: 200,
78+
body: JSON.stringify(items)
79+
};
80+
} catch (err) {
81+
tracer.addErrorAsMetadata(err as Error);
82+
logger.error('Error reading from table. ' + err);
83+
84+
return {
85+
statusCode: 500,
86+
body: JSON.stringify({ 'error': 'Error reading from table.' })
87+
};
88+
}
89+
};
90+
91+
// Wrap the handler with middy
92+
export const handler = middy(getAllItemsHandler)
93+
// Use the middleware by passing the Metrics instance as a parameter
94+
.use(logMetrics(metrics))
95+
// Use the middleware by passing the Logger instance as a parameter
96+
.use(injectLambdaContext(logger, { logEvent: true }))
97+
// Use the middleware by passing the Tracer instance as a parameter
98+
.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

Diff for: examples/cdk/functions/get-by-id.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
2+
import { tableName } from './common/constants';
3+
import { logger, tracer, metrics } from './common/powertools';
4+
import { LambdaInterface } from '@aws-lambda-powertools/commons';
5+
import { docClient } from './common/dynamodb-client';
6+
import { GetCommand } from '@aws-sdk/lib-dynamodb';
7+
import { default as request } from 'phin';
8+
9+
/*
10+
*
11+
* This example uses the Method decorator instrumentation.
12+
* Use TypeScript method decorators if you prefer writing your business logic using TypeScript Classes.
13+
* If you aren’t using Classes, this requires the most significant refactoring.
14+
* Find more Information in the docs: https://awslabs.github.io/aws-lambda-powertools-typescript/
15+
*
16+
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
17+
* @param {APIGatewayProxyEvent} event - API Gateway Lambda Proxy Input Format
18+
*
19+
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
20+
* @returns {Promise<APIGatewayProxyResult>} object - API Gateway Lambda Proxy Output Format
21+
*
22+
*/
23+
24+
class Lambda implements LambdaInterface {
25+
26+
@tracer.captureMethod()
27+
public async getUuid(): Promise<string> {
28+
// Request a sample random uuid from a webservice
29+
const res = await request<{ uuid: string }>({
30+
url: 'https://httpbin.org/uuid',
31+
parse: 'json',
32+
});
33+
const { uuid } = res.body;
34+
35+
return uuid;
36+
}
37+
38+
@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
39+
@logger.injectLambdaContext({ logEvent: true })
40+
@metrics.logMetrics({ throwOnEmptyMetrics: false, captureColdStartMetric: true })
41+
public async handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> {
42+
43+
if (event.httpMethod !== 'GET') {
44+
throw new Error(`getById only accepts GET method, you tried: ${event.httpMethod}`);
45+
}
46+
47+
// Tracer: Add awsRequestId as annotation
48+
tracer.putAnnotation('awsRequestId', context.awsRequestId);
49+
50+
// Logger: Append awsRequestId to each log statement
51+
logger.appendKeys({
52+
awsRequestId: context.awsRequestId,
53+
});
54+
55+
// Call the getUuid function
56+
const uuid = await this.getUuid();
57+
58+
// Logger: Append uuid to each log statement
59+
logger.appendKeys({ uuid });
60+
61+
// Tracer: Add uuid as annotation
62+
tracer.putAnnotation('uuid', uuid);
63+
64+
// Metrics: Add uuid as metadata
65+
metrics.addMetadata('uuid', uuid);
66+
67+
// Get the item from the table
68+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#get-property
69+
try {
70+
if (!tableName) {
71+
throw new Error('SAMPLE_TABLE environment variable is not set');
72+
}
73+
if (!event.pathParameters) {
74+
throw new Error('event does not contain pathParameters');
75+
}
76+
if (!event.pathParameters.id) {
77+
throw new Error('PathParameter id is missing');
78+
}
79+
const data = await docClient.send(new GetCommand({
80+
TableName: tableName,
81+
Key: {
82+
id: event.pathParameters.id
83+
}
84+
}));
85+
const item = data.Item;
86+
87+
logger.info(`Response ${event.path}`, {
88+
statusCode: 200,
89+
body: item,
90+
});
91+
92+
return {
93+
statusCode: 200,
94+
body: JSON.stringify(item)
95+
};
96+
} catch (err) {
97+
tracer.addErrorAsMetadata(err as Error);
98+
logger.error('Error reading from table. ' + err);
99+
100+
return {
101+
statusCode: 500,
102+
body: JSON.stringify({ 'error': 'Error reading from table.' })
103+
};
104+
}
105+
}
106+
107+
}
108+
109+
const handlerClass = new Lambda();
110+
export const handler = handlerClass.handler.bind(handlerClass);

0 commit comments

Comments
 (0)