From c6f5f5ed50981f3c18ef2c990b6b8be5025c84b3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 14 Aug 2022 10:22:55 +0200 Subject: [PATCH 1/7] fix: changed decorator implementation to regular function --- packages/metrics/src/Metrics.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index aba0e0d865..78a177808e 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -1,4 +1,4 @@ -import { Callback, Context } from 'aws-lambda'; +import { Callback, Context, Handler } from 'aws-lambda'; import { Utility } from '@aws-lambda-powertools/commons'; import { MetricsInterface } from '.'; import { ConfigServiceInterface, EnvironmentVariablesService } from './config'; @@ -236,24 +236,28 @@ class Metrics extends Utility implements MetricsInterface { this.setDefaultDimensions(defaultDimensions); } - return (target, _propertyKey, descriptor) => { + return (_target, _propertyKey, descriptor) => { /** * The descriptor.value is the method this decorator decorates, it cannot be undefined. */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const originalMethod = descriptor.value!; - descriptor.value = ( async (event: unknown, context: Context, callback: Callback): Promise => { - this.functionName = context.functionName; - if (captureColdStartMetric) this.captureColdStartMetric(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const metricsRef = this; + // Use a function() {} instead of an () => {} arrow function so that we can + // access `myClass` as `this` in a decorated `myClass.myMethod()`. + descriptor.value = ( async function(this: Handler, event: unknown, context: Context, callback: Callback): Promise { + metricsRef.functionName = context.functionName; + if (captureColdStartMetric) metricsRef.captureColdStartMetric(); let result: unknown; try { - result = await originalMethod.apply(target, [ event, context, callback ]); + result = await originalMethod.apply(this, [ event, context, callback ]); } catch (error) { throw error; } finally { - this.publishStoredMetrics(); + metricsRef.publishStoredMetrics(); } return result; From 62cd39aae8fd191ad83268fe39837ff3047eef0e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 14 Aug 2022 10:23:17 +0200 Subject: [PATCH 2/7] docs: update jsdocs --- packages/metrics/src/Metrics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 78a177808e..d97918e0c3 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -59,7 +59,7 @@ const DEFAULT_NAMESPACE = 'default_namespace'; * } * * export const handlerClass = new MyFunctionWithDecorator(); - * export const handler = handlerClass.handler; + * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * ### Standard function @@ -222,7 +222,7 @@ class Metrics extends Utility implements MetricsInterface { * } * * export const handlerClass = new MyFunctionWithDecorator(); - * export const handler = handlerClass.handler; + * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * @decorator Class From c0b95fe497913cacbee74e2bfd393f14faa70bff Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 14 Aug 2022 10:27:13 +0200 Subject: [PATCH 3/7] chore: added unit test --- packages/metrics/tests/unit/Metrics.test.ts | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 1a1d0712c3..9f0a45b5b2 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -619,6 +619,33 @@ describe('Class: Metrics', () => { expect(console.log).toBeCalledTimes(1); }); + + test('Using decorator should preserve `this` in decorated class', async () => { + // Prepare + const metrics = new Metrics({ namespace: 'test' }); + + // Act + class LambdaFunction implements LambdaInterface { + @metrics.logMetrics() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + public handler( + _event: TEvent, + _context: Context, + _callback: Callback, + ): void | Promise { + this.dummyMethod(); + } + + private dummyMethod(): void { + metrics.addMetric('test_name', MetricUnits.Seconds, 1); + } + } + await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!')); + + // Assess + expect(console.log).toBeCalledTimes(1); + }); }); describe('Feature: Custom Config Service', () => { From 56dd30989528a7af133ab816618ab229177fd713 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 14 Aug 2022 10:40:10 +0200 Subject: [PATCH 4/7] chore: added e2e tests --- .../basicFeatures.decorator.test.functionCode.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts b/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts index 60c007c040..158e58668d 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts @@ -1,6 +1,6 @@ import { Metrics, MetricUnits } from '../../src'; import { Context } from 'aws-lambda'; -import { LambdaInterface } from '../../examples/utils/lambda/LambdaInterface'; +import { LambdaInterface } from '@aws-lambda-powertools/commons'; const namespace = process.env.EXPECTED_NAMESPACE ?? 'CdkExample'; const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler'; @@ -19,21 +19,28 @@ const metrics = new Metrics({ namespace: namespace, serviceName: serviceName }); class Lambda implements LambdaInterface { @metrics.logMetrics({ captureColdStartMetric: true, defaultDimensions: JSON.parse(defaultDimensions), throwOnEmptyMetrics: true }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore public async handler(_event: unknown, _context: Context): Promise { metrics.addMetric(metricName, metricUnit, parseInt(metricValue)); metrics.addDimension( Object.entries(JSON.parse(extraDimension))[0][0], Object.entries(JSON.parse(extraDimension))[0][1] as string, ); - + + this.dummyMethod(); + } + + private dummyMethod(): void { const metricWithItsOwnDimensions = metrics.singleMetric(); metricWithItsOwnDimensions.addDimension( Object.entries(JSON.parse(singleMetricDimension))[0][0], Object.entries(JSON.parse(singleMetricDimension))[0][1] as string, ); + metricWithItsOwnDimensions.addMetric(singleMetricName, singleMetricUnit, parseInt(singleMetricValue)); } } export const handlerClass = new Lambda(); -export const handler = handlerClass.handler; \ No newline at end of file +export const handler = handlerClass.handler.bind(handlerClass); \ No newline at end of file From 6eebd7abd8c485ea568a8014dd245312d45761aa Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 14 Aug 2022 10:40:49 +0200 Subject: [PATCH 5/7] docs: updated docs --- docs/core/metrics.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index b2ed48ca24..e5c3f9469e 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -269,15 +269,20 @@ You can add default dimensions to your metrics by passing them as parameters in const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); const DEFAULT_DIMENSIONS = { 'environment': 'prod', 'foo': 'bar' }; - export class MyFunction implements LambdaInterface { + export class Lambda implements LambdaInterface { // Decorate your handler class method @metrics.logMetrics({ defaultDimensions: DEFAULT_DIMENSIONS }) public async handler(_event: any, _context: any): Promise { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } } + + export const handlerClass = new Lambda(); + export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` + 1. Binding your handler method allows your handler to access `this` within the class methods. + If you'd like to remove them at some point, you can use the `clearDefaultDimensions` method. ### Flushing metrics @@ -362,15 +367,20 @@ The `logMetrics` decorator of the metrics utility can be used when your Lambda h const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); - export class MyFunction implements LambdaInterface { + class Lambda implements LambdaInterface { @metrics.logMetrics() public async handler(_event: any, _context: any): Promise { metrics.addMetric('successfulBooking', MetricUnits.Count, 1); } } + + export const handlerClass = new Lambda(); + export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` + 1. Binding your handler method allows your handler to access `this` within the class methods. + === "Example CloudWatch Logs excerpt" ```json hl_lines="2 7 10 15 22" @@ -629,6 +639,8 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr } } - export const myFunction = new Lambda(); - export const handler = myFunction.handler; + export const handlerClass = new Lambda(); + export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` + + 1. Binding your handler method allows your handler to access `this` within the class methods. \ No newline at end of file From 0879acf033977af912bd6e1ffeb33187b1af8e4c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 17 Aug 2022 14:59:57 +0200 Subject: [PATCH 6/7] chore: removed extra export in docs/examples --- docs/core/metrics.md | 6 +++--- packages/metrics/src/Metrics.ts | 4 ++-- .../tests/e2e/basicFeatures.decorator.test.functionCode.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index e5c3f9469e..c357a3630c 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -277,7 +277,7 @@ You can add default dimensions to your metrics by passing them as parameters in } } - export const handlerClass = new Lambda(); + const handlerClass = new Lambda(); export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` @@ -375,7 +375,7 @@ The `logMetrics` decorator of the metrics utility can be used when your Lambda h } } - export const handlerClass = new Lambda(); + const handlerClass = new Lambda(); export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` @@ -639,7 +639,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr } } - export const handlerClass = new Lambda(); + const handlerClass = new Lambda(); export const handler = handlerClass.handler.bind(handlerClass); // (1) ``` diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index d97918e0c3..39a0d09ecd 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -58,7 +58,7 @@ const DEFAULT_NAMESPACE = 'default_namespace'; * } * } * - * export const handlerClass = new MyFunctionWithDecorator(); + * const handlerClass = new MyFunctionWithDecorator(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * @@ -221,7 +221,7 @@ class Metrics extends Utility implements MetricsInterface { * } * } * - * export const handlerClass = new MyFunctionWithDecorator(); + * const handlerClass = new MyFunctionWithDecorator(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * diff --git a/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts b/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts index 158e58668d..066d8d4140 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts @@ -42,5 +42,5 @@ class Lambda implements LambdaInterface { } } -export const handlerClass = new Lambda(); +const handlerClass = new Lambda(); export const handler = handlerClass.handler.bind(handlerClass); \ No newline at end of file From 1b6715b8f0d4eb6be3f762cf81284933498be405 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 17 Aug 2022 16:35:00 +0200 Subject: [PATCH 7/7] chore: added more assertions to test case --- packages/metrics/tests/unit/Metrics.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 9f0a45b5b2..bb59bde57a 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -642,9 +642,13 @@ describe('Class: Metrics', () => { } } await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!')); + const loggedData = JSON.parse(consoleSpy.mock.calls[0][0]); // Assess expect(console.log).toBeCalledTimes(1); + expect(loggedData._aws.CloudWatchMetrics[0].Metrics.length).toEqual(1); + expect(loggedData._aws.CloudWatchMetrics[0].Metrics[0].Name).toEqual('test_name'); + expect(loggedData._aws.CloudWatchMetrics[0].Metrics[0].Unit).toEqual('Seconds'); }); });