Skip to content

Commit 73990bb

Browse files
authored
fix(logger): decorated class methods cannot access this (aws-powertools#1060)
1 parent 107fa04 commit 73990bb

File tree

4 files changed

+98
-25
lines changed

4 files changed

+98
-25
lines changed

docs/core/logger.md

+13-6
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,12 @@ Key | Example
155155

156156
}
157157

158-
export const myFunction = new Lambda();
159-
export const handler = myFunction.handler;
158+
const myFunction = new Lambda();
159+
export const handler = myFunction.handler.bind(myFunction); // (1)
160160
```
161+
162+
1. Binding your handler method allows your handler to access `this` within the class methods.
163+
161164
=== "Manual"
162165

163166
```typescript hl_lines="7"
@@ -233,10 +236,12 @@ This is disabled by default to prevent sensitive info being logged
233236

234237
}
235238

236-
export const myFunction = new Lambda();
237-
export const handler = myFunction.handler;
239+
const myFunction = new Lambda();
240+
export const handler = myFunction.handler.bind(myFunction); // (1)
238241
```
239242

243+
1. Binding your handler method allows your handler to access `this` within the class methods.
244+
240245
### Appending persistent additional log keys and values
241246

242247
You can append additional persistent keys and values in the logs generated during a Lambda invocation using either mechanism:
@@ -398,10 +403,12 @@ If you want to make sure that persistent attributes added **inside the handler f
398403

399404
}
400405

401-
export const myFunction = new Lambda();
402-
export const handler = myFunction.handler;
406+
const myFunction = new Lambda();
407+
export const handler = myFunction.handler.bind(myFunction); // (1)
403408
```
404409

410+
1. Binding your handler method allows your handler to access `this` within the class methods.
411+
405412
In each case, the printed log will look like this:
406413

407414
=== "First invocation"

packages/logger/src/Logger.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Console } from 'console';
2-
import type { Context } from 'aws-lambda';
2+
import type { Context, Handler } from 'aws-lambda';
33
import { Utility } from '@aws-lambda-powertools/commons';
44
import { LogFormatterInterface, PowertoolLogFormatter } from './formatter';
55
import { LogItem } from './log';
@@ -99,8 +99,8 @@ import type {
9999
* }
100100
* }
101101
*
102-
* export const myFunction = new Lambda();
103-
* export const handler = myFunction.handler;
102+
* const handlerClass = new Lambda();
103+
* export const handler = handlerClass.handler.bind(handlerClass);
104104
* ```
105105
*
106106
* @class
@@ -279,30 +279,33 @@ class Logger extends Utility implements ClassThatLogs {
279279
/**
280280
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
281281
*/
282-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283282
const originalMethod = descriptor.value;
284283

285-
descriptor.value = (event, context, callback) => {
284+
// eslint-disable-next-line @typescript-eslint/no-this-alias
285+
const loggerRef = this;
286+
// Use a function() {} instead of an () => {} arrow function so that we can
287+
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
288+
descriptor.value = (function (this: Handler, event, context, callback) {
286289

287290
let initialPersistentAttributes = {};
288291
if (options && options.clearState === true) {
289-
initialPersistentAttributes = { ...this.getPersistentLogAttributes() };
292+
initialPersistentAttributes = { ...loggerRef.getPersistentLogAttributes() };
290293
}
291294

292-
Logger.injectLambdaContextBefore(this, event, context, options);
295+
Logger.injectLambdaContextBefore(loggerRef, event, context, options);
293296

294297
/* eslint-disable @typescript-eslint/no-non-null-assertion */
295298
let result: unknown;
296299
try {
297-
result = originalMethod!.apply(target, [ event, context, callback ]);
300+
result = originalMethod!.apply(this, [ event, context, callback ]);
298301
} catch (error) {
299302
throw error;
300303
} finally {
301-
Logger.injectLambdaContextAfterOrOnError(this, initialPersistentAttributes, options);
304+
Logger.injectLambdaContextAfterOrOnError(loggerRef, initialPersistentAttributes, options);
302305
}
303306

304307
return result;
305-
};
308+
});
306309
};
307310
}
308311

packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,39 @@ import { Logger } from '../../src';
22
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
33
import { LambdaInterface } from '@aws-lambda-powertools/commons';
44

5-
const SAMPLE_RATE = parseFloat(process.env.SAMPLE_RATE);
6-
const LOG_MSG = process.env.LOG_MSG;
5+
const SAMPLE_RATE = parseFloat(process.env.SAMPLE_RATE || '0.1');
6+
const LOG_MSG = process.env.LOG_MSG || 'Hello World';
77

88
const logger = new Logger({
99
sampleRateValue: SAMPLE_RATE,
1010
});
1111

1212
class Lambda implements LambdaInterface {
13+
private readonly logMsg: string;
14+
15+
public constructor() {
16+
this.logMsg = LOG_MSG;
17+
}
18+
1319
// Decorate your handler class method
1420
@logger.injectLambdaContext()
21+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
22+
// @ts-ignore
1523
public async handler(event: APIGatewayProxyEvent, context: Context): Promise<{requestId: string}> {
16-
logger.debug(LOG_MSG);
17-
logger.info(LOG_MSG);
18-
logger.warn(LOG_MSG);
19-
logger.error(LOG_MSG);
20-
24+
this.printLogInAllLevels();
25+
2126
return {
2227
requestId: context.awsRequestId,
2328
};
2429
}
30+
31+
private printLogInAllLevels() : void {
32+
logger.debug(this.logMsg);
33+
logger.info(this.logMsg);
34+
logger.warn(this.logMsg);
35+
logger.error(this.logMsg);
36+
}
2537
}
2638

27-
export const myFunction = new Lambda();
28-
export const handler = myFunction.handler;
39+
const myFunction = new Lambda();
40+
export const handler = myFunction.handler.bind(myFunction);

packages/logger/tests/unit/Logger.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,57 @@ describe('Class: Logger', () => {
11151115

11161116
});
11171117

1118+
test('when used as decorator the value of `this` is preserved on the decorated method/class', async () => {
1119+
1120+
// Prepare
1121+
const logger = new Logger({
1122+
logLevel: 'DEBUG',
1123+
});
1124+
const consoleSpy = jest.spyOn(logger['console'], 'info').mockImplementation();
1125+
1126+
class LambdaFunction implements LambdaInterface {
1127+
private readonly memberVariable: string;
1128+
1129+
public constructor(memberVariable: string) {
1130+
this.memberVariable = memberVariable;
1131+
}
1132+
1133+
@logger.injectLambdaContext()
1134+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1135+
// @ts-ignore
1136+
public handler<TResult>(_event: unknown, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
1137+
this.dummyMethod();
1138+
1139+
return;
1140+
}
1141+
1142+
private dummyMethod(): void {
1143+
logger.info({ message: `memberVariable:${this.memberVariable}` });
1144+
}
1145+
}
1146+
1147+
// Act
1148+
const lambda = new LambdaFunction('someValue');
1149+
const handler = lambda.handler.bind(lambda);
1150+
await handler({}, dummyContext, () => console.log('Lambda invoked!'));
1151+
1152+
// Assess
1153+
expect(consoleSpy).toBeCalledTimes(1);
1154+
expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({
1155+
cold_start: true,
1156+
function_arn: 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function',
1157+
function_memory_size: 128,
1158+
function_name: 'foo-bar-function',
1159+
function_request_id: 'c6af9ac6-7b61-11e6-9a41-93e812345678',
1160+
level: 'INFO',
1161+
message: 'memberVariable:someValue',
1162+
service: 'hello-world',
1163+
timestamp: '2016-06-20T12:08:10.000Z',
1164+
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
1165+
}));
1166+
1167+
});
1168+
11181169
});
11191170

11201171
describe('Method: refreshSampleRateCalculation', () => {

0 commit comments

Comments
 (0)