Test Drive AWS Lambda Powertools for Typescript
Tags: javascript, tutorial, aws, serverless
Published: 2022-01-10 10:35:54


Last week, AWS announced the beta release of [AWS Lambda Powertools Typescript](https://github.com/awslabs/aws-lambda-powertools-typescript). This is a suite of TypeScript utilities for AWS Lambda functions to enable the adoption of best practices in areas such as structured logging, tracing and custom metrics. It follows on the back of the existing [AWS Lambda Powertools Python](https://awslabs.github.io/aws-lambda-powertools-python/latest/) and [AWS Lambda Powertools Java](https://awslabs.github.io/aws-lambda-powertools-java/) libraries which have been hugely popular.

Over the past few years, serverless architectures on AWS have converged upon multiple individual AWS Lambda functions, with each one implementing least privilege and responsible for one task (single responsibility principle). This is great from a security, testing and availability perspective, but meant duplicating boiler plate code in each function.

The next natural step was to look at middleware such as [Middy](https://github.com/middyjs/middy) to decorate the main business logic, and allow these tasks to be defined once and used consistently. I remember listening to a Comic Relief talk about their Lambda wrapper back in [2019](https://medium.com/comic-relief/aws-summit-london-case-study-going-serverless-at-comic-relief-51a22c15e398). Since then, I was introduced by [Yan Cui](https://twitter.com/theburningmonk) to [DAZN Lambda Powertools](https://github.com/getndazn/dazn-lambda-powertools), which was the inspiration for this new project.

These tools both used Middy, but provide an opinionated view on top of standards to be adopted. What I love about the AWS Lambda Powertools is this it allows a new project to be stood up, that conforms to the best practices as set out in the Serverless Lens of the Well Architected Framework. This is a view shared by many in AWS including [Francessca Vasquez](https://twitter.com/Francessca_V) and [Werner Vogels](https://twitter.com/Werner)

![Twitter Quote](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8y995b69bksh0arpe5s.png)

This initial release of the AWS Lambda Powertools Typescript provides features around:

* Logging
* Tracing
* Metrics

My goal was to take an existing Nodejs serverless prototype, and implement these features to see what value I get out of them. To start with, I decided to use Middy middleware to provide some additional benefits.

This meant installing the core module

```shell
npm install --save @middy/core
```

and wrapping the existing function handler in the `middy()` factory function

```javascript
const middy = require('@middy/core');

const handler = async (event) => {
    ...
};

module.exports.handler = middy(handler);
```

Now we were reading to start adding the new features.

### Logging

The `Logger` provides an opinionated logger with output structures as JSON.

The first step was to install the library:

```shell
npm install @aws-lambda-powertools/logger
```

The library requires a log level and a service name to be set. I was setting these using environment variables. In the `provider` section of the `serverless.yml` file, I set the following environment variables that would be applied to all functions defined:

* LOG_LEVEL - sets how minimum level that will be output
* LOGGER_SAMPLE_RATE - this was set to 0.2 meaning that 20% of all logs will be output, including debug. This sampling decision gets made at the initialisation of the Logger

Each individual function then had a service name defined:

```yaml
provider:
  name: aws
  runtime: nodejs14.x
  ...
  environment:
    LOG_LEVEL: WARN
    POWERTOOLS_LOGGER_SAMPLE_RATE: 0.2

functions:
  createLicence:
    ...
    environment:
      POWERTOOLS_SERVICE_NAME: createLicence
```

Each Lambda function was instrumented as follows:

```javascript
const { Logger, injectLambdaContext } = require('@aws-lambda-powertools/logger');
const middy = require('@middy/core');

//  Params fetched from the env vars
const logger = new Logger();

const handler = async (event) => {
    logger.debug(...)
};

module.exports.handler = middy(handler)
  .use(injectLambdaContext(logger));
```

The `injectLambdaContext` enriches the structured logs with key Lambda context information. This meant all my logs in CloudWatch used the following JSON format:

```json
{
    "cold_start": true,
    "function_arn": "arn:aws:lambda:eu-west-1:{account-id}:function:create-licence-dev",
    "function_memory_size": 512,
    "function_name": "create-licence-dev",
    "function_request_id": "5e3298ed-e7d2-483a-a775-67c70927ad5d",
    "level": "DEBUG",
    "message": "In the create licence handler",
    "service": "qldb-simple-demo",
    "timestamp": "2022-01-08T13:40:59.640Z"
}
```

This is incredibly rich, and also meant I could use simple queries in `CloudWatch Logs Insights` such as

```shell
stats count(*) by cold_start
```

to show the number of cold starts vs non-cold starts and then start grouping by time

### Tracer

Tracer is an 'opinionated thin wrapper' around `AWS X-Ray`. 

The first step again was to install the library:

```shell
npm install @aws-lambda-powertools/tracer
```

Next, each Lambda function needs permission to send traces to X-Ray. I did this at the provider level in the `serverless.yml` file as follows:

```yaml
provider:
  tracing:
    apiGateway: true
    lambda: true
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - 'qldb:SendCommand'
          Resource:
            - !Sub "arn:aws:qldb:${AWS::Region}:${AWS::AccountId}:ledger/qldb-simple-demo-${self:provider.stage}"
        - Effect: "Allow"
          Action:
            - "xray:PutTraceSegments"
            - "xray:PutTelemetryRecords"
          Resource: "*" 
```

The skeleton of each Lambda function is shown below. The tracer library requires a Service Name value, and this is the same setting used for the Logger. I used the `captureLambdaHandler` middleware. This has a number of benefits, including:

* Creating a `ColdStart` annotation
* Creating a `Service` annotation
* Capture responses or exceptions generated by the handler and include them as tracing metadata

I also used the `captureAWS` methdod to patch all AWS SDK v2 clients which the demo uses.

```javascript
const { Tracer, captureLambdaHandler } = require('@aws-lambda-powertools/tracer');
const middy = require('@middy/core');

//  Params fetched from the env vars
const tracer = new Tracer();
tracer.captureAWS(require('aws-sdk'));

const handler = async (event) => {
    ...
};

module.exports.handler = middy(handler)
  .use(injectLambdaContext(logger))
  .use(captureLambdaHandler(tracer));

```

In the `AWS X-Ray` service, you can look at the raw data. This shows the custom annotations created by the Tracer library.

```json
    ...
    "subsegments": [
        {
            ...
            "annotations": {
                "ColdStart": false,
                "Service": "createLicence"
            },
```

This also means you can use a filter expression to filter the traces that are shown:

```shell
annotation.Service = "createLicence" AND annotation.ColdStart = true
```

Clicking on one of the traces, you can see the trace map, which in this case traces from API Gateway to Lambda to QLDB. As we had filtered by cold start, the trace details also show the initialisation time for the Lambda function.

![Tracer Service Map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttrvvhxxjq2d6y50apkx.png)

In the trace above, an HTTP 400 error is returned, as it was a duplicate request. This was returned using a custom response as opposed to throwing an error from the function, which would have been highlighted in red in the trace.

As the responses are returned as metadata, we can click into the metadata section of the function to see the actual response

![Tracer Metadata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eh1qfdyh87zl5px01ji8.png)

This can be disabled if it might contain sensitive data.

### Metrics

Finally, it was time to capture some metrics, starting again by installing the library:

```shell
npm install @aws-lambda-powertools/metrics
```

The Metrics library requires a Metrics Namespace and Service Name which were set as environment variables again.

The `logMetrics` middleware is useful as it will automatically flush any metrics created. It also supports a `captureColdStartMetric` param which is set to true. This allows you to keep cold start metrics separate from application metrics if needed.

To test it out, I added two simple metrics, which counted the number of successful and failed attempts to create a new licence:

```javascript
const { Metrics, MetricUnits, logMetrics } = require('@aws-lambda-powertools/metrics');
const middy = require('@middy/core');

//  Params fetched from the env vars
const metrics = new Metrics();

const handler = async (event) => {
    try {
        metrics.addMetric('createLicenceSucceeded', MetricUnits.Count, 1);
    } catch (error) {
        metrics.addMetric('createLicenceFailed', MetricUnits.Count, 1);
    }
};

module.exports.handler = middy(handler)
  .use(injectLambdaContext(logger))
  .use(captureLambdaHandler(tracer))
  .use(logMetrics(metrics, { captureColdStartMetric: true }));
```

All that was left was to send in a few requests, including several duplicate requests that would fail, and it was then simple to start graphing some of these metrics:

![Powertools Metrics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77hcpftjvxq7m4ijgucx.png)

### Conclusion

AWS Lambda Powertools for Typescript is an awesome new library for anyone that develops Lambda functions in JavaScript. With the experience and support of the developer experience team at AWS, you should definitely check it out, and I can see it being my go to library for bootstrapping all new serverless projects written in JavaScript. Hats off to the team that made the release possible, which includes [Sara Gerion](https://twitter.com/Sarutule), [Heitor Lessa](https://twitter.com/heitor_lessa), [Florian Chazal](https://twitter.com/flochaz1), [@ijemmy](https://github.com/ijemmy), [Andrea Amorosi](https://twitter.com/dreamorosi), [Alan Churley](https://github.com/alan-churley), and [Michael Bahr](https://twitter.com/bahrdev).

To try this out yourself, check out the backend folder in [qldb simple demo](https://github.com/AWS-South-Wales-User-Group/qldb-simple-demo) on GitHub.