From 5956e8d6f224a8cefab669d2a282a810f3553d3a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 2 Oct 2024 16:16:19 +0200 Subject: [PATCH 1/4] chore(metrics): clean up MetricsInterface & API links --- packages/metrics/README.md | 127 ++--- packages/metrics/src/Metrics.ts | 460 +++++++++++------ .../src/config/EnvironmentVariablesService.ts | 8 +- packages/metrics/src/constants.ts | 26 + packages/metrics/src/middleware/middy.ts | 39 +- .../src/types/ConfigServiceInterface.ts | 4 +- packages/metrics/src/types/Metrics.ts | 466 +++++++++++++++++- .../metrics/src/types/MetricsInterface.ts | 32 -- packages/metrics/src/types/index.ts | 2 +- 9 files changed, 849 insertions(+), 315 deletions(-) delete mode 100644 packages/metrics/src/types/MetricsInterface.ts diff --git a/packages/metrics/README.md b/packages/metrics/README.md index 40f7036aa7..8d327f48c1 100644 --- a/packages/metrics/README.md +++ b/packages/metrics/README.md @@ -1,15 +1,14 @@ -# Powertools for AWS Lambda (TypeScript) +# Powertools for AWS Lambda (TypeScript) Powertools for AWS Lambda (TypeScript) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda/typescript/latest/#features). You can use the library in both TypeScript and JavaScript code bases. -- [Intro](#intro) - [Usage](#usage) - - [Basic usage](#basic-usage) - [Flushing metrics](#flushing-metrics) - [Capturing cold start as a metric](#capturing-cold-start-as-a-metric) - - [Adding metadata](#adding-metadata) + - [Class method decorator](#class-method-decorator) + - [Middy.js middleware](#middyjs-middleware) - [Contribute](#contribute) - [Roadmap](#roadmap) - [Connect](#connect) @@ -19,88 +18,52 @@ You can use the library in both TypeScript and JavaScript code bases. - [Using Lambda Layer](#using-lambda-layer) - [License](#license) -## Intro - ## Usage +The library provides a utility function to emit metrics to CloudWatch using [Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html). + To get started, install the library by running: ```sh npm i @aws-lambda-powertools/metrics ``` -### Basic usage - -The library provides a utility function to emit metrics to CloudWatch using [Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html). +After initializing the Metrics class, you can add metrics using the [Metrics.addMetric](`addMetric()`) method. The metrics are stored in a buffer and are flushed when calling [Metrics.publishStoredMetrics](`publishStoredMetrics()`). Each metric can have dimensions and metadata added to it. ```ts -import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders', + defaultDimensions: { environment: process.env.ENVIRONMENT }, }); -export const handler = async ( - _event: unknown, - _context: unknown -): Promise => { +export const handler = async (event: { requestId: string }) => { + metrics.addMetadata('request_id', event.requestId); metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + metrics.publishStoredMetrics(); }; ``` ### 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: - -- manually by calling `publishStoredMetrics()` at the end of your Lambda function - -```ts -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 => { - metrics.addMetric('successfulBooking', MetricUnit.Count, 1); - metrics.publishStoredMetrics(); -}; -``` - -- middy compatible middleware `logMetrics()` +As you finish adding all your metrics, you need to serialize and "flush them" by calling [publishStoredMetrics()](`publishStoredMetrics()`), which will emit the metrics to stdout in the Embedded Metric Format (EMF). The metrics are then picked up by the Lambda runtime and sent to CloudWatch. -```ts -import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; -import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'; -import middy from '@middy/core'; +When you -const metrics = new Metrics({ - namespace: 'serverlessAirline', - serviceName: 'orders', -}); +### Capturing cold start as a metric -const lambdaHandler = async ( - _event: unknown, - _context: unknown -): Promise => { - metrics.addMetric('successfulBooking', MetricUnit.Count, 1); -}; +You can flush metrics automatically using one of the following methods: -export const handler = middy(lambdaHandler).use(logMetrics(metrics)); -``` +### Class method decorator -- using decorator `@logMetrics()` +If you are using TypeScript and are comfortable with writing classes, you can use the `@logMetrics()` decorator to automatically flush metrics at the end of your Lambda function as well as configure additional options such as throwing an error if no metrics are added, capturing cold start as a metric, and more. ```ts +import type { Context } from 'aws-lambda'; import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; -import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; const metrics = new Metrics({ namespace: 'serverlessAirline', @@ -108,45 +71,27 @@ const metrics = new Metrics({ }); class Lambda implements LambdaInterface { - @metrics.logMetrics() - public async handler(_event: unknown, _context: unknown): Promise { + ⁣@metrics.logMetrics({ captureColdStartMetric: true, throwOnEmptyMetrics: true }) + public async handler(event: { requestId: string }, _: Context) { + metrics.addMetadata('request_id', event.requestId); metrics.addMetric('successfulBooking', MetricUnit.Count, 1); } } const handlerClass = new Lambda(); -export const handler = handlerClass.handler.bind(handlerClass); +export const handler = handlerClass.handler.bind(handlerClass); ``` -Using the Middy middleware or decorator will automatically validate, serialize, and flush all your metrics. - -### Capturing cold start as a metric - -You can optionally capture cold start metrics with the logMetrics middleware or decorator via the captureColdStartMetric param. - -```ts -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', -}); +Decorators are a Stage 3 proposal for JavaScript and are not yet part of the ECMAScript standard. The current implmementation in this library is based on the legacy TypeScript decorator syntax enabled by the [`experimentalDecorators` flag](https://www.typescriptlang.org/tsconfig/#experimentalDecorators) set to `true` in the `tsconfig.json` file. -export class MyFunction implements LambdaInterface { - @metrics.logMetrics({ captureColdStartMetric: true }) - public async handler(_event: unknown, _context: unknown): Promise { - metrics.addMetric('successfulBooking', MetricUnit.Count, 1); - } -} -``` +### Middy.js middleware -### Adding metadata +If instead you are using [Middy.js](http://middy.js.org) and prefer to use middleware, you can use the `@logMetrics()` middleware to do the same as the class method decorator. -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. +The `@logMetrics()` middleware can be used with Middy.js to automatically flush metrics at the end of your Lambda function as well as configure additional options such as throwing an error if no metrics are added, capturing cold start as a metric, and set default dimensions. ```ts -import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'; import middy from '@middy/core'; @@ -155,17 +100,17 @@ const metrics = new Metrics({ serviceName: 'orders', }); -const lambdaHandler = async ( - _event: unknown, - _context: unknown -): Promise => { +export const handler = middy(async (event) => { + metrics.addMetadata('request_id', event.requestId); metrics.addMetric('successfulBooking', MetricUnit.Count, 1); - metrics.addMetadata('bookingId', '7051cd10-6283-11ec-90d6-0242ac120003'); -}; - -export const handler = middy(lambdaHandler).use(logMetrics(metrics)); +}).use(logMetrics(metrics, { + captureColdStartMetric: true, + throwOnEmptyMetrics: true, +})); ``` +The `logMetrics()` middleware is compatible with `@middy/core@3.x` and above. + ## Contribute If you are interested in contributing to this project, please refer to our [Contributing Guidelines](https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/CONTRIBUTING.md). diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 9ce41a20ef..127f282200 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -26,64 +26,62 @@ import type { } from './types/index.js'; /** - * ## Intro - * Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF). + * The Metrics utility creates custom metrics asynchronously by logging metrics to standard output following {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html | Amazon CloudWatch Embedded Metric Format (EMF)}. * * These metrics can be visualized through Amazon CloudWatch Console. * - * ## Key features - * * Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob) - * * Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc) - * * Metrics are created asynchronously by CloudWatch service, no custom stacks needed - * * Context manager to create a one off metric with a different dimension + * **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. * - * ## Usage - * - * ### Functions usage with middleware - * - * Using this middleware on your handler function will automatically flush metrics after the function returns or throws an error. - * Additionally, you can configure the middleware to easily: - * * ensure that at least one metric is emitted before you flush them - * * capture a `ColdStart` a metric - * * set default dimensions for all your metrics + * After initializing the Metrics class, you can add metrics using the {@link Metrics.addMetric | `addMetric()`} method. + * The metrics are stored in a buffer and are flushed when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}. + * Each metric can have dimensions and metadata added to it. * * @example * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'; - * import middy from '@middy/core'; + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * defaultDimensions: { environment: 'dev' }, + * }); * - * const lambdaHandler = async (_event: unknown, _context: unknown) => { - * ... + * export const handler = async (event: { requestId: string }) => { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); * }; - * - * export const handler = middy(lambdaHandler).use(logMetrics(metrics)); * ``` * - * ### Object oriented way with decorator + * If you don't want to manually flush the metrics, you can use the {@link Metrics.logMetrics | `logMetrics()`} decorator or + * the Middy.js middleware to automatically flush the metrics after the handler function returns or throws an error. * - * If you are used to TypeScript Class usage to encapsulate your Lambda handler you can leverage the {@link Metrics.logMetrics} decorator to automatically: - * * capture a `ColdStart` metric - * * flush buffered metrics - * * throw on empty metrics + * In addition to this, the decorator and middleware can also be configured to capture a `ColdStart` metric and + * set default dimensions for all metrics. + * + * **Class method decorator** * * @example * * ```typescript - * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + * import type { Context } from 'aws-lambda'; * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * * class Lambda implements LambdaInterface { - * // Decorate your handler with the logMetrics decorator * ⁣@metrics.logMetrics({ captureColdStartMetric: true, throwOnEmptyMetrics: true }) - * public handler(_event: unknown, _context: unknown): Promise { - * // ... - * metrics.addMetric('test-metric', MetricUnit.Count, 10); - * // ... + * public async handler(_event: { requestId: string }, _: Context) { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * } * } * @@ -91,23 +89,35 @@ import type { * export const handler = handlerClass.handler.bind(handlerClass); * ``` * - * ### Standard function + * Note that decorators are a Stage 3 proposal for JavaScript and are not yet part of the ECMAScript standard. + * The current implmementation in this library is based on the legacy TypeScript decorator syntax enabled by the [`experimentalDecorators` flag](https://www.typescriptlang.org/tsconfig/#experimentalDecorators) + * set to `true` in the `tsconfig.json` file. * - * If you are used to classic JavaScript functions, you can leverage the different methods provided to create and publish metrics. + * **Middy.js middleware** * * @example * * ```typescript * import { Metrics, MetricUnit } 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 metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * - * export const handler = async (_event: unknown, __context: unknown): Promise => { - * metrics.captureColdStartMetric(); - * metrics.addMetric('test-metric', MetricUnit.Count, 10); - * metrics.publishStoredMetrics(); - * }; + * export const handler = middy(async () => { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * }).use(logMetrics(metrics, { + * captureColdStartMetric: true, + * throwOnEmptyMetrics: true, + * })); * ``` + * + * The `logMetrics()` middleware is compatible with `@middy/core@3.x` and above. + * */ class Metrics extends Utility implements MetricsInterface { /** @@ -186,13 +196,16 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Add a dimension to the metrics. + * Add a dimension to metrics. * - * A dimension is a key-value pair that is used to group metrics. + * A dimension is a key-value pair that is used to group metrics, and it is included in all metrics emitted after it is added. * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Dimension for more details. - * @param name The name of the dimension - * @param value The value of the dimension + * When calling the {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of + * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are + * included in all metrics, use the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method. + * + * @param name - The name of the dimension + * @param value - The value of the dimension */ public addDimension(name: string, value: string): void { if (MAX_DIMENSION_COUNT <= this.getCurrentDimensionsCount()) { @@ -206,11 +219,15 @@ class Metrics extends Utility implements MetricsInterface { /** * Add multiple dimensions to the metrics. * - * A dimension is a key-value pair that is used to group metrics. + * This method is useful when you want to add multiple dimensions to the metrics at once. + * + * When calling the {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of + * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are + * included in all metrics, use the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method. * - * @param dimensions A key-value pair of dimensions + * @param dimensions - An object with key-value pairs of dimensions */ - public addDimensions(dimensions: { [key: string]: string }): void { + public addDimensions(dimensions: Dimensions): void { const newDimensions = { ...this.dimensions }; for (const dimensionName of Object.keys(dimensions)) { newDimensions[dimensionName] = dimensions[dimensionName]; @@ -226,12 +243,31 @@ class Metrics extends Utility implements MetricsInterface { } /** - * A high-cardinality data part of your Metrics log. + * A metadata key-value pair to be included with metrics. * + * You can use this method to add high-cardinality data as part of your metrics. * This is useful when you want to search highly contextual information along with your metrics in your logs. * - * @param key The key of the metadata - * @param value The value of the metadata + * Note that the metadata is not included in the Amazon CloudWatch UI, but it can be used to search and filter logs. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async (event) => { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; + * ``` + * + * @param key - The key of the metadata + * @param value - The value of the metadata */ public addMetadata(key: string, value: string): void { this.metadata[key] = value; @@ -240,38 +276,37 @@ class Metrics extends Utility implements MetricsInterface { /** * Add a metric to the metrics buffer. * - * By default, metrics are buffered and flushed at the end of the Lambda invocation - * or when calling {@link Metrics.publishStoredMetrics}. + * By default, metrics are buffered and flushed when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, + * or at the end of the handler function when using the {@link Metrics.logMetrics | `logMetrics()`} decorator or the Middy.js middleware. * - * You can add a metric by specifying the metric name, unit, and value. For convenience, - * we provide a set of constants for the most common units in {@link MetricUnit}. + * Metrics are emitted to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. In AWS Lambda, the logs are + * automatically picked up by CloudWatch logs and processed asynchronously. * - * @example - * ```typescript - * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - * - * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); - * ``` + * You can add a metric by specifying the metric name, unit, and value. For convenience, + * we provide a set of constants for the most common units in the {@link MetricUnits | MetricUnit} dictionary object. * - * Optionally, you can specify the metric resolution, which can be either `High` or `Standard`. - * By default, metrics are published with a resolution of `Standard`, click [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition) - * to learn more about metric resolutions. + * Optionally, you can specify a {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition | resolution}, which can be either `High` or `Standard`, using the {@link MetricResolutions | MetricResolution} dictionary object. + * By default, metrics are published with a resolution of `Standard`. * * @example * ```typescript - * import { Metrics, MetricUnit, MetricResolution } from '@aws-lambda-powertools/metrics'; + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * - * metrics.addMetric('successfulBooking', MetricUnit.Count, 1, MetricResolution.High); + * export const handler = async () => { + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; * ``` * - * @param name The metric name - * @param unit The metric unit - * @param value The metric value - * @param resolution - The metric resolution + * @param name - The metric name + * @param unit - The metric unit, see {@link MetricUnits | MetricUnit} + * @param value - The metric value + * @param resolution - The metric resolution, see {@link MetricResolutions | MetricResolution} */ public addMetric( name: string, @@ -284,23 +319,27 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Create a singleMetric to capture cold start. + * Immediately emit a `ColdStart` metric if this is a cold start invocation. * - * If it's a cold start invocation, this feature will: - * * Create a separate EMF blob that contains a single metric named ColdStart - * * Add function_name and service dimensions + * A cold start is when AWS Lambda initializes a new instance of your function. To take advantage of this feature, + * you must instantiate the Metrics class outside of the handler function. * - * This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions, - * as well as avoiding potential data loss from metrics not being published for other reasons. + * By using this method, the metric will be emitted immediately without you having to call {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}. + * + * If you are using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, you can enable this + * feature by setting the `captureColdStartMetric` option to `true`. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * - * export const handler = async (_event: unknown, __context: unknown): Promise => { - * metrics.captureColdStartMetric(); + * export const handler = async () => { + * metrics.captureColdStartMetric(); * }; * ``` */ @@ -320,21 +359,71 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Clear all default dimensions. + * Clear all previously set default dimensions. + * + * This will remove all default dimensions set by the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method + * or via the `defaultDimensions` parameter in the constructor. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * defaultDimensions: { environment: 'dev' }, + * }); + * + * metrics.setDefaultDimensions({ region: 'us-west-2' }); + * + * // both environment and region dimensions are removed + * metrics.clearDefaultDimensions(); + * ``` */ public clearDefaultDimensions(): void { this.defaultDimensions = {}; } /** - * Clear all dimensions. + * Clear all the dimensions added to the Metrics instance via {@link Metrics.addDimension | `addDimension()`} or {@link Metrics.addDimensions | `addDimensions()`}. + * + * These dimensions are normally cleared when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}, but + * you can use this method to clear specific dimensions that you no longer need at runtime. + * + * This method does not clear the default dimensions set via {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} or via + * the `defaultDimensions` parameter in the constructor. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * metrics.addDimension('region', 'us-west-2'); + * + * // ... + * + * metrics.clearDimensions(); // olnly the region dimension is removed + * }; + * ``` + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ public clearDimensions(): void { this.dimensions = {}; } /** - * Clear all metadata. + * Clear all the metadata added to the Metrics instance. + * + * Metadata is normally cleared when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}, but + * you can use this method to clear specific metadata that you no longer need at runtime. + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ public clearMetadata(): void { this.metadata = {}; @@ -342,26 +431,39 @@ class Metrics extends Utility implements MetricsInterface { /** * Clear all the metrics stored in the buffer. + * + * This is useful when you want to clear the metrics stored in the buffer without publishing them. + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ public clearMetrics(): void { this.storedMetrics = {}; } /** - * A decorator automating coldstart capture, throw on empty metrics and publishing metrics on handler exit. + * A class method decorator to automatically log metrics after the method returns or throws an error. + * + * The decorator can be used with TypeScript classes and can be configured to optionally capture a `ColdStart` metric (see {@link Metrics.captureColdStartMetric | `captureColdStartMetric()`}), + * throw an error if no metrics are emitted (see {@link Metrics.setThrowOnEmptyMetrics | `setThrowOnEmptyMetrics()`}), + * and set default dimensions for all metrics (see {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`}). * * @example * * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; + * import type { Context } from 'aws-lambda'; * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * * class Lambda implements LambdaInterface { - * @metrics.logMetrics({ captureColdStartMetric: true }) - * public handler(_event: unknown, __context: unknown): Promise { - * // ... + * ⁣@metrics.logMetrics({ captureColdStartMetric: true }) + * public async handler(_event: { requestId: string }, _: Context) { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * } * } * @@ -369,13 +471,18 @@ class Metrics extends Utility implements MetricsInterface { * export const handler = handlerClass.handler.bind(handlerClass); * ``` * - * @param options - The options to configure the logMetrics decorator + * You can configure the decorator with the following options: + * - `captureColdStartMetric` - Whether to capture a `ColdStart` metric + * - `defaultDimensions` - Default dimensions to add to all metrics + * - `throwOnEmptyMetrics` - Whether to throw an error if no metrics are emitted + * + * @param options - Options to configure the behavior of the decorator, see {@link ExtraOptions} */ public logMetrics(options: ExtraOptions = {}): HandlerMethodDecorator { const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { - this.throwOnEmptyMetrics(); + this.setThrowOnEmptyMetrics(throwOnEmptyMetrics); } if (defaultDimensions !== undefined) { this.setDefaultDimensions(defaultDimensions); @@ -411,18 +518,23 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Synchronous function to actually publish your metrics. (Not needed if using logMetrics decorator). - * It will create a new EMF blob and log it to standard output to be then ingested by Cloudwatch logs and processed automatically for metrics creation. + * Flush the stored metrics to standard output. * - * @example + * The method empties the metrics buffer and emits the metrics to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. + * + * When using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the metrics are automatically flushed after the handler function returns or throws an error. * + * @example * ```typescript * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); // Sets metric namespace, and service as a metric dimension + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * - * export const handler = async (_event: unknown, __context: unknown): Promise => { - * metrics.addMetric('test-metric', MetricUnit.Count, 10); + * export const handler = async () => { + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; * ``` @@ -443,14 +555,16 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Function to create a new metric object compliant with the EMF (Embedded Metric Format) schema which - * includes the metric name, unit, and optionally storage resolution. + * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema. * - * The function will create a new EMF blob and log it to standard output to be then ingested by Cloudwatch - * logs and processed automatically for metrics creation. + * The EMF schema is a JSON object that contains the following properties: + * - `_aws`: An object containing the timestamp and the CloudWatch metrics. + * - `CloudWatchMetrics`: An array of CloudWatch metrics objects. + * - `Namespace`: The namespace of the metrics. + * - `Dimensions`: An array of dimensions for the metrics. + * - `Metrics`: An array of metric definitions. * - * @returns metrics as JSON object compliant EMF Schema Specification - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html for more details + * The object is then emitted to standard output, which in AWS Lambda is picked up by CloudWatch logs and processed asynchronously. */ public serializeMetrics(): EmfOutput { // Storage resolution is included only for High resolution metrics @@ -515,7 +629,26 @@ class Metrics extends Utility implements MetricsInterface { /** * Set default dimensions that will be added to all metrics. * - * @param dimensions The default dimensions to be added to all metrics. + * This method will merge the provided dimensions with the existing default dimensions. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * defaultDimensions: { environment: 'dev' }, + * }); + * + * // Default dimensions will contain both region and environment + * metrics.setDefaultDimensions({ + * region: 'us-west-2', + * environment: 'prod', + * }); + * ``` + * + * @param dimensions - The dimensions to be added to the default dimensions object */ public setDefaultDimensions(dimensions: Dimensions | undefined): void { const targetDimensions = { @@ -529,28 +662,69 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Sets the function name to be added to the metric. + * Set the function name to be added to each metric as a dimension. + * + * When using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function + * name is automatically inferred from the Lambda context. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * metrics.setFunctionName('my-function-name'); + * ``` + * + * @param name - The function name + */ + public setFunctionName(name: string): void { + this.functionName = name; + } + + /** + * Set the flag to throw an error if no metrics are emitted. * - * @param value The function name to be added to the metric. + * You can use this method to enable or disable this opt-in feature. This is useful if you want to ensure + * that at least one metric is emitted when flushing the metrics. This can be useful to catch bugs where + * metrics are not being emitted as expected. + * + * @param enabled - Whether to throw an error if no metrics are emitted */ - public setFunctionName(value: string): void { - this.functionName = value; + public setThrowOnEmptyMetrics(enabled: boolean): void { + this.shouldThrowOnEmptyMetrics = enabled; } /** - * CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric if you have a metric that should have different dimensions. + * Create a new Metrics instance configured to immediately flush a single metric. * - * You don't need to call publishStoredMetrics() after calling addMetric for a singleMetrics, they will be flushed directly. + * CloudWatch EMF uses the same dimensions and timestamp across all your metrics, this is useful when you have a metric that should have different dimensions + * or when you want to emit a single metric without buffering it. * - * @example + * This method is used internally by the {@link Metrics.captureColdStartMetric | `captureColdStartMetric()`} method to emit the `ColdStart` metric immediately + * after the handler function is called. * + * @example * ```typescript - * const singleMetric = metrics.singleMetric(); - * singleMetric.addDimension('InnerDimension', 'true'); - * singleMetric.addMetric('single-metric', MetricUnit.Percent, 50); - * ``` + * import { Metrics } from '@aws-lambda-powertools/metrics'; * - * @returns the Metrics + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * const singleMetric = metrics.singleMetric(); + * // The single metric will be emitted immediately + * singleMetric.addMetric('coldStart', MetricUnit.Count, 1); + * + * // These other metrics will be buffered and emitted when calling `publishStoredMetrics()` + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; */ public singleMetric(): Metrics { return new Metrics({ @@ -562,29 +736,14 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Throw an Error if the metrics buffer is empty. - * - * @example - * - * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName:'orders' }); - * - * export const handler = async (_event: unknown, __context: unknown): Promise => { - * metrics.throwOnEmptyMetrics(); - * metrics.publishStoredMetrics(); // will throw since no metrics added. - * }; - * ``` + * @deprecated Use {@link Metrics.setThrowOnEmptyMetrics | `setThrowOnEmptyMetrics()`} instead. */ public throwOnEmptyMetrics(): void { this.shouldThrowOnEmptyMetrics = true; } /** - * Gets the current number of dimensions stored. - * - * @returns the number of dimensions currently stored + * Gets the current number of dimensions count. */ private getCurrentDimensionsCount(): number { return ( @@ -616,8 +775,8 @@ class Metrics extends Utility implements MetricsInterface { * the same name. If the units are inconsistent, we throw an error as this is likely a bug or typo. * This can happen if a metric is added without using the `MetricUnit` helper in JavaScript codebases. * - * @param name The name of the metric - * @param unit The unit of the metric + * @param name - The name of the metric + * @param unit - The unit of the metric */ private isNewMetric(name: string, unit: MetricUnit): boolean { if (this.storedMetrics[name]) { @@ -634,11 +793,10 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Initialize the console property as an instance of the internal version of Console() class (PR #748) + * Initialize the console property as an instance of the internal version of `Console()` class (PR #748) * or as the global node console if the `POWERTOOLS_DEV' env variable is set and has truthy value. * * @private - * @returns {void} */ private setConsole(): void { if (!this.getEnvVarsService().isDevMode()) { @@ -674,7 +832,7 @@ class Metrics extends Utility implements MetricsInterface { /** * Set the namespace to be used. * - * @param namespace The namespace to be used + * @param namespace - The namespace to be used */ private setNamespace(namespace: string | undefined): void { this.namespace = (namespace || @@ -687,7 +845,7 @@ class Metrics extends Utility implements MetricsInterface { * * This method is used during the initialization of the Metrics instance. * - * @param options The options to be used + * @param options - The options to be used */ private setOptions(options: MetricsOptions): Metrics { const { @@ -712,7 +870,7 @@ class Metrics extends Utility implements MetricsInterface { /** * Set the service to be used. * - * @param service The service to be used + * @param service - The service to be used */ private setService(service: string | undefined): void { const targetService = @@ -729,12 +887,12 @@ class Metrics extends Utility implements MetricsInterface { * Store a metric in the buffer. * * If the buffer is full, or the metric reaches the maximum number of values, - * the buffer is published to stdout. + * the metrics are published. * - * @param name The name of the metric to store - * @param unit The unit of the metric to store - * @param value The value of the metric to store - * @param resolution The resolution of the metric to store + * @param name - The name of the metric to store + * @param unit - The unit of the metric to store + * @param value - The value of the metric to store + * @param resolution - The resolution of the metric to store */ private storeMetric( name: string, diff --git a/packages/metrics/src/config/EnvironmentVariablesService.ts b/packages/metrics/src/config/EnvironmentVariablesService.ts index da6670dcce..5f277e3010 100644 --- a/packages/metrics/src/config/EnvironmentVariablesService.ts +++ b/packages/metrics/src/config/EnvironmentVariablesService.ts @@ -1,6 +1,12 @@ import { EnvironmentVariablesService as CommonEnvironmentVariablesService } from '@aws-lambda-powertools/commons'; import type { ConfigServiceInterface } from '../types/ConfigServiceInterface.js'; +/** + * Class EnvironmentVariablesService + * + * This class is used to return environment variables that are available in the runtime of + * the current Lambda invocation. + */ class EnvironmentVariablesService extends CommonEnvironmentVariablesService implements ConfigServiceInterface @@ -8,7 +14,7 @@ class EnvironmentVariablesService private namespaceVariable = 'POWERTOOLS_METRICS_NAMESPACE'; /** - * It returns the value of the POWERTOOLS_METRICS_NAMESPACE environment variable. + * Get the value of the `POWERTOOLS_METRICS_NAMESPACE` environment variable. */ public getNamespace(): string { return this.get(this.namespaceVariable); diff --git a/packages/metrics/src/constants.ts b/packages/metrics/src/constants.ts index a3c2ee9e4c..18edeb0516 100644 --- a/packages/metrics/src/constants.ts +++ b/packages/metrics/src/constants.ts @@ -1,9 +1,30 @@ +/** + * The dimension key for the cold start metric. + */ const COLD_START_METRIC = 'ColdStart'; +/** + * The default namespace for metrics. + */ const DEFAULT_NAMESPACE = 'default_namespace'; +/** + * The maximum number of metrics that can be emitted in a single EMF blob. + */ const MAX_METRICS_SIZE = 100; +/** + * The maximum number of metric values that can be emitted in a single metric. + */ const MAX_METRIC_VALUES_SIZE = 100; +/** + * The maximum number of dimensions that can be added to a metric (0-indexed). + */ const MAX_DIMENSION_COUNT = 29; +/** + * The unit of the metric. + * + * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Unit | Amazon CloudWatch Units} + * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html | Amazon CloudWatch MetricDatum} + */ const MetricUnit = { Seconds: 'Seconds', Microseconds: 'Microseconds', @@ -34,6 +55,11 @@ const MetricUnit = { NoUnit: 'None', } as const; +/** + * The resolution of the metric. + * + * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition | Amazon CloudWatch Resolution} + */ const MetricResolution = { Standard: 60, High: 1, diff --git a/packages/metrics/src/middleware/middy.ts b/packages/metrics/src/middleware/middy.ts index 8274383829..e02a39d79a 100644 --- a/packages/metrics/src/middleware/middy.ts +++ b/packages/metrics/src/middleware/middy.ts @@ -7,31 +7,40 @@ import type { Metrics } from '../Metrics.js'; import type { ExtraOptions } from '../types/Metrics.js'; /** - * A middy middleware automating capture of metadata and annotations on segments or subsegments for a Lambda Handler. + * A Middy.js middleware automating capture of Amazon CloudWatch metrics. * - * Using this middleware on your handler function will automatically flush metrics after the function returns or throws an error. - * Additionally, you can configure the middleware to easily: - * * ensure that at least one metric is emitted before you flush them - * * capture a `ColdStart` a metric - * * set default dimensions for all your metrics + * This middleware is compatible with `@middy/core@3.x` and above. + * + * The middleware automatically flushes metrics after the handler function returns or throws an error, + * so you don't need to call {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} manually. * * @example * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; + * import { Metrics, MetricUnit } 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: any, _context: any) => { - * ... - * }; + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); * - * export const handler = middy(lambdaHandler).use(logMetrics(metrics)); + * export const handler = middy(async () => { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * }).use(logMetrics(metrics, { + * captureColdStartMetric: true, + * throwOnEmptyMetrics: true, + * })); * ``` * + * You can configure the middleware with the following options: + * - `captureColdStartMetric`: Whether to capture a `ColdStart` metric + * - `defaultDimensions`: Default dimensions to add to all metrics + * - `throwOnEmptyMetrics`: Whether to throw an error if no metrics are emitted + * * @param target - The Metrics instance to use for emitting metrics - * @param options - (_optional_) Options for the middleware + * @param options - Options to configure the middleware, see {@link ExtraOptions} */ const logMetrics = ( target: Metrics | Metrics[], @@ -57,7 +66,7 @@ const logMetrics = ( const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { - metrics.throwOnEmptyMetrics(); + metrics.setThrowOnEmptyMetrics(throwOnEmptyMetrics); } if (defaultDimensions !== undefined) { metrics.setDefaultDimensions(defaultDimensions); diff --git a/packages/metrics/src/types/ConfigServiceInterface.ts b/packages/metrics/src/types/ConfigServiceInterface.ts index 3f8557f399..81caa2d53b 100644 --- a/packages/metrics/src/types/ConfigServiceInterface.ts +++ b/packages/metrics/src/types/ConfigServiceInterface.ts @@ -9,9 +9,7 @@ import type { ConfigServiceInterface as ConfigServiceBaseInterface } from '@aws- */ interface ConfigServiceInterface extends ConfigServiceBaseInterface { /** - * It returns the value of the POWERTOOLS_METRICS_NAMESPACE environment variable. - * - * @returns {string} + * Get the value of the `POWERTOOLS_METRICS_NAMESPACE` environment variable. */ getNamespace(): string; } diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index 3b0a163134..1c65dc5212 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -1,19 +1,68 @@ +import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; import type { - MetricResolution as MetricResolutionList, - MetricUnit as MetricUnitList, + MetricResolution as MetricResolutions, + MetricUnit as MetricUnits, } from '../constants.js'; import type { ConfigServiceInterface } from './ConfigServiceInterface.js'; +/** + * A set of key-value pairs that define the dimensions of a metric. + */ type Dimensions = Record; +/** + * Options to configure the Metrics class. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * singleMetric: true, + * }); + * ``` + */ type MetricsOptions = { + /** + * A custom configuration service to use for retrieving configuration values. + * + * @default undefined + */ customConfigService?: ConfigServiceInterface; + /** + * The namespace to use for all metrics. + * + * @default undefined + */ namespace?: string; + /** + * The service name to use for all metrics. + * + * @default undefined + */ serviceName?: string; + /** + * Whether to configure the Metrics class to emit a single metric as soon as it is added. + * + * @default false + * @see {@link MetricsInterface.singleMetric | `singleMetric()`} + */ singleMetric?: boolean; + /** + * The default dimensions to add to all metrics. + * + * @default {} + * @see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} + */ defaultDimensions?: Dimensions; }; +/** + * The output of the {@link MetricsInterface.serializeMetrics | `serializeMetrics()`} method, + * compliant with the {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html | Amazon CloudWatch Embedded Metric Format (EMF)}. + */ type EmfOutput = Readonly<{ [key: string]: string | number | object; _aws: { @@ -27,35 +76,50 @@ type EmfOutput = Readonly<{ }>; /** - * Options for the metrics decorator - * - * Usage: - * - * ```typescript - * - * const metricsOptions: MetricsOptions = { - * throwOnEmptyMetrics: true, - * defaultDimensions: {'environment': 'dev'}, - * captureColdStartMetric: true, - * } - * - * @metrics.logMetric(metricsOptions) - * public handler(event: any, context: any, callback: any) { - * // ... - * } - * ``` + * Options to customize the behavior of the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator or Middy.js middleware. */ type ExtraOptions = { + /** + * Whether to throw an error if no metrics are emitted. + * + * @default false + * @see {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`} + */ throwOnEmptyMetrics?: boolean; + /** + * Default dimensions to add to all metrics. + * + * @default {} + * @see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} + */ defaultDimensions?: Dimensions; + /** + * Whether to capture a `ColdStart` metric. + * + * @default false + * @see {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} + */ captureColdStartMetric?: boolean; }; +/** + * A list of possible metric resolutions. + * + * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition | Amazon CloudWatch Resolution} + */ type MetricResolution = - (typeof MetricResolutionList)[keyof typeof MetricResolutionList]; + (typeof MetricResolutions)[keyof typeof MetricResolutions]; -type MetricUnit = (typeof MetricUnitList)[keyof typeof MetricUnitList]; +/** + * A list of possible metric units. + * + * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Unit | Amazon CloudWatch Units} + */ +type MetricUnit = (typeof MetricUnits)[keyof typeof MetricUnits]; +/** + * Data structure to store a metric that has been added to the Metrics class. + */ type StoredMetric = { name: string; unit: MetricUnit; @@ -63,14 +127,373 @@ type StoredMetric = { resolution: MetricResolution; }; +/** + * A map of stored metrics, where the key is the metric name and the value is the stored metric. + */ type StoredMetrics = Record; +/** + * A definition of a metric that can be added to the Metrics class. + */ type MetricDefinition = { Name: string; Unit: MetricUnit; StorageResolution?: MetricResolution; }; +interface MetricsInterface { + /** + * Add a dimension to metrics. + * + * A dimension is a key-value pair that is used to group metrics, and it is included in all metrics emitted after it is added. + * + * When calling the {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of + * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are + * included in all metrics, use the {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} method. + * + * @param name - The name of the dimension + * @param value - The value of the dimension + */ + addDimension(name: string, value: string): void; + /** + * Add multiple dimensions to the metrics. + * + * This method is useful when you want to add multiple dimensions to the metrics at once. + * + * When calling the {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of + * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are + * included in all metrics, use the {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} method. + * + * @param dimensions - An object with key-value pairs of dimensions + */ + addDimensions(dimensions: Dimensions): void; + /** + * A metadata key-value pair to be included with metrics. + * + * You can use this method to add high-cardinality data as part of your metrics. + * This is useful when you want to search highly contextual information along with your metrics in your logs. + * + * Note that the metadata is not included in the Amazon CloudWatch UI, but it can be used to search and filter logs. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async (event) => { + * metrics.addMetadata('request_id', event.requestId); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; + * ``` + * + * @param key - The key of the metadata + * @param value - The value of the metadata + */ + addMetadata(key: string, value: string): void; + /** + * Add a metric to the metrics buffer. + * + * By default, metrics are buffered and flushed when calling {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`} method, + * or at the end of the handler function when using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator or the Middy.js middleware. + * + * Metrics are emitted to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. In AWS Lambda, the logs are + * automatically picked up by CloudWatch logs and processed asynchronously. + * + * You can add a metric by specifying the metric name, unit, and value. For convenience, + * we provide a set of constants for the most common units in the {@link MetricUnits | MetricUnit} dictionary object. + * + * Optionally, you can specify a {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition | resolution}, which can be either `High` or `Standard`, using the {@link MetricResolutions | MetricResolution} dictionary object. + * By default, metrics are published with a resolution of `Standard`. + * + * @example + * ```typescript + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; + * ``` + * + * @param name - The metric name + * @param unit - The metric unit, see {@link MetricUnits | MetricUnit} + * @param value - The metric value + * @param resolution - The metric resolution, see {@link MetricResolutions | MetricResolution} + */ + addMetric( + name: string, + unit: MetricUnit, + value: number, + resolution?: MetricResolution + ): void; + /** + * Immediately emit a `ColdStart` metric if this is a cold start invocation. + * + * A cold start is when AWS Lambda initializes a new instance of your function. To take advantage of this feature, + * you must instantiate the Metrics class outside of the handler function. + * + * By using this method, the metric will be emitted immediately without you having to call {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`}. + * + * If you are using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, you can enable this + * feature by setting the `captureColdStartMetric` option to `true`. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * metrics.captureColdStartMetric(); + * }; + * ``` + */ + captureColdStartMetric(): void; + /** + * Clear all previously set default dimensions. + * + * This will remove all default dimensions set by the {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} method + * or via the `defaultDimensions` parameter in the constructor. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * defaultDimensions: { environment: 'dev' }, + * }); + * + * metrics.setDefaultDimensions({ region: 'us-west-2' }); + * + * // both environment and region dimensions are removed + * metrics.clearDefaultDimensions(); + * ``` + */ + clearDefaultDimensions(): void; + /** + * Clear all the dimensions added to the Metrics instance via {@link MetricsInterface.addDimension | `addDimension()`} or {@link MetricsInterface.addDimensions | `addDimensions()`}. + * + * These dimensions are normally cleared when calling {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`}, but + * you can use this method to clear specific dimensions that you no longer need at runtime. + * + * This method does not clear the default dimensions set via {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} or via + * the `defaultDimensions` parameter in the constructor. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * metrics.addDimension('region', 'us-west-2'); + * + * // ... + * + * metrics.clearDimensions(); // olnly the region dimension is removed + * }; + * ``` + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. + */ + clearDimensions(): void; + /** + * Clear all the metadata added to the Metrics instance. + * + * Metadata is normally cleared when calling {@link MetricsInterface.publishStoredMetrics | `publishStoredMetrics()`}, but + * you can use this method to clear specific metadata that you no longer need at runtime. + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. + */ + clearMetadata(): void; + /** + * Clear all the metrics stored in the buffer. + * + * This is useful when you want to clear the metrics stored in the buffer without publishing them. + * + * The method is primarily intended for internal use, but it is exposed for advanced use cases. + */ + clearMetrics(): void; + /** + * A class method decorator to automatically log metrics after the method returns or throws an error. + * + * The decorator can be used with TypeScript classes and can be configured to optionally capture a `ColdStart` metric (see {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`}), + * throw an error if no metrics are emitted (see {@link MetricsInterface.setThrowOnEmptyMetrics | `setThrowOnEmptyMetrics()`}), + * and set default dimensions for all metrics (see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`}). + * + * @example + * + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * class Lambda implements LambdaInterface { + * ⁣@metrics.logMetrics({ captureColdStartMetric: true }) + * public handler(_event: unknown, _context: unknown) { + * // ... + * } + * } + * + * const handlerClass = new Lambda(); + * export const handler = handlerClass.handler.bind(handlerClass); + * ``` + * + * You can configure the decorator with the following options: + * - `captureColdStartMetric`: Whether to capture a `ColdStart` metric + * - `defaultDimensions`: Default dimensions to add to all metrics + * - `throwOnEmptyMetrics`: Whether to throw an error if no metrics are emitted + * + * @param options - Options to configure the behavior of the decorator, see {@link ExtraOptions} + */ + logMetrics(options?: ExtraOptions): HandlerMethodDecorator; + /** + * Flush the stored metrics to standard output. + * + * The method empties the metrics buffer and emits the metrics to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. + * + * When using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the metrics are automatically flushed after the handler function returns or throws an error. + * + * @example + * ```typescript + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; + * ``` + */ + publishStoredMetrics(): void; + /** + * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema. + * + * The EMF schema is a JSON object that contains the following properties: + * - `_aws`: An object containing the timestamp and the CloudWatch metrics. + * - `CloudWatchMetrics`: An array of CloudWatch metrics objects. + * - `Namespace`: The namespace of the metrics. + * - `Dimensions`: An array of dimensions for the metrics. + * - `Metrics`: An array of metric definitions. + * + * The serialized object is returned for later use. + * + * This is primarily an internal method used by the Metrics class, but it is exposed for advanced use cases. + */ + serializeMetrics(): EmfOutput; + /** + * Set default dimensions that will be added to all metrics. + * + * This method will merge the provided dimensions with the existing default dimensions. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * defaultDimensions: { environment: 'dev' }, + * }); + * + * // Default dimensions will contain both region and environment + * metrics.setDefaultDimensions({ + * region: 'us-west-2', + * environment: 'prod', + * }); + * ``` + * + * @param dimensions - The dimensions to be added to the default dimensions object + */ + setDefaultDimensions(dimensions: Dimensions | undefined): void; + /** + * Set the function name to be added to each metric as a dimension. + * + * When using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function + * name is automatically inferred from the Lambda context. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * metrics.setFunctionName('my-function-name'); + * ``` + * + * @param name - The function name + */ + setFunctionName(name: string): void; + /** + * Set the flag to throw an error if no metrics are emitted. + * + * You can use this method to enable or disable this opt-in feature. This is useful if you want to ensure + * that at least one metric is emitted when flushing the metrics. This can be useful to catch bugs where + * metrics are not being emitted as expected. + * + * @param enabled - Whether to throw an error if no metrics are emitted + */ + setThrowOnEmptyMetrics(enabled: boolean): void; + /** + * Create a new Metrics instance configured to immediately flush a single metric. + * + * CloudWatch EMF uses the same dimensions and timestamp across all your metrics, this is useful when you have a metric that should have different dimensions + * or when you want to emit a single metric without buffering it. + * + * This method is used internally by the {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} method to emit the `ColdStart` metric immediately + * after the handler function is called. + * + * @example + * ```typescript + * import { Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders' + * }); + * + * export const handler = async () => { + * const singleMetric = metrics.singleMetric(); + * // The single metric will be emitted immediately + * singleMetric.addMetric('coldStart', MetricUnit.Count, 1); + * + * // These other metrics will be buffered and emitted when calling `publishStoredMetrics()` + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * }; + */ + singleMetric(): MetricsInterface; +} + export type { MetricsOptions, Dimensions, @@ -81,4 +504,5 @@ export type { MetricDefinition, MetricResolution, MetricUnit, + MetricsInterface, }; diff --git a/packages/metrics/src/types/MetricsInterface.ts b/packages/metrics/src/types/MetricsInterface.ts deleted file mode 100644 index ccc3e9f7c5..0000000000 --- a/packages/metrics/src/types/MetricsInterface.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; -import type { Metrics } from '../Metrics.js'; -import type { - Dimensions, - EmfOutput, - MetricResolution, - MetricUnit, - MetricsOptions, -} from './Metrics.js'; - -interface MetricsInterface { - addDimension(name: string, value: string): void; - addDimensions(dimensions: { [key: string]: string }): void; - addMetadata(key: string, value: string): void; - addMetric( - name: string, - unit: MetricUnit, - value: number, - resolution?: MetricResolution - ): void; - clearDimensions(): void; - clearMetadata(): void; - clearMetrics(): void; - clearDefaultDimensions(): void; - logMetrics(options?: MetricsOptions): HandlerMethodDecorator; - publishStoredMetrics(): void; - serializeMetrics(): EmfOutput; - setDefaultDimensions(dimensions: Dimensions | undefined): void; - singleMetric(): Metrics; -} - -export type { MetricsInterface }; diff --git a/packages/metrics/src/types/index.ts b/packages/metrics/src/types/index.ts index 7d30ec8888..6cb26881a1 100644 --- a/packages/metrics/src/types/index.ts +++ b/packages/metrics/src/types/index.ts @@ -8,6 +8,6 @@ export type { MetricDefinition, MetricResolution, MetricUnit, + MetricsInterface, } from './Metrics.js'; export type { ConfigServiceInterface } from './ConfigServiceInterface.js'; -export type { MetricsInterface } from './MetricsInterface.js'; From 5742243dd3a4fc7c62183a9bcd4eda4040d89915 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 2 Oct 2024 16:18:23 +0200 Subject: [PATCH 2/4] test: fix test usage of deprecated --- packages/metrics/tests/unit/Metrics.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index e1f69b3d8a..b68d2b7e38 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -1112,7 +1112,7 @@ describe('Class: Metrics', () => { let publishStoredMetricsSpy: jest.SpyInstance; let addMetricSpy: jest.SpyInstance; let captureColdStartMetricSpy: jest.SpyInstance; - let throwOnEmptyMetricsSpy: jest.SpyInstance; + let setThrowOnEmptyMetricsSpy: jest.SpyInstance; let setDefaultDimensionsSpy: jest.SpyInstance; const decoratorLambdaExpectedReturnValue = 'Lambda invoked!'; const decoratorLambdaMetric = 'decorator-lambda-test-metric'; @@ -1122,7 +1122,7 @@ describe('Class: Metrics', () => { publishStoredMetricsSpy = jest.spyOn(metrics, 'publishStoredMetrics'); addMetricSpy = jest.spyOn(metrics, 'addMetric'); captureColdStartMetricSpy = jest.spyOn(metrics, 'captureColdStartMetric'); - throwOnEmptyMetricsSpy = jest.spyOn(metrics, 'throwOnEmptyMetrics'); + setThrowOnEmptyMetricsSpy = jest.spyOn(metrics, 'setThrowOnEmptyMetrics'); setDefaultDimensionsSpy = jest.spyOn(metrics, 'setDefaultDimensions'); }); @@ -1145,7 +1145,7 @@ describe('Class: Metrics', () => { ); expect(publishStoredMetricsSpy).toBeCalledTimes(1); expect(captureColdStartMetricSpy).not.toBeCalled(); - expect(throwOnEmptyMetricsSpy).not.toBeCalled(); + expect(setThrowOnEmptyMetricsSpy).not.toBeCalled(); expect(setDefaultDimensionsSpy).not.toBeCalled(); }); @@ -1170,7 +1170,7 @@ describe('Class: Metrics', () => { ); expect(captureColdStartMetricSpy).toBeCalledTimes(1); expect(publishStoredMetricsSpy).toBeCalledTimes(1); - expect(throwOnEmptyMetricsSpy).not.toBeCalled(); + expect(setThrowOnEmptyMetricsSpy).not.toBeCalled(); expect(setDefaultDimensionsSpy).not.toBeCalled(); }); @@ -1193,7 +1193,7 @@ describe('Class: Metrics', () => { MetricUnit.Count, 1 ); - expect(throwOnEmptyMetricsSpy).toBeCalledTimes(1); + expect(setThrowOnEmptyMetricsSpy).toBeCalledTimes(1); expect(publishStoredMetricsSpy).toBeCalledTimes(1); expect(captureColdStartMetricSpy).not.toBeCalled(); expect(setDefaultDimensionsSpy).not.toBeCalled(); @@ -1227,7 +1227,7 @@ describe('Class: Metrics', () => { defaultDimensions ); expect(publishStoredMetricsSpy).toBeCalledTimes(1); - expect(throwOnEmptyMetricsSpy).not.toBeCalled(); + expect(setThrowOnEmptyMetricsSpy).not.toBeCalled(); expect(captureColdStartMetricSpy).not.toBeCalled(); }); From fd3a6f07a10f3a39b5aa33c8eecd77770de345db Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 2 Oct 2024 16:35:49 +0200 Subject: [PATCH 3/4] chore: fixed links --- packages/metrics/README.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/metrics/README.md b/packages/metrics/README.md index 8d327f48c1..abe9180f5a 100644 --- a/packages/metrics/README.md +++ b/packages/metrics/README.md @@ -28,7 +28,9 @@ To get started, install the library by running: npm i @aws-lambda-powertools/metrics ``` -After initializing the Metrics class, you can add metrics using the [Metrics.addMetric](`addMetric()`) method. The metrics are stored in a buffer and are flushed when calling [Metrics.publishStoredMetrics](`publishStoredMetrics()`). Each metric can have dimensions and metadata added to it. +After initializing the Metrics class, you can add metrics using the [https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/#creating-metrics](`addMetric()`) method. The metrics are stored in a buffer and are flushed when calling [https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/#flushing-metrics](`publishStoredMetrics()`). + +Each metric can also have dimensions and metadata added to it. ```ts import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; @@ -48,13 +50,30 @@ export const handler = async (event: { requestId: string }) => { ### Flushing metrics -As you finish adding all your metrics, you need to serialize and "flush them" by calling [publishStoredMetrics()](`publishStoredMetrics()`), which will emit the metrics to stdout in the Embedded Metric Format (EMF). The metrics are then picked up by the Lambda runtime and sent to CloudWatch. +As you finish adding all your metrics, you need to serialize and "flush them" by calling `publishStoredMetrics()`, which will emit the metrics to stdout in the Embedded Metric Format (EMF). The metrics are then picked up by the Lambda runtime and sent to CloudWatch. -When you +The `publishStoredMetrics()` method is synchronous and will block the event loop until the metrics are flushed. If you want Metrics to flush automatically at the end of your Lambda function, you can use the `@logMetrics()` decorator or the `logMetrics()` middleware. ### Capturing cold start as a metric -You can flush metrics automatically using one of the following methods: +With Metrics, you can capture cold start as a metric by calling the `captureColdStartMetric()` method. This method will add a metric with the name `ColdStart` and the value `1` to the metrics buffer. + +This metric is flushed automatically as soon as the method is called, to ensure that the cold start is captured regardless of whether the metrics are flushed manually or automatically. + +```ts +import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', +}); + +export const handler = async (event: { requestId: string }) => { + metrics.captureColdStartMetric(); +}; +``` + +Note that we don't emit a `ColdStart` metric with value `0` when the function is warm, as this would result in a high volume of metrics being emitted to CloudWatch, so you'll need to rely on the absence of the `ColdStart` metric to determine if the function is warm. ### Class method decorator From 328d1bed3ef3572f1da6113c1892d1b994bc1c11 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 3 Oct 2024 13:40:05 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Leandro Damascena --- packages/metrics/src/Metrics.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 127f282200..d34419dc28 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -279,8 +279,7 @@ class Metrics extends Utility implements MetricsInterface { * By default, metrics are buffered and flushed when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, * or at the end of the handler function when using the {@link Metrics.logMetrics | `logMetrics()`} decorator or the Middy.js middleware. * - * Metrics are emitted to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. In AWS Lambda, the logs are - * automatically picked up by CloudWatch logs and processed asynchronously. + * Metrics are emitted to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. * * You can add a metric by specifying the metric name, unit, and value. For convenience, * we provide a set of constants for the most common units in the {@link MetricUnits | MetricUnit} dictionary object. @@ -887,7 +886,7 @@ class Metrics extends Utility implements MetricsInterface { * Store a metric in the buffer. * * If the buffer is full, or the metric reaches the maximum number of values, - * the metrics are published. + * the metrics are flushed to stdout. * * @param name - The name of the metric to store * @param unit - The unit of the metric to store