Skip to content

Commit 8d0c2a1

Browse files
authored
Fix(metrics): decorated class methods cannot access this (aws-powertools#1059)
1 parent 73990bb commit 8d0c2a1

File tree

4 files changed

+73
-19
lines changed

4 files changed

+73
-19
lines changed

Diff for: docs/core/metrics.md

+16-4
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,20 @@ You can add default dimensions to your metrics by passing them as parameters in
269269
const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' });
270270
const DEFAULT_DIMENSIONS = { 'environment': 'prod', 'foo': 'bar' };
271271

272-
export class MyFunction implements LambdaInterface {
272+
export class Lambda implements LambdaInterface {
273273
// Decorate your handler class method
274274
@metrics.logMetrics({ defaultDimensions: DEFAULT_DIMENSIONS })
275275
public async handler(_event: any, _context: any): Promise<void> {
276276
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
277277
}
278278
}
279+
280+
const handlerClass = new Lambda();
281+
export const handler = handlerClass.handler.bind(handlerClass); // (1)
279282
```
280283

284+
1. Binding your handler method allows your handler to access `this` within the class methods.
285+
281286
If you'd like to remove them at some point, you can use the `clearDefaultDimensions` method.
282287

283288
### Flushing metrics
@@ -362,15 +367,20 @@ The `logMetrics` decorator of the metrics utility can be used when your Lambda h
362367

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

365-
export class MyFunction implements LambdaInterface {
370+
class Lambda implements LambdaInterface {
366371

367372
@metrics.logMetrics()
368373
public async handler(_event: any, _context: any): Promise<void> {
369374
metrics.addMetric('successfulBooking', MetricUnits.Count, 1);
370375
}
371376
}
377+
378+
const handlerClass = new Lambda();
379+
export const handler = handlerClass.handler.bind(handlerClass); // (1)
372380
```
373381

382+
1. Binding your handler method allows your handler to access `this` within the class methods.
383+
374384
=== "Example CloudWatch Logs excerpt"
375385

376386
```json hl_lines="2 7 10 15 22"
@@ -629,6 +639,8 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `singleMetr
629639
}
630640
}
631641

632-
export const myFunction = new Lambda();
633-
export const handler = myFunction.handler;
642+
const handlerClass = new Lambda();
643+
export const handler = handlerClass.handler.bind(handlerClass); // (1)
634644
```
645+
646+
1. Binding your handler method allows your handler to access `this` within the class methods.

Diff for: packages/metrics/src/Metrics.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Callback, Context } from 'aws-lambda';
1+
import { Callback, Context, Handler } from 'aws-lambda';
22
import { Utility } from '@aws-lambda-powertools/commons';
33
import { MetricsInterface } from '.';
44
import { ConfigServiceInterface, EnvironmentVariablesService } from './config';
@@ -58,8 +58,8 @@ const DEFAULT_NAMESPACE = 'default_namespace';
5858
* }
5959
* }
6060
*
61-
* export const handlerClass = new MyFunctionWithDecorator();
62-
* export const handler = handlerClass.handler;
61+
* const handlerClass = new MyFunctionWithDecorator();
62+
* export const handler = handlerClass.handler.bind(handlerClass);
6363
* ```
6464
*
6565
* ### Standard function
@@ -221,8 +221,8 @@ class Metrics extends Utility implements MetricsInterface {
221221
* }
222222
* }
223223
*
224-
* export const handlerClass = new MyFunctionWithDecorator();
225-
* export const handler = handlerClass.handler;
224+
* const handlerClass = new MyFunctionWithDecorator();
225+
* export const handler = handlerClass.handler.bind(handlerClass);
226226
* ```
227227
*
228228
* @decorator Class
@@ -236,24 +236,28 @@ class Metrics extends Utility implements MetricsInterface {
236236
this.setDefaultDimensions(defaultDimensions);
237237
}
238238

239-
return (target, _propertyKey, descriptor) => {
239+
return (_target, _propertyKey, descriptor) => {
240240
/**
241241
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
242242
*/
243243
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244244
const originalMethod = descriptor.value!;
245245

246-
descriptor.value = ( async (event: unknown, context: Context, callback: Callback): Promise<unknown> => {
247-
this.functionName = context.functionName;
248-
if (captureColdStartMetric) this.captureColdStartMetric();
246+
// eslint-disable-next-line @typescript-eslint/no-this-alias
247+
const metricsRef = this;
248+
// Use a function() {} instead of an () => {} arrow function so that we can
249+
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
250+
descriptor.value = ( async function(this: Handler, event: unknown, context: Context, callback: Callback): Promise<unknown> {
251+
metricsRef.functionName = context.functionName;
252+
if (captureColdStartMetric) metricsRef.captureColdStartMetric();
249253

250254
let result: unknown;
251255
try {
252-
result = await originalMethod.apply(target, [ event, context, callback ]);
256+
result = await originalMethod.apply(this, [ event, context, callback ]);
253257
} catch (error) {
254258
throw error;
255259
} finally {
256-
this.publishStoredMetrics();
260+
metricsRef.publishStoredMetrics();
257261
}
258262

259263
return result;

Diff for: packages/metrics/tests/e2e/basicFeatures.decorator.test.functionCode.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Metrics, MetricUnits } from '../../src';
22
import { Context } from 'aws-lambda';
3-
import { LambdaInterface } from '../../examples/utils/lambda/LambdaInterface';
3+
import { LambdaInterface } from '@aws-lambda-powertools/commons';
44

55
const namespace = process.env.EXPECTED_NAMESPACE ?? 'CdkExample';
66
const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler';
@@ -19,21 +19,28 @@ const metrics = new Metrics({ namespace: namespace, serviceName: serviceName });
1919
class Lambda implements LambdaInterface {
2020

2121
@metrics.logMetrics({ captureColdStartMetric: true, defaultDimensions: JSON.parse(defaultDimensions), throwOnEmptyMetrics: true })
22+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
23+
// @ts-ignore
2224
public async handler(_event: unknown, _context: Context): Promise<void> {
2325
metrics.addMetric(metricName, metricUnit, parseInt(metricValue));
2426
metrics.addDimension(
2527
Object.entries(JSON.parse(extraDimension))[0][0],
2628
Object.entries(JSON.parse(extraDimension))[0][1] as string,
2729
);
28-
30+
31+
this.dummyMethod();
32+
}
33+
34+
private dummyMethod(): void {
2935
const metricWithItsOwnDimensions = metrics.singleMetric();
3036
metricWithItsOwnDimensions.addDimension(
3137
Object.entries(JSON.parse(singleMetricDimension))[0][0],
3238
Object.entries(JSON.parse(singleMetricDimension))[0][1] as string,
3339
);
40+
3441
metricWithItsOwnDimensions.addMetric(singleMetricName, singleMetricUnit, parseInt(singleMetricValue));
3542
}
3643
}
3744

38-
export const handlerClass = new Lambda();
39-
export const handler = handlerClass.handler;
45+
const handlerClass = new Lambda();
46+
export const handler = handlerClass.handler.bind(handlerClass);

Diff for: packages/metrics/tests/unit/Metrics.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,37 @@ describe('Class: Metrics', () => {
619619

620620
expect(console.log).toBeCalledTimes(1);
621621
});
622+
623+
test('Using decorator should preserve `this` in decorated class', async () => {
624+
// Prepare
625+
const metrics = new Metrics({ namespace: 'test' });
626+
627+
// Act
628+
class LambdaFunction implements LambdaInterface {
629+
@metrics.logMetrics()
630+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
631+
// @ts-ignore
632+
public handler<TEvent, TResult>(
633+
_event: TEvent,
634+
_context: Context,
635+
_callback: Callback<TResult>,
636+
): void | Promise<TResult> {
637+
this.dummyMethod();
638+
}
639+
640+
private dummyMethod(): void {
641+
metrics.addMetric('test_name', MetricUnits.Seconds, 1);
642+
}
643+
}
644+
await new LambdaFunction().handler(dummyEvent, dummyContext.helloworldContext, () => console.log('Lambda invoked!'));
645+
const loggedData = JSON.parse(consoleSpy.mock.calls[0][0]);
646+
647+
// Assess
648+
expect(console.log).toBeCalledTimes(1);
649+
expect(loggedData._aws.CloudWatchMetrics[0].Metrics.length).toEqual(1);
650+
expect(loggedData._aws.CloudWatchMetrics[0].Metrics[0].Name).toEqual('test_name');
651+
expect(loggedData._aws.CloudWatchMetrics[0].Metrics[0].Unit).toEqual('Seconds');
652+
});
622653
});
623654

624655
describe('Feature: Custom Config Service', () => {

0 commit comments

Comments
 (0)