diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 12f74854bd..8cd17c0a36 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -27,6 +27,9 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar * **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](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition).
@@ -117,7 +120,23 @@ You can create metrics using the `addMetric` method, and you can create dimensio 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. !!! warning "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 behaviour. + 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](https://aws.amazon.com/about-aws/whats-new/2023/02/amazon-cloudwatch-high-resolution-metric-extraction-structured-logs/) passing `resolution` as parameter to `addMetric`. + +!!! tip "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. + +=== "Metrics with high resolution" + + ```typescript hl_lines="6" + --8<-- "docs/snippets/metrics/addHighResolutionMetric.ts" + ``` + +!!! tip "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 diff --git a/docs/snippets/metrics/addHighResolutionMetric.ts b/docs/snippets/metrics/addHighResolutionMetric.ts new file mode 100644 index 0000000000..0e44bcbcbb --- /dev/null +++ b/docs/snippets/metrics/addHighResolutionMetric.ts @@ -0,0 +1,7 @@ +import { Metrics, MetricUnits, MetricResolution } 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', MetricUnits.Count, 1, MetricResolution.High); +}; diff --git a/docs/snippets/metrics/basicUsage.ts b/docs/snippets/metrics/basicUsage.ts index 15388d2c82..ccd8606a01 100644 --- a/docs/snippets/metrics/basicUsage.ts +++ b/docs/snippets/metrics/basicUsage.ts @@ -2,6 +2,6 @@ import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); -export const handler = async (_event, _context): Promise => { +export const handler = async (_event: unknown, _context: unknown): Promise => { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); }; \ No newline at end of file diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index dbde8e0438..12bfc85451 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -11,6 +11,9 @@ import { ExtraOptions, MetricUnit, MetricUnits, + MetricResolution, + MetricDefinition, + StoredMetric, } from './types'; const MAX_METRICS_SIZE = 100; @@ -165,12 +168,33 @@ class Metrics extends Utility implements MetricsInterface { /** * Add a metric to the metrics buffer. - * @param name - * @param unit - * @param value + * + * @example + * + * Add Metric using MetricUnit Enum supported by Cloudwatch + * + * ```ts + * metrics.addMetric('successfulBooking', MetricUnits.Count, 1); + * ``` + * + * @example + * + * Add Metric using MetricResolution type with resolutions High or Standard supported by cloudwatch + * + * ```ts + * metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High); + * ``` + * + * @param name - The metric name + * @param unit - The metric unit + * @param value - The metric value + * @param resolution - The metric resolution + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition Amazon Cloudwatch Concepts Documentation + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html#CloudWatch_Embedded_Metric_Format_Specification_structure_metricdefinition Metric Definition of Embedded Metric Format Specification */ - public addMetric(name: string, unit: MetricUnit, value: number): void { - this.storeMetric(name, unit, value); + + public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = MetricResolution.Standard): void { + this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } @@ -314,15 +338,29 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Function to create the right object compliant with Cloudwatch EMF (Event Metric Format). + * Function to create the right object compliant with Cloudwatch EMF (Embedded Metric Format). + * + * + * @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 - * @returns {string} */ public serializeMetrics(): EmfOutput { - const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({ - Name: metricDefinition.name, - Unit: metricDefinition.unit, - })); + // For high-resolution metrics, add StorageResolution property + // Example: [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 1 } ] + + // For standard resolution metrics, don't add StorageResolution property to avoid unnecessary ingestion of data into cloudwatch + // Example: [ { "Name": "metric_name", "Unit": "Count"} ] + const metricDefinitions: MetricDefinition[] = Object.values(this.storedMetrics).map((metricDefinition) => + this.isHigh(metricDefinition['resolution']) + ? ({ + Name: metricDefinition.name, + Unit: metricDefinition.unit, + StorageResolution: metricDefinition.resolution + }): ({ + Name: metricDefinition.name, + Unit: metricDefinition.unit, + })); + if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) { throw new RangeError('The number of metrics recorded must be higher than zero'); } @@ -429,6 +467,10 @@ class Metrics extends Utility implements MetricsInterface { return this.envVarsService; } + private isHigh(resolution: StoredMetric['resolution']): resolution is typeof MetricResolution['High'] { + return resolution === MetricResolution.High; + } + private isNewMetric(name: string, unit: MetricUnit): boolean { if (this.storedMetrics[name]){ // Inconsistent units indicates a bug or typos and we want to flag this to users early @@ -479,7 +521,12 @@ class Metrics extends Utility implements MetricsInterface { } } - private storeMetric(name: string, unit: MetricUnit, value: number): void { + private storeMetric( + name: string, + unit: MetricUnit, + value: number, + resolution: MetricResolution, + ): void { if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } @@ -488,8 +535,10 @@ class Metrics extends Utility implements MetricsInterface { this.storedMetrics[name] = { unit, value, - name, + name, + resolution }; + } else { const storedMetric = this.storedMetrics[name]; if (!Array.isArray(storedMetric.value)) { @@ -501,4 +550,8 @@ class Metrics extends Utility implements MetricsInterface { } -export { Metrics, MetricUnits }; +export { + Metrics, + MetricUnits, + MetricResolution, +}; \ No newline at end of file diff --git a/packages/metrics/src/MetricsInterface.ts b/packages/metrics/src/MetricsInterface.ts index cda2fd577e..bb8dea45b7 100644 --- a/packages/metrics/src/MetricsInterface.ts +++ b/packages/metrics/src/MetricsInterface.ts @@ -1,11 +1,17 @@ import { Metrics } from './Metrics'; -import { MetricUnit, EmfOutput, HandlerMethodDecorator, Dimensions, MetricsOptions } from './types'; - +import { + MetricUnit, + MetricResolution, + EmfOutput, + HandlerMethodDecorator, + Dimensions, + MetricsOptions +} from './types'; 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): void + addMetric(name: string, unit:MetricUnit, value:number, resolution?: MetricResolution): void clearDimensions(): void clearMetadata(): void clearMetrics(): void diff --git a/packages/metrics/src/types/MetricResolution.ts b/packages/metrics/src/types/MetricResolution.ts new file mode 100644 index 0000000000..76065be623 --- /dev/null +++ b/packages/metrics/src/types/MetricResolution.ts @@ -0,0 +1,8 @@ +const MetricResolution = { + Standard: 60, + High: 1, +} as const; + +type MetricResolution = typeof MetricResolution[keyof typeof MetricResolution]; + +export { MetricResolution }; \ No newline at end of file diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index c25653ca44..8c0c12f541 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -2,6 +2,7 @@ import { Handler } from 'aws-lambda'; import { LambdaInterface, AsyncHandler, SyncHandler } from '@aws-lambda-powertools/commons'; import { ConfigServiceInterface } from '../config'; import { MetricUnit } from './MetricUnit'; +import { MetricResolution } from './MetricResolution'; type Dimensions = { [key: string]: string }; @@ -19,8 +20,8 @@ type EmfOutput = { Timestamp: number CloudWatchMetrics: { Namespace: string - Dimensions: [string[]] - Metrics: { Name: string; Unit: MetricUnit }[] + Dimensions: [string[]] + Metrics: MetricDefinition[] }[] } }; @@ -60,10 +61,17 @@ type StoredMetric = { name: string unit: MetricUnit value: number | number[] + resolution: MetricResolution }; type StoredMetrics = { [key: string]: StoredMetric }; -export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics }; +type MetricDefinition = { + Name: string + Unit: MetricUnit + StorageResolution?: MetricResolution +}; + +export { MetricsOptions, Dimensions, EmfOutput, HandlerMethodDecorator, ExtraOptions, StoredMetrics, StoredMetric, MetricDefinition }; diff --git a/packages/metrics/src/types/index.ts b/packages/metrics/src/types/index.ts index 44ff701f27..14416fbd33 100644 --- a/packages/metrics/src/types/index.ts +++ b/packages/metrics/src/types/index.ts @@ -1,2 +1,3 @@ export * from './Metrics'; -export * from './MetricUnit'; \ No newline at end of file +export * from './MetricUnit'; +export * from './MetricResolution'; \ No newline at end of file diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 5f2bd02bf8..a3960b9635 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -6,7 +6,8 @@ import { ContextExamples as dummyContext, Events as dummyEvent, LambdaInterface } from '@aws-lambda-powertools/commons'; import { Context, Callback } from 'aws-lambda'; -import { Metrics, MetricUnits } from '../../src/'; + +import { Metrics, MetricUnits, MetricResolution } from '../../src/'; const MAX_METRICS_SIZE = 100; const MAX_DIMENSION_COUNT = 29; @@ -563,6 +564,61 @@ describe('Class: Metrics', () => { }); }); + describe('Feature: Resolution of Metrics', ()=>{ + + test('serialized metrics in EMF format should not contain `StorageResolution` as key if none is set', () => { + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10); + const serializedMetrics = metrics.serializeMetrics(); + + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit'); + + }); + test('serialized metrics in EMF format should not contain `StorageResolution` as key if `Standard` is set', () => { + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.Standard); + const serializedMetrics = metrics.serializeMetrics(); + + // expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.Standard); + // expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(60); + + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit'); + }); + + test('serialized metrics in EMF format should not contain `StorageResolution` as key if `60` is set',()=>{ + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60); + const serializedMetrics = metrics.serializeMetrics(); + + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).not.toContain('StorageResolution'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Name'); + expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit'); + }); + + test('Should be StorageResolution `1` if MetricResolution is set to `High`',()=>{ + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.High); + const serializedMetrics = metrics.serializeMetrics(); + + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High); + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1); + }); + + test('Should be StorageResolution `1` if MetricResolution is set to `1`',()=>{ + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10, 1); + const serializedMetrics = metrics.serializeMetrics(); + + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.High); + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1); + + }); + }); + describe('Feature: Clearing Metrics ', () => { test('Clearing metrics should return empty', async () => { const metrics = new Metrics({ namespace: 'test' }); diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts index ad5a502120..1ee25e79d6 100644 --- a/packages/metrics/tests/unit/middleware/middy.test.ts +++ b/packages/metrics/tests/unit/middleware/middy.test.ts @@ -4,8 +4,12 @@ * @group unit/metrics/middleware */ -import { Metrics, MetricUnits, logMetrics } from '../../../../metrics/src'; -import middy from '@middy/core'; +import { + Metrics, + MetricUnits, + logMetrics, + MetricResolution +} from '../../../../metrics/src';import middy from '@middy/core'; import { ExtraOptions } from '../../../src/types'; const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); @@ -315,4 +319,77 @@ describe('Middy middleware', () => { ); }); }); + describe('Metrics resolution', () => { + + test('serialized metrics in EMF format should not contain `StorageResolution` as key if `60` is set', async () => { + // Prepare + const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + + const lambdaHandler = (): void => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.Standard); + }; + + const handler = middy(lambdaHandler).use(logMetrics(metrics)); + + // Act + await handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toHaveBeenCalledWith( + JSON.stringify({ + _aws: { + Timestamp: 1466424490000, + CloudWatchMetrics: [ + { + Namespace: 'serverlessAirline', + Dimensions: [['service']], + Metrics: [{ + Name: 'successfulBooking', + Unit: 'Count', + }], + }, + ], + }, + service: 'orders', + successfulBooking: 1, + }) + ); + }); + + test('Should be StorageResolution `1` if MetricResolution is set to `High`', async () => { + // Prepare + const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); + + const lambdaHandler = (): void => { + metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High); + }; + + const handler = middy(lambdaHandler).use(logMetrics(metrics)); + + // Act + await handler(dummyEvent, dummyContext, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toHaveBeenCalledWith( + JSON.stringify({ + _aws: { + Timestamp: 1466424490000, + CloudWatchMetrics: [ + { + Namespace: 'serverlessAirline', + Dimensions: [['service']], + Metrics: [{ + Name: 'successfulBooking', + Unit: 'Count', + StorageResolution: 1 + }], + }, + ], + }, + service: 'orders', + successfulBooking: 1, + }) + ); + }); + }); });