From ad92af4f122f17fa17feb2efbfc5446155f40b3d Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Sat, 11 Mar 2023 15:02:19 +0200 Subject: [PATCH 1/8] feat(metrics): type concept support high resolution metrics (#1277) --- packages/metrics/src/Metrics.ts | 13 ++-- packages/metrics/src/MetricsInterface.ts | 4 +- .../metrics/src/types/MetricResolution.ts | 8 +++ packages/metrics/src/types/Metrics.ts | 6 +- packages/metrics/src/types/index.ts | 3 +- packages/metrics/tests/unit/Metrics.test.ts | 60 ++++++++++++++++++- .../tests/unit/middleware/middy.test.ts | 12 ++-- 7 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 packages/metrics/src/types/MetricResolution.ts diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index dbde8e0438..a9ee392d62 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -11,11 +11,13 @@ import { ExtraOptions, MetricUnit, MetricUnits, + MetricResolution } from './types'; const MAX_METRICS_SIZE = 100; const MAX_DIMENSION_COUNT = 29; const DEFAULT_NAMESPACE = 'default_namespace'; +const DEFAULT_METRIC_RESOLUTION = MetricResolution.Standard; /** * ## Intro @@ -169,8 +171,9 @@ class Metrics extends Utility implements MetricsInterface { * @param unit * @param value */ - public addMetric(name: string, unit: MetricUnit, value: number): void { - this.storeMetric(name, unit, value); + + public addMetric(name: string, unit: MetricUnit, value: number, resolution?: MetricResolution): void { + this.storeMetric(name, unit, value, resolution ?? DEFAULT_METRIC_RESOLUTION ); if (this.isSingleMetric) this.publishStoredMetrics(); } @@ -322,6 +325,7 @@ class Metrics extends Utility implements MetricsInterface { const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({ Name: metricDefinition.name, Unit: metricDefinition.unit, + StorageResolution: metricDefinition.resolution })); if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) { throw new RangeError('The number of metrics recorded must be higher than zero'); @@ -479,7 +483,7 @@ 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(); } @@ -489,6 +493,7 @@ class Metrics extends Utility implements MetricsInterface { unit, value, name, + resolution }; } else { const storedMetric = this.storedMetrics[name]; @@ -501,4 +506,4 @@ class Metrics extends Utility implements MetricsInterface { } -export { Metrics, MetricUnits }; +export { Metrics, MetricUnits, MetricResolution }; diff --git a/packages/metrics/src/MetricsInterface.ts b/packages/metrics/src/MetricsInterface.ts index cda2fd577e..f7ad653b43 100644 --- a/packages/metrics/src/MetricsInterface.ts +++ b/packages/metrics/src/MetricsInterface.ts @@ -1,11 +1,11 @@ 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..1d1874bdfe 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: { Name: string; Unit: MetricUnit; StorageResolution: MetricResolution }[] }[] } }; @@ -60,6 +61,7 @@ type StoredMetric = { name: string unit: MetricUnit value: number | number[] + resolution: MetricResolution }; type StoredMetrics = { 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..b55e32ef74 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -6,11 +6,13 @@ 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; const DEFAULT_NAMESPACE = 'default_namespace'; +const DEFAULT_METRIC_RESOLUTION = MetricResolution.Standard; const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); @@ -554,8 +556,8 @@ describe('Class: Metrics', () => { const serializedMetrics = metrics.serializeMetrics(); expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics).toStrictEqual([ - { Name: 'test_name', Unit: 'Count' }, - { Name: 'test_name2', Unit: 'Count' }, + { Name: 'test_name', Unit: 'Count', StorageResolution: DEFAULT_METRIC_RESOLUTION }, + { Name: 'test_name2', Unit: 'Count', StorageResolution: DEFAULT_METRIC_RESOLUTION }, ]); expect(serializedMetrics['test_name']).toBe(1); @@ -563,6 +565,18 @@ describe('Class: Metrics', () => { }); }); + describe('Feature: Resolution of Metrics', ()=>{ + test('Should use default metric resolution (STANDARD) if none is set',()=>{ + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10); + const serializedMetrics = metrics.serializeMetrics(); + + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(DEFAULT_METRIC_RESOLUTION); + + }); + + }); + describe('Feature: Clearing Metrics ', () => { test('Clearing metrics should return empty', async () => { const metrics = new Metrics({ namespace: 'test' }); @@ -747,4 +761,44 @@ describe('Class: Metrics', () => { expect(loggedData._aws.CloudWatchMetrics[0].Namespace).toEqual(namespace); }); }); + + describe('concept', ()=>{ + test('metric DX type with const', ()=>{ + const MetricResolutionConcept = { + Standard: 60, + High: 1 + } as const; + type MetricResolutionConcept = typeof MetricResolutionConcept[keyof typeof MetricResolutionConcept]; + + const use = (resolution: MetricResolutionConcept):void => { + if (resolution === MetricResolutionConcept.Standard) expect(resolution).toBe(MetricResolution.Standard); + if (resolution === MetricResolution.High) expect(resolution).toBe(MetricResolutionConcept.High); + }; + + // prefered design of Metric Resolution, strcutural typing, compile time guidance + use(MetricResolution.Standard); + use(60); + use(1); + // use(10); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345) + //use(80); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345) + }); + + test('metric DX type with enum', ()=>{ + enum MetricResolutionEnum { + Standard = 60, + High = 1 + } + + const use = (resolution: MetricResolutionEnum):void => { + if (resolution === MetricResolutionEnum.Standard) expect(resolution).toBe(MetricResolution.Standard); + if (resolution === MetricResolutionEnum.High) expect(resolution).toBe(MetricResolutionEnum.High); + }; + use(MetricResolution.Standard); + + // enum design, allows the following usage at compile time + use(10); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum' + use(80); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum' + }); + + }); }); diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts index ad5a502120..d0f7177b4d 100644 --- a/packages/metrics/tests/unit/middleware/middy.test.ts +++ b/packages/metrics/tests/unit/middleware/middy.test.ts @@ -4,7 +4,7 @@ * @group unit/metrics/middleware */ -import { Metrics, MetricUnits, logMetrics } from '../../../../metrics/src'; +import { Metrics, MetricUnits, logMetrics, MetricResolution } from '../../../../metrics/src'; import middy from '@middy/core'; import { ExtraOptions } from '../../../src/types'; @@ -178,7 +178,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], }, ], }, @@ -215,7 +215,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [[ 'service', 'environment', 'aws_region', 'function_name' ]], - Metrics: [{ Name: 'ColdStart', Unit: 'Count' }], + Metrics: [{ Name: 'ColdStart', Unit: 'Count', StorageResolution: MetricResolution.Standard }], }, ], }, @@ -235,7 +235,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [[ 'service', 'environment', 'aws_region' ]], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], }, ], }, @@ -270,7 +270,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], }, ], }, @@ -305,7 +305,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], }, ], }, From b3690c6ad8a862c22555dd97eddfee189127d087 Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Sat, 11 Mar 2023 17:06:36 +0200 Subject: [PATCH 2/8] feat(metrics): refactored addMetric function and new metric resolution tests (#1277) --- packages/metrics/src/Metrics.ts | 4 ++-- packages/metrics/tests/unit/Metrics.test.ts | 24 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index a9ee392d62..0b3c3333a4 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -172,8 +172,8 @@ class Metrics extends Utility implements MetricsInterface { * @param value */ - public addMetric(name: string, unit: MetricUnit, value: number, resolution?: MetricResolution): void { - this.storeMetric(name, unit, value, resolution ?? DEFAULT_METRIC_RESOLUTION ); + public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = DEFAULT_METRIC_RESOLUTION): void { + this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index b55e32ef74..a4e7876d53 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -566,12 +566,34 @@ describe('Class: Metrics', () => { }); describe('Feature: Resolution of Metrics', ()=>{ - test('Should use default metric resolution (STANDARD) if none is set',()=>{ + test('Should use default metric resolution `Standard, 60` if none is set',()=>{ const metrics = new Metrics(); metrics.addMetric('test_name', MetricUnits.Seconds, 10); const serializedMetrics = metrics.serializeMetrics(); expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(DEFAULT_METRIC_RESOLUTION); + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(MetricResolution.Standard); + expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(60); + + }); + + test('Should use metric resolution `Standard, 60` 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); + + }); + + test('Should use metric resolution `High, 1` if `High` is set',()=>{ + 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); }); From a345831c6b7dbf37e907caff9490839c84e938c1 Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Sat, 11 Mar 2023 17:13:33 +0200 Subject: [PATCH 3/8] feat(metrics): raw values as metric resolution tests (#1277) --- packages/metrics/tests/unit/Metrics.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index a4e7876d53..8b3a4cffbf 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -587,6 +587,26 @@ describe('Class: Metrics', () => { }); + test('Should use metric resolution `Standard, 60` if `60` is set',()=>{ + const metrics = new Metrics(); + metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60); + 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); + + }); + + test('Should use metric resolution `High, 1` if `1` is set',()=>{ + 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); + + }); + test('Should use metric resolution `High, 1` if `High` is set',()=>{ const metrics = new Metrics(); metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.High); From dad0f2002842defdb6a679e528aa03c371fa7ca9 Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Sun, 12 Mar 2023 02:20:50 +0200 Subject: [PATCH 4/8] feat(metrics): use case Storage Resolution key when not set (#1277) --- packages/metrics/src/Metrics.ts | 13 +++- packages/metrics/src/types/Metrics.ts | 4 +- packages/metrics/tests/unit/Metrics.test.ts | 29 +++----- .../tests/unit/middleware/middy.test.ts | 74 +++++++++++++++++-- 4 files changed, 92 insertions(+), 28 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 0b3c3333a4..21e8eb17a7 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -17,7 +17,6 @@ import { const MAX_METRICS_SIZE = 100; const MAX_DIMENSION_COUNT = 29; const DEFAULT_NAMESPACE = 'default_namespace'; -const DEFAULT_METRIC_RESOLUTION = MetricResolution.Standard; /** * ## Intro @@ -170,9 +169,10 @@ class Metrics extends Utility implements MetricsInterface { * @param name * @param unit * @param value + * @param resolution */ - public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = DEFAULT_METRIC_RESOLUTION): void { + public addMetric(name: string, unit: MetricUnit, value: number, resolution?: MetricResolution): void { this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } @@ -322,11 +322,15 @@ class Metrics extends Utility implements MetricsInterface { * @returns {string} */ public serializeMetrics(): EmfOutput { - const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => ({ + const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => 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'); } @@ -483,7 +487,7 @@ class Metrics extends Utility implements MetricsInterface { } } - private storeMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution): void { + private storeMetric(name: string, unit: MetricUnit, value: number, resolution?: MetricResolution): void { if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } @@ -495,6 +499,7 @@ class Metrics extends Utility implements MetricsInterface { name, resolution }; + } else { const storedMetric = this.storedMetrics[name]; if (!Array.isArray(storedMetric.value)) { diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index 1d1874bdfe..a4133c2faf 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -21,7 +21,7 @@ type EmfOutput = { CloudWatchMetrics: { Namespace: string Dimensions: [string[]] - Metrics: { Name: string; Unit: MetricUnit; StorageResolution: MetricResolution }[] + Metrics: { Name: string; Unit: MetricUnit; StorageResolution?: MetricResolution }[] }[] } }; @@ -61,7 +61,7 @@ type StoredMetric = { name: string unit: MetricUnit value: number | number[] - resolution: MetricResolution + resolution?: MetricResolution }; type StoredMetrics = { diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 8b3a4cffbf..8c8b5078fa 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -12,7 +12,6 @@ import { Metrics, MetricUnits, MetricResolution } from '../../src/'; const MAX_METRICS_SIZE = 100; const MAX_DIMENSION_COUNT = 29; const DEFAULT_NAMESPACE = 'default_namespace'; -const DEFAULT_METRIC_RESOLUTION = MetricResolution.Standard; const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); @@ -556,8 +555,8 @@ describe('Class: Metrics', () => { const serializedMetrics = metrics.serializeMetrics(); expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics).toStrictEqual([ - { Name: 'test_name', Unit: 'Count', StorageResolution: DEFAULT_METRIC_RESOLUTION }, - { Name: 'test_name2', Unit: 'Count', StorageResolution: DEFAULT_METRIC_RESOLUTION }, + { Name: 'test_name', Unit: 'Count' }, + { Name: 'test_name2', Unit: 'Count' }, ]); expect(serializedMetrics['test_name']).toBe(1); @@ -566,57 +565,53 @@ describe('Class: Metrics', () => { }); describe('Feature: Resolution of Metrics', ()=>{ - test('Should use default metric resolution `Standard, 60` if none is set',()=>{ + test('Should serialized metrics in EMF format 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(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(DEFAULT_METRIC_RESOLUTION); - 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('Should use metric resolution `Standard, 60` if `Standard` is set',()=>{ + test('Should be StorageResolution 60 if MetricResolution is set to `Standard`',()=>{ 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); - }); - test('Should use metric resolution `Standard, 60` if `60` is set',()=>{ + test('Should be StorageResolution 60 if MetricResolution is set to `60`',()=>{ const metrics = new Metrics(); metrics.addMetric('test_name', MetricUnits.Seconds, 10, 60); 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); - }); - test('Should use metric resolution `High, 1` if `1` is set',()=>{ + test('Should be StorageResolution 1 if MetricResolution is set to `High`',()=>{ const metrics = new Metrics(); - metrics.addMetric('test_name', MetricUnits.Seconds, 10, 1); + 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 use metric resolution `High, 1` if `High` is set',()=>{ + test('Should be StorageResolution 1 if MetricResolution is set to `1`',()=>{ const metrics = new Metrics(); - metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.High); + 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 ', () => { diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts index d0f7177b4d..8db2924275 100644 --- a/packages/metrics/tests/unit/middleware/middy.test.ts +++ b/packages/metrics/tests/unit/middleware/middy.test.ts @@ -178,7 +178,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], }, ], }, @@ -215,7 +215,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [[ 'service', 'environment', 'aws_region', 'function_name' ]], - Metrics: [{ Name: 'ColdStart', Unit: 'Count', StorageResolution: MetricResolution.Standard }], + Metrics: [{ Name: 'ColdStart', Unit: 'Count' }], }, ], }, @@ -235,7 +235,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [[ 'service', 'environment', 'aws_region' ]], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], }, ], }, @@ -270,7 +270,7 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], }, ], }, @@ -305,7 +305,71 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: MetricResolution.Standard }], + Metrics: [{ Name: 'successfulBooking', Unit: 'Count' }], + }, + ], + }, + service: 'orders', + successfulBooking: 1, + }) + ); + }); + }); + describe('Feature: Resolution of Metrics', ()=>{ + test('Should use metric resolution `Standard, 60` if `Standard` 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', StorageResolution: 60 }], + }, + ], + }, + service: 'orders', + successfulBooking: 1, + }) + ); + }); + test('Should use metric resolution `High, 1` if `High` is set', 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 }], }, ], }, From 7a25f2f768ea814bb449920832557204339730ba Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Tue, 14 Mar 2023 01:28:01 +0200 Subject: [PATCH 5/8] feat(metrics): apply styling --- packages/metrics/src/Metrics.ts | 17 ++++++++--- packages/metrics/src/MetricsInterface.ts | 10 +++++-- packages/metrics/src/types/Metrics.ts | 6 +++- packages/metrics/tests/unit/Metrics.test.ts | 6 ++-- .../tests/unit/middleware/middy.test.ts | 28 ++++++++++++++----- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 21e8eb17a7..075f77d5b4 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -11,7 +11,7 @@ import { ExtraOptions, MetricUnit, MetricUnits, - MetricResolution + MetricResolution, } from './types'; const MAX_METRICS_SIZE = 100; @@ -487,7 +487,12 @@ class Metrics extends Utility implements MetricsInterface { } } - private storeMetric(name: string, unit: MetricUnit, value: number, resolution?: MetricResolution): void { + private storeMetric( + name: string, + unit: MetricUnit, + value: number, + resolution?: MetricResolution, + ): void { if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } @@ -497,7 +502,7 @@ class Metrics extends Utility implements MetricsInterface { unit, value, name, - resolution + resolution, }; } else { @@ -511,4 +516,8 @@ class Metrics extends Utility implements MetricsInterface { } -export { Metrics, MetricUnits, MetricResolution }; +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 f7ad653b43..bb8dea45b7 100644 --- a/packages/metrics/src/MetricsInterface.ts +++ b/packages/metrics/src/MetricsInterface.ts @@ -1,6 +1,12 @@ import { Metrics } from './Metrics'; -import { MetricUnit, MetricResolution, 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 diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index a4133c2faf..2c856c84ab 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -21,7 +21,11 @@ type EmfOutput = { CloudWatchMetrics: { Namespace: string Dimensions: [string[]] - Metrics: { Name: string; Unit: MetricUnit; StorageResolution?: MetricResolution }[] + Metrics: { + Name: string + Unit: MetricUnit + StorageResolution?: MetricResolution + }[] }[] } }; diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 8c8b5078fa..a826cef583 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -565,7 +565,8 @@ describe('Class: Metrics', () => { }); describe('Feature: Resolution of Metrics', ()=>{ - test('Should serialized metrics in EMF format not contain StorageResolution as key if none is set',()=>{ + + 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(); @@ -575,8 +576,7 @@ describe('Class: Metrics', () => { expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit'); }); - - test('Should be StorageResolution 60 if MetricResolution is set to `Standard`',()=>{ + test('should set StorageResolution 60 if MetricResolution is set to `Standard`', () => { const metrics = new Metrics(); metrics.addMetric('test_name', MetricUnits.Seconds, 10, MetricResolution.Standard); const serializedMetrics = metrics.serializeMetrics(); diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts index 8db2924275..464b4a660a 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, MetricResolution } 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,8 +319,9 @@ describe('Middy middleware', () => { ); }); }); - describe('Feature: Resolution of Metrics', ()=>{ - test('Should use metric resolution `Standard, 60` if `Standard` is set', async () => { + describe('Metrics resolution', () => { + + test('should use metric resolution `Standard, 60` if `Standard` is set', async () => { // Prepare const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); @@ -338,7 +343,11 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: 60 }], + Metrics: [{ + Name: 'successfulBooking', + Unit: 'Count', + StorageResolution: 60, + }], }, ], }, @@ -347,7 +356,8 @@ describe('Middy middleware', () => { }) ); }); - test('Should use metric resolution `High, 1` if `High` is set', async () => { + + test('should use metric resolution `High, 1` if `High` is set', async () => { // Prepare const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); @@ -369,7 +379,11 @@ describe('Middy middleware', () => { { Namespace: 'serverlessAirline', Dimensions: [['service']], - Metrics: [{ Name: 'successfulBooking', Unit: 'Count', StorageResolution: 1 }], + Metrics: [{ + Name: 'successfulBooking', + Unit: 'Count', + StorageResolution: 1 + }], }, ], }, From b80798ff0bd409267450b956174d5602232c2aae Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Tue, 14 Mar 2023 22:46:14 +0100 Subject: [PATCH 6/8] feat(metrics): remove learning concept as const --- packages/metrics/tests/unit/Metrics.test.ts | 40 --------------------- 1 file changed, 40 deletions(-) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index a826cef583..725bfb8967 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -798,44 +798,4 @@ describe('Class: Metrics', () => { expect(loggedData._aws.CloudWatchMetrics[0].Namespace).toEqual(namespace); }); }); - - describe('concept', ()=>{ - test('metric DX type with const', ()=>{ - const MetricResolutionConcept = { - Standard: 60, - High: 1 - } as const; - type MetricResolutionConcept = typeof MetricResolutionConcept[keyof typeof MetricResolutionConcept]; - - const use = (resolution: MetricResolutionConcept):void => { - if (resolution === MetricResolutionConcept.Standard) expect(resolution).toBe(MetricResolution.Standard); - if (resolution === MetricResolution.High) expect(resolution).toBe(MetricResolutionConcept.High); - }; - - // prefered design of Metric Resolution, strcutural typing, compile time guidance - use(MetricResolution.Standard); - use(60); - use(1); - // use(10); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345) - //use(80); // Argument of type '10' is not assignable to parameter of type 'MetricResolutionConcept'.ts(2345) - }); - - test('metric DX type with enum', ()=>{ - enum MetricResolutionEnum { - Standard = 60, - High = 1 - } - - const use = (resolution: MetricResolutionEnum):void => { - if (resolution === MetricResolutionEnum.Standard) expect(resolution).toBe(MetricResolution.Standard); - if (resolution === MetricResolutionEnum.High) expect(resolution).toBe(MetricResolutionEnum.High); - }; - use(MetricResolution.Standard); - - // enum design, allows the following usage at compile time - use(10); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum' - use(80); // Argument of type '10' is assignable to parameter of type 'MetricResolutionEnum' - }); - - }); }); From fc379f9d5df06fa2a7d6243d1042108f702db28a Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Thu, 16 Mar 2023 05:30:08 +0100 Subject: [PATCH 7/8] feat(metrics): refactoring cost improvement website docs and code comments --- docs/core/metrics.md | 21 +++++- .../metrics/addHighResolutionMetric.ts | 7 ++ docs/snippets/metrics/basicUsage.ts | 2 +- docs/snippets/metrics/tsconfig.json | 19 +++++ packages/metrics/src/Metrics.ts | 72 ++++++++++++++----- packages/metrics/src/types/Metrics.ts | 16 +++-- packages/metrics/tests/unit/Metrics.test.ts | 21 +++--- .../tests/unit/middleware/middy.test.ts | 5 +- 8 files changed, 124 insertions(+), 39 deletions(-) create mode 100644 docs/snippets/metrics/addHighResolutionMetric.ts create mode 100644 docs/snippets/metrics/tsconfig.json 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/docs/snippets/metrics/tsconfig.json b/docs/snippets/metrics/tsconfig.json new file mode 100644 index 0000000000..40951b4161 --- /dev/null +++ b/docs/snippets/metrics/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "noEmit": true, + "strict": true, + "noImplicitAny": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "skipLibCheck": true, + "pretty": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@aws-lambda-powertools/metrics": ["../../../packages/metrics/src"] + } + }, + "exclude": ["./node_modules"], + "lib": ["ES2020"], + "types": ["node"] +} diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 075f77d5b4..35b38f8611 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -12,6 +12,8 @@ import { MetricUnit, MetricUnits, MetricResolution, + MetricDefinition, + StoredMetric, } from './types'; const MAX_METRICS_SIZE = 100; @@ -166,13 +168,32 @@ class Metrics extends Utility implements MetricsInterface { /** * Add a metric to the metrics buffer. - * @param name - * @param unit - * @param value - * @param resolution + * + * @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, resolution?: MetricResolution): void { + public addMetric(name: string, unit: MetricUnit, value: number, resolution: MetricResolution = MetricResolution.Standard): void { this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } @@ -317,19 +338,28 @@ 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) => metricDefinition.resolution ? ({ - Name: metricDefinition.name, - Unit: metricDefinition.unit, - StorageResolution: metricDefinition.resolution - }): ({ - 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'); @@ -437,6 +467,10 @@ class Metrics extends Utility implements MetricsInterface { return this.envVarsService; } + private isHigh(resolution: StoredMetric['resolution']): resolution is 1 { + 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 @@ -491,7 +525,7 @@ class Metrics extends Utility implements MetricsInterface { name: string, unit: MetricUnit, value: number, - resolution?: MetricResolution, + resolution: MetricResolution, ): void { if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); @@ -501,10 +535,10 @@ class Metrics extends Utility implements MetricsInterface { this.storedMetrics[name] = { unit, value, - name, - resolution, + name, + resolution }; - + } else { const storedMetric = this.storedMetrics[name]; if (!Array.isArray(storedMetric.value)) { diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index 2c856c84ab..8c0c12f541 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -21,11 +21,7 @@ type EmfOutput = { CloudWatchMetrics: { Namespace: string Dimensions: [string[]] - Metrics: { - Name: string - Unit: MetricUnit - StorageResolution?: MetricResolution - }[] + Metrics: MetricDefinition[] }[] } }; @@ -65,11 +61,17 @@ type StoredMetric = { name: string unit: MetricUnit value: number | number[] - resolution?: MetricResolution + 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/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 725bfb8967..a3960b9635 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -576,25 +576,30 @@ describe('Class: Metrics', () => { expect(Object.keys(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0])).toContain('Unit'); }); - test('should set StorageResolution 60 if MetricResolution is set to `Standard`', () => { + 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(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('Should be StorageResolution 60 if MetricResolution is set to `60`',()=>{ + 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(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('Should be StorageResolution 1 if MetricResolution is set to `High`',()=>{ + 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(); @@ -603,7 +608,7 @@ describe('Class: Metrics', () => { expect(serializedMetrics._aws.CloudWatchMetrics[0].Metrics[0].StorageResolution).toBe(1); }); - test('Should be StorageResolution 1 if MetricResolution is set to `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(); diff --git a/packages/metrics/tests/unit/middleware/middy.test.ts b/packages/metrics/tests/unit/middleware/middy.test.ts index 464b4a660a..1ee25e79d6 100644 --- a/packages/metrics/tests/unit/middleware/middy.test.ts +++ b/packages/metrics/tests/unit/middleware/middy.test.ts @@ -321,7 +321,7 @@ describe('Middy middleware', () => { }); describe('Metrics resolution', () => { - test('should use metric resolution `Standard, 60` if `Standard` is set', async () => { + 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' }); @@ -346,7 +346,6 @@ describe('Middy middleware', () => { Metrics: [{ Name: 'successfulBooking', Unit: 'Count', - StorageResolution: 60, }], }, ], @@ -357,7 +356,7 @@ describe('Middy middleware', () => { ); }); - test('should use metric resolution `High, 1` if `High` is set', async () => { + test('Should be StorageResolution `1` if MetricResolution is set to `High`', async () => { // Prepare const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); From 4b14bcb4403335d0cd9d9f07991e8657b8e73c2e Mon Sep 17 00:00:00 2001 From: niko-achilles Date: Thu, 16 Mar 2023 17:12:56 +0100 Subject: [PATCH 8/8] feat(metrics): improve isHigh typeguard, remove tsconfig from metrics snippet --- docs/snippets/metrics/tsconfig.json | 19 ------------------- packages/metrics/src/Metrics.ts | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 docs/snippets/metrics/tsconfig.json diff --git a/docs/snippets/metrics/tsconfig.json b/docs/snippets/metrics/tsconfig.json deleted file mode 100644 index 40951b4161..0000000000 --- a/docs/snippets/metrics/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "noEmit": true, - "strict": true, - "noImplicitAny": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "skipLibCheck": true, - "pretty": true, - "resolveJsonModule": true, - "baseUrl": ".", - "paths": { - "@aws-lambda-powertools/metrics": ["../../../packages/metrics/src"] - } - }, - "exclude": ["./node_modules"], - "lib": ["ES2020"], - "types": ["node"] -} diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 35b38f8611..12bfc85451 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -467,7 +467,7 @@ class Metrics extends Utility implements MetricsInterface { return this.envVarsService; } - private isHigh(resolution: StoredMetric['resolution']): resolution is 1 { + private isHigh(resolution: StoredMetric['resolution']): resolution is typeof MetricResolution['High'] { return resolution === MetricResolution.High; }