First Look at Lambda Powertools TypeScript
Tags: aws, serverless, typescript, lambda
Published: 2022-01-10 12:41:01

AWS Senior Solutions Architect [Sara Gerion](https://twitter.com/Sarutule) announced on January 5, 2022 that [Lambda Powertools TypeScript](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/) had [entered public beta](https://twitter.com/Sarutule/status/1478763457625350144). Lambda Powertools is an AWS-sponsored open source project with the goal of improving developer experience and adoption of best practices when using AWS Lambda. Lambda Powertools TypeScript joins [Java](https://awslabs.github.io/aws-lambda-powertools-java/) and [Python](https://awslabs.github.io/aws-lambda-powertools-python/latest/) Lambda Powertools libraries.

## Table of Contents
* <a href="#node">Node.js Tooling for Lambda</a>
* <a href="#decorators">Decorators</a>
* <a href="#no-decorators">No Decorators</a>
* <a href="#capabilities">Capabilities</a>
* <a href="#tracer">Tracer</a>
* <a href="#logger">Logger</a>
* <a href="#metrics">Metrics</a>
* <a href="#package-size">Package Size</a>
* <a href="#conclusion">Conclusion</a>

## Node.js Tooling for Lambda<a name="node"/>

I'm a big fan of TypeScript and in fact [co-authored a book](https://www.amazon.com/gp/product/B093Y29GW3) about it. I don't find myself using Java or Python much, so while I've been interested in Lambda Powertools, I never tried it out until now. Lambda Powertools TypeScript joins [middy](https://middy.js.org/) and [DAZN Lambda Powertools](https://github.com/getndazn/dazn-lambda-powertools) in the Lambda tooling space for the Node.js runtime. Two things that differentiate Lambda Powertools TypeScript from comparable libraries are it is sponsored by AWS and it supports decorators.

Lambda Powertools TypeScript supports both v2 and v3 of the AWS SDK for JavaScript and examples are given for both versions.

## Decorators<a name="decorators"/>

Opinions vary on decorators, but I find them to be useful abstractions and have used them heavily in object-oriented TypeScript. However, there's a fairly major limitation in TypeScript and that is that we can put decorators on class methods, but not functions. This means, unfortunately, code that looks like this is out of reach for now.

```typescript
// This doesn't work :(
@doSomethingGreat()
export const handler = async () => {
  ...
};
```

It's a pity because that would be an excellent developer experience. To attach the `doSomethingGreat` decorator to our handler, we need to write something like this instead.

```typescript
class Lambda {
  @doSomethingGreat()
  public async handler() {
    ...
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler;
```

It's five extra lines - perhaps not a big deal. In any case, there's not a lot the Powertools team can do as it may well be years [before decorators are supported](https://github.com/microsoft/TypeScript/issues/7318) for functions. Keep in mind if we do choose to use decorators and classes in Lambda, we need to be careful around the [this reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this).

Decorators and TypeScript aren't supported out of the box in Lambda (without using [deno](https://github.com/hayd/deno-lambda)) so we'll also need a transpilation step if we go this route. Fortunately this is a mostly solved problem for [AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html), [AWS SAM](https://aws.amazon.com/blogs/mt/introducing-typescript-support-for-building-aws-cloudformation-resource-types/) and [Serverless Framework](https://www.serverless.com/plugins/serverless-plugin-typescript) users. If you want or need to roll your own, [esbuild](https://esbuild.github.io/) is a great place to start and seems to be the bundler of choice for this purpose.

## No Decorators<a name="no-decorators"/>

We may wish to opt out of using classes or transpilers. Lambda Powertools TypeScript can be used without decorators and in fact without TypeScript. We could use the library with vanilla Node.js.

The [documentation](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/) for Lambda Powertools TypeScript is pretty good and provides several examples with even more [on GitHub](https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/examples/cdk).

## Capabilities<a name="capabilities"/>

All three versions of Lambda Powertools include Metrics, Logger and Tracer as core utilities. [Lambda Powertools Python](https://awslabs.github.io/aws-lambda-powertools-python/latest/) includes an Event Handler and several other useful utilities supporting Batch Processing, Idempotency, Validation and more.

Each version of Lambda Powertools is developed independently and the capabilities are custom-tailored to the distinct needs of the different runtimes. This is in contrast to AWS CDK using [jsii](https://github.com/aws/jsii) to publish the same constructs to multiple runtimes. Although it may be frustrating to wait for some of these capabilities, this is likely the right approach as it would just add to the complexity to think about generic code that compiles to something that can decorate custom code supported in multiple runtimes.

To test this out, I've implemented all three of the Lambda Powertools TypeScript utilities in one of my sample projects. I chose my [CDK Async Testing](https://dev.to/aws-builders/testing-the-async-cloud-with-aws-cdk-33aj) project because it contains several Lambda functions and includes asynchronous workflows via EventBridge and Step Functions.

If you want to see my instrumented code, it is available in [this branch](https://github.com/elthrasher/cdk-async-testing-example/tree/powertools).

## Tracer<a name="tracer"/>

In order to instrument my functions with the Tracer, I needed to rewrite them as classes. I chose to use decorators because I'm really undecided about whether to start using classes everywhere, so I need to get a feel for it. Starting off with `collect.ts`, here's the function as it was originally at eleven lines of code.

```typescript
import { Payment } from '../models/payment';

export const handler = async (input: {
  Payload: { Payment: Payment };
}): Promise<{ Status: number; Payment: Payment }> => {
  const min = 0;
  const max = 1;
  const Status = Math.floor(Math.random() * (max - min + 1)) + min;

  return { Status, Payment: input.Payload.Payment };
};
```

And here's the refactor to include the Tracer.

```typescript
import { Tracer } from '@aws-lambda-powertools/tracer';

import type { LambdaInterface } from '@aws-lambda-powertools/commons';
import type { Context } from 'aws-lambda';

import type { Payment } from '../models/payment';

const tracer = new Tracer({ serviceName: 'paymentCollections' });

class Lambda implements LambdaInterface {
  @tracer.captureLambdaHandler()
  public async handler(
    input: { Payload: { Payment: Payment } },
    _context: Context,
  ): Promise<{ Status: number; Payment: Payment }> {
    const min = 0;
    const max = 1;
    const Status = Math.floor(Math.random() * (max - min + 1)) + min;

    return { Status, Payment: input.Payload.Payment };
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler;
```

The function is now 25 lines of code, which isn't horrible. It ends up at 33 lines after adding in the Metrics and Logger utilities. Of course it's expected that my function will be larger as it has more capabilities now. We should see the increase as additive, not multiplicative. My 11 line function became 25 lines. Had it originally been 111 lines, it would've grown to 125, not doubled.

So what do we get for that? The Tracer module wraps the [AWS X-Ray SDK](https://github.com/aws/aws-xray-sdk-node) (as a transitive dependency). It doesn't really add any net new capabilities, but makes the SDK easier to work with. In my experience, that SDK is a bit of a bear so this may be well worth it. We can decorate class methods to introduce new trace segments in a single line of code. We can also use the imperative form to add new traces where we see fit. We can capture AWS clients, but that simply exposes the X-Ray SDK.

One thing that isn't solved here (yet?) is the oddness of working with the X-Ray SDK and DynamoDB DocumentClient. When using DocumentClient with X-Ray, we need to do a workaround as the SDK needs access to the DocumentClient service property, but that's not exposed on the type. Here's how I've solved that problem in the past.

```typescript
import DynamoDB, { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { captureAWSClient } from 'aws-xray-sdk-core';

const client = new DocumentClient();
captureAWSClient((client as DocumentClient & { service: DynamoDB }).service);
```

UPDATE!<a name="update1"/> This was solved in [release 0.5.0](https://github.com/awslabs/aws-lambda-powertools-typescript/releases/tag/v0.5.0)! The above code can now be written as:

```typescript
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { captureAWSClient } from 'aws-xray-sdk-core';

const client = new DocumentClient();
captureAWSClient(client);
```

Very much appreciate the quick turnaround on this improvement! Now that I have better DX, here's what my instrumented app looks like.

{% spoiler original text %}
And now with Lambda Powertools TypeScript.

```typescript
import DynamoDB, { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'paymentCollections' });
const client = new DocumentClient();
tracer.captureAWSClient((client as DocumentClient & { service: DynamoDB }).service);
```

So basically the same thing. The advantage of this utility will really only come into play vs just using the X-Ray SDK if we're adding several segments to our code. I don't think I'm unlocking that in my sample application, but I did enable tracing in all of my functions and my state machine and the result is pretty good.
{% endspoiler %}

![ServiceLens Map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtw31ru7xj7nq77s6ytm.png)

Not only do I get this great service map, I also get detailed traces.

![Trace #1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6mlay2n1c3z86pxjtbi.png)

![Trace #2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fn0c6pso1cu34992n3id.png)

The Tracer module is adding the `## index.handler` part seen on these screenshots. I'll want to add more traces to really take advantage of this tool. Overall getting these detailed traces of my entire application through all the functions and services is quite impressive and useful. X-Ray is doing most of the work, but a better developer experience means we'll wind up with more applications instrumented properly and that's certainly a good thing.

Getting ahead of myself slightly here, but traces also include logs and logs for the instrumented segments will be found alongside the traces in CloudWatch.

![Trace logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9dhus5xdbec96cxbo5ix.png)
 
Again, this is great. I'm able to write my application in a distributed way that uses single-purpose functions, but see the whole picture when it executes.

X-Ray can be a fairly cheap service to use provided we remember to set a sampling rate on high-throughput applications. Pricing seems to be based on trace so adding additional segments to a trace doesn't add to the cost.

## Logger<a name="logger"/>

The Logger utility is a drop-in replacement for any logger, including console. The value add Logger brings is the ability to inject the Lambda context into each logging message. When we annotate our handler method with `@logger.injectLambdaContext()`, then use logger.info, we'll see log messages that look like this.

```json
{
  "cold_start": true,
  "function_arn": "arn:aws:lambda:us-east-1:1234567890:function:openCollection",
  "function_memory_size": 128,
  "function_name": "openCollection",
  "function_request_id": "df21844d-f4b6-49b4-b246-da54183ce5cf",
  "level": "INFO",
  "message": "Payment set to collection",
  "service": "paymentCollections",
  "timestamp": "2022-01-09T18:46:20.622Z"
}
```

If we plan to ingest logs into a search index or even if we just want to use CloudWatch Logs Insights, this can be really handy as the structure will help us to search and filter log messages. On the other hand, if we're just going through a few log messages, this can be a bit noisy. We should keep in mind that any log service (including CloudWatch) is going to bill on volume and extremely verbose logs can be expensive.

With that in mind, there are a lot of good features for the Logger utility and we can structure our logs however we like. Additionally Logger includes a sampling rate feature which can be used to keep costs down.

By default logger methods take one or more arguments. The first argument needs to be a string or object with a message key. I noticed that if I gave a string as a subsequent argument, the string would be converted into a character array and printed like that, so that's something to watch out for.

## Metrics<a name="metrics"/>

The Metrics utility is used to publish custom CloudWatch metrics. Although Lambda automatically publishes a number of useful metrics like latency, concurrent executions, throttling, etc., custom metrics give us the opportunity to complete the observability story by adding relevant business events to our metrics.

Tracking reliability is important, but it _does not tell the whole story!_ If anything, custom metrics should be the most important ones. How many customers signed up this week? How many of them were able to complete valuable workflows? The answers to these questions lie in our code and if we emit custom metrics, they can also be in our dashboards.

Custom metrics have a [pricing structure](https://aws.amazon.com/cloudwatch/pricing/) which can be expensive. [Embedded Metrics Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html) can help manage the cost and is supported by Lambda Powertools TypeScript. Again, [the docs](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/metrics/) here are pretty good, so no need for me to break it down. Instead let's look at the experience. I've added a custom metric of "collectionSuccess" to my collectionSuccess function. In my hypothetical app, some payments wind up in collections and here I'm marking whether or not the collection resulted in a payment.

```typescript
import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';

import { PaymentEntity, PaymentStatus } from '../models/payment';

import type { LambdaInterface } from '@aws-lambda-powertools/commons';
import type { Context } from 'aws-lambda';
import type { Payment } from '../models/payment';

const powerToolsConfig = { namespace: 'payments', serviceName: 'paymentCollections' };
const logger = new Logger(powerToolsConfig);
const metrics = new Metrics(powerToolsConfig);
const tracer = new Tracer(powerToolsConfig);

class Lambda implements LambdaInterface {
  @logger.injectLambdaContext()
  @metrics.logMetrics({ captureColdStartMetric: true })
  @tracer.captureLambdaHandler()
  public async handler(input: { Payload: { Payment: Payment } }, _context: Context): Promise<void> {
    try {
      await PaymentEntity.update({ id: input.Payload.Payment.id, status: PaymentStatus.COLLECTION_SUCCESS });
      metrics.addMetric('collectionSuccess', MetricUnits.Count, 1);
    } catch (e) {
      logger.error('Failed to record collection success', e);
      throw e;
    }
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler;
```

Adding `@metrics.logMetrics` will cause any metrics we emit to also be logged to CloudWatch. We may or may not want that (keeping costs in mind again). To add a custom metric, we simply use `metrics.addMetric`.

I instrumented my app to emit metrics for cold starts as well as some custom metrics that describe important events in my application, such as successful and failed payments and collections. Because the point of my application is to demonstrate an integration test, I also instrumented custom metrics that indicate when the test is run.

![CloudWatch Metrics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oiyplsqdt5p9w91gngg6.png)

These metrics can be found in CloudWatch Metrics, placed in a dashboard or exported via API to a 3rd party tool.

## Package Size<a name="package-size"/>

Adding all of the utilities to my project seemed to add about 600kb unminified or 200kb to minified bundles. Given the value and the need to chain some dependencies into the AWS SDK or X-Ray SDK, this seems quite reasonable and the team has done a good job of staying with their **Keep it lean** tenent.

## Conclusion<a name="conclusion"/>

Lambda Powertools does an excellent job putting focus on the kinds of utilities developers really need to improve their applications and follow best practices. The core modules here all focus on observability and that emphasis is needed and appreciated. The team has done a good job developing an API that will be attractive to developers who want to use decorators as well as those who don't.

I eagerly await the general availability of this library and will follow the [roadmap](https://github.com/awslabs/aws-lambda-powertools-roadmap/projects/1) to see what's coming up and how I can get involved.

[COVER](https://publicdomainreview.org/essay/revolutionary-colossus)