Skip to content

Metrics

Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF).

These metrics can be visualized through Amazon CloudWatch Console.

Key features

  • Aggregating up to 100 metrics using a single CloudWatch EMF object (large JSON blob).
  • Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics).
  • Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency.
  • Creating a one-off metric with different dimensions.


Screenshot of the Amazon CloudWatch Console showing an example of business metrics in the Metrics Explorer
Metrics showcase - Metrics Explorer

Terminologies

If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility:

  • Namespace. It's the highest level container that will group multiple metrics from multiple services for a given application, for example ServerlessEcommerce.
  • Dimensions. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example ColdStart metric by Payment service.
  • Metric. It's the name of the metric, for example: SuccessfulBooking or UpdatedBooking.
  • Unit. It's a value representing the unit of measure for the corresponding metric, for example: Count or Seconds.
  • Resolution. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more here.
metrics terminology diagram
Metric terminology, visually explained

Getting started

Installation

Install the library in your project:

1
npm install @aws-lambda-powertools/metrics

Caution

When using the Lambda Advanced Logging Controls feature you must install version of Powertools for AWS Lambda (TypeScript) v1.15.0 or newer.

Usage

The Metrics utility must always be instantiated outside of the Lambda handler. In doing this, subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time. In addition, Metrics can track cold start and emit the appropriate metrics.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

Utility settings

The library requires two settings. You can set them as environment variables, or pass them in the constructor.

These settings will be used across all metrics emitted:

Setting Description Environment variable Default Allowed Values Example Constructor parameter
Service Optionally, sets service metric dimension across all metrics POWERTOOLS_SERVICE_NAME service_undefined Any string serverlessAirline serviceName
Metric namespace Logical container where all metrics will be placed POWERTOOLS_METRICS_NAMESPACE default_namespace Any string serverlessAirline default_namespace

Tip

Use your application name or main service as the metric namespace to easily group all metrics

Example using AWS Serverless Application Model (SAM)

The Metrics utility is instantiated outside of the Lambda handler. In doing this, the same instance can be used across multiple invocations inside the same execution environment. This allows Metrics to be aware of things like whether or not a given invocation had a cold start or not.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

// Metrics parameters fetched from the environment variables (see template.yaml tab)
const metrics = new Metrics();
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);

// You can also pass the parameters in the constructor
// const metrics = new Metrics({
//   namespace: 'serverlessAirline',
//   serviceName: 'orders'
// });
1
2
3
4
5
6
7
8
9
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs20.x
      Environment:
      Variables:
        POWERTOOLS_SERVICE_NAME: orders
        POWERTOOLS_METRICS_NAMESPACE: serverlessAirline

You can initialize Metrics anywhere in your code - It'll keep track of your aggregate metrics in memory.

Creating metrics

You can create metrics using the addMetric method, and you can create dimensions for all your aggregate metrics using the addDimension method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  metrics.publishStoredMetrics();
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addDimension('environment', 'prod');
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  metrics.publishStoredMetrics();
};

Autocomplete Metric Units

Use the MetricUnit enum to easily find a supported metric unit by CloudWatch. Alternatively, you can pass the value as a string if you already know them e.g. "Count".

Metrics overflow

CloudWatch EMF supports a max of 100 metrics per batch. Metrics will automatically propagate all the metrics when adding the 100th metric. Subsequent metrics, e.g. 101th, will be aggregated into a new EMF object, for your convenience.

Do not create metrics or dimensions outside the handler

Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.

Adding high-resolution metrics

You can create high-resolution metrics passing resolution as parameter to addMetric.

When is it useful?

High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {
  MetricResolution,
  MetricUnit,
  Metrics,
} from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric(
    'successfulBooking',
    MetricUnit.Count,
    1,
    MetricResolution.High
  );
};

Autocomplete Metric Resolutions

Use the MetricResolution type to easily find a supported metric resolution by CloudWatch. Alternatively, you can pass the allowed values of 1 or 60 as an integer.

Adding multi-value metrics

You can call addMetric() with the same name multiple times. The values will be grouped together in an array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('performedActionA', MetricUnit.Count, 2);
  // do something else...
  metrics.addMetric('performedActionA', MetricUnit.Count, 1);
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
    "performedActionA": [
        2,
        1
    ],
    "_aws": {
        "Timestamp": 1592234975665,
        "CloudWatchMetrics": [
            {
            "Namespace": "serverlessAirline",
            "Dimensions": [
                [
                "service"
                ]
            ],
            "Metrics": [
                {
                "Name": "performedActionA",
                "Unit": "Count"
                }
            ]
            }
        ]
    },
    "service": "orders"
}

Adding default dimensions

You can add default dimensions to your metrics by passing them as parameters in 4 ways:

  • in the constructor
  • in the Middy-compatible middleware
  • using the setDefaultDimensions method
  • in the decorator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
  defaultDimensions: { environment: 'prod', foo: 'bar' },
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

A note about Middy

We guarantee support for both Middy.js v4.x & v5.x with the latter being available only if you are using ES modules. Check their docs to learn more about Middy and its middleware stack as well as best practices when working with Powertools.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

// Wrap the handler with middy
export const handler = middy(lambdaHandler)
  // Use the middleware by passing the Metrics instance as a parameter
  .use(
    logMetrics(metrics, {
      defaultDimensions: { environment: 'prod', foo: 'bar' },
    })
  );
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});
metrics.setDefaultDimensions({ environment: 'prod', foo: 'bar' });

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

Note

The class method decorators in this project follow the experimental implementation enabled via the experimentalDecorators compiler option in TypeScript.

Additionally, they are implemented to decorate async methods. When decorating a synchronous one, the decorator replaces its implementation with an async one causing the caller to have to await the now decorated method.

If this is not the desired behavior, you can use the logMetrics middleware instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});
const DEFAULT_DIMENSIONS = { environment: 'prod', foo: 'bar' };

export class Lambda implements LambdaInterface {
  // Decorate your handler class method
  @metrics.logMetrics({ defaultDimensions: DEFAULT_DIMENSIONS })
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
  1. Binding your handler method allows your handler to access this within the class methods.

If you'd like to remove them at some point, you can use the clearDefaultDimensions method.

Flushing metrics

As you finish adding all your metrics, you need to serialize and "flush them" by calling publishStoredMetrics(). This will print the metrics to standard output.

You can flush metrics automatically using one of the following methods:

Using the Middy middleware or decorator will automatically validate, serialize, and flush all your metrics. During metrics validation, if no metrics are provided then a warning will be logged, but no exception will be thrown. If you do not use the middleware or decorator, you have to flush your metrics manually.

Metric validation

If metrics are provided, and any of the following criteria are not met, a RangeError error will be thrown:

Middy middleware

See below an example of how to automatically flush metrics with the Middy-compatible logMetrics middleware.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

export const handler = middy(lambdaHandler).use(logMetrics(metrics));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "successfulBooking": 1.0,
    "_aws": {
        "Timestamp": 1592234975665,
        "CloudWatchMetrics": [{
            "Namespace": "serverlessAirline",
            "Dimensions": [
                [ "service" ]
            ],
            "Metrics": [{
                "Name": "successfulBooking",
                "Unit": "Count"
            }]
        }]
    },
    "service": "orders"
}

Using the class decorator

Note

The class method decorators in this project follow the experimental implementation enabled via the experimentalDecorators compiler option in TypeScript.

Additionally, they are implemented to decorate async methods. When decorating a synchronous one, the decorator replaces its implementation with an async one causing the caller to have to await the now decorated method.

If this is not the desired behavior, you can use the logMetrics middleware instead.

The logMetrics decorator of the metrics utility can be used when your Lambda handler function is implemented as method of a Class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

class Lambda implements LambdaInterface {
  @metrics.logMetrics()
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
  1. Binding your handler method allows your handler to access this within the class methods.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "successfulBooking": 1.0,
    "_aws": {
        "Timestamp": 1592234975665,
        "CloudWatchMetrics": [{
            "Namespace": "successfulBooking",
            "Dimensions": [
                [ "service" ]
            ],
            "Metrics": [{
                "Name": "successfulBooking",
                "Unit": "Count"
            }]
        }]
    },
    "service": "orders"
}

Manually

You can manually flush the metrics with publishStoredMetrics as follows:

Warning

Metrics, dimensions and namespace validation still applies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 10);
  metrics.publishStoredMetrics();
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "successfulBooking": 1.0,
    "_aws": {
        "Timestamp": 1592234975665,
        "CloudWatchMetrics": [{
            "Namespace": "successfulBooking",
            "Dimensions": [
                [ "service" ]
            ],
            "Metrics": [{
                "Name": "successfulBooking",
                "Unit": "Count"
            }]
        }]
    },
    "service": "orders"
}

Throwing a RangeError when no metrics are emitted

If you want to ensure that at least one metric is emitted before you flush them, you can use the throwOnEmptyMetrics parameter and pass it to the middleware or decorator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

export const handler = middy(lambdaHandler).use(
  logMetrics(metrics, { throwOnEmptyMetrics: true })
);

Capturing a cold start invocation as metric

You can optionally capture cold start metrics with the logMetrics middleware or decorator via the captureColdStartMetric param.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

export const handler = middy(lambdaHandler).use(
  logMetrics(metrics, { captureColdStartMetric: true })
);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

export class MyFunction implements LambdaInterface {
  @metrics.logMetrics({ captureColdStartMetric: true })
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  }
}

If it's a cold start invocation, this feature will:

  • Create a separate EMF blob solely containing a metric named ColdStart
  • Add the function_name, service and default dimensions

This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions.

We do not emit 0 as a value for the ColdStart metric for cost-efficiency reasons. Let us know if you'd prefer a flag to override it.

Advanced

Adding metadata

You can add high-cardinality data as part of your Metrics log with the addMetadata method. This is useful when you want to search highly contextual information along with your metrics in your logs.

Warning

This will not be available during metrics visualization - Use dimensions for this purpose

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
  metrics.addMetadata('bookingId', '7051cd10-6283-11ec-90d6-0242ac120003');
};

export const handler = middy(lambdaHandler).use(logMetrics(metrics));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "successfulBooking": 1.0,
    "_aws": {
        "Timestamp": 1592234975665,
        "CloudWatchMetrics": [{
            "Namespace": "serverlessAirline",
            "Dimensions": [
                [ "service" ]
            ],
            "Metrics": [{
                "Namespace": "exampleApplication",
                "Dimensions": [
                    [ "service" ]
                ],
                "Metrics": [{
                    "Name": "successfulBooking",
                    "Unit": "Count"
                }]
            }]
        }]
    },
    "service": "orders",
    "bookingId": "7051cd10-6283-11ec-90d6-0242ac120003"
}

Single metric with different dimensions

CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric if you have a metric that should have different dimensions.

Info

For cost-efficiency, this feature would be used sparsely since you pay for unique metric. Keep the following formula in mind:

unique metric = (metric_name + dimension_name + dimension_value)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  metrics.addDimension('metricUnit', 'milliseconds');
  // This metric will have the "metricUnit" dimension, and no "metricType" dimension:
  metrics.addMetric('latency', MetricUnit.Milliseconds, 56);

  const singleMetric = metrics.singleMetric();
  // This metric will have the "metricType" dimension, and no "metricUnit" dimension:
  singleMetric.addDimension('metricType', 'business');
  singleMetric.addMetric('orderSubmitted', MetricUnit.Count, 1);
};

export const handler = middy(lambdaHandler).use(logMetrics(metrics));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
  namespace: 'serverlessAirline',
  serviceName: 'orders',
});

class Lambda implements LambdaInterface {
  @metrics.logMetrics()
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    metrics.addDimension('metricUnit', 'milliseconds');
    // This metric will have the "metricUnit" dimension, and no "metricType" dimension:
    metrics.addMetric('latency', MetricUnit.Milliseconds, 56);

    const singleMetric = metrics.singleMetric();
    // This metric will have the "metricType" dimension, and no "metricUnit" dimension:
    singleMetric.addDimension('metricType', 'business');
    singleMetric.addMetric('orderSubmitted', MetricUnit.Count, 1);
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
  1. Binding your handler method allows your handler to access this within the class methods.

Testing your code

When unit testing your code that uses the Metrics utility, you may want to silence the logs emitted by the utility or assert that metrics are being emitted correctly. By default, the utility manages its own console instance, which means that you can't easily access or mock the logs emitted by the utility.

To make it easier to test your code, you can set the POWERTOOLS_DEV environment variable to true to instruct the utility to use the global console object instead of its own.

This allows you to spy on the logs emitted by the utility and assert that the metrics are being emitted correctly.

Spying on emitted metrics
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
describe('Metrics tests', () => {
  beforeAll(() => {
    process.env.POWERTOOLS_DEV = 'true';
  })

  it('function metrics properly', async () => {
    // Prepare
    const metricsSpy = jest.spyOn(console, 'log').mockImplementation();

    // Act & Assess
  });
});

When running your tests with both Jest and Vitest, you can use the --silent flag to silence the logs emitted by the utility.

Disabling logs while testing
1
export POWERTOOLS_DEV=true && npx vitest --silent