Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(logger): decorated class methods cannot access this #1060

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/core/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,11 @@ Key | Example
}

export const myFunction = new Lambda();
export const handler = myFunction.handler;
export const handler = myFunction.handler.bind(myFunction); // (1)
```

1. Binding your handler method allows your handler to access `this` within the class methods.

=== "Manual"

```typescript hl_lines="7"
Expand Down Expand Up @@ -234,9 +237,11 @@ This is disabled by default to prevent sensitive info being logged
}

export const myFunction = new Lambda();
export const handler = myFunction.handler;
export const handler = myFunction.handler.bind(myFunction); // (1)
```

1. Binding your handler method allows your handler to access `this` within the class methods.

### Appending persistent additional log keys and values

You can append additional persistent keys and values in the logs generated during a Lambda invocation using either mechanism:
Expand Down Expand Up @@ -399,9 +404,11 @@ If you want to make sure that persistent attributes added **inside the handler f
}

export const myFunction = new Lambda();
export const handler = myFunction.handler;
export const handler = myFunction.handler.bind(myFunction); // (1)
```

1. Binding your handler method allows your handler to access `this` within the class methods.

In each case, the printed log will look like this:

=== "First invocation"
Expand Down
23 changes: 13 additions & 10 deletions packages/logger/src/Logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Console } from 'console';
import type { Context } from 'aws-lambda';
import type { Context, Handler } from 'aws-lambda';
import { Utility } from '@aws-lambda-powertools/commons';
import { LogFormatterInterface, PowertoolLogFormatter } from './formatter';
import { LogItem } from './log';
Expand Down Expand Up @@ -99,8 +99,8 @@ import type {
* }
* }
*
* export const myFunction = new Lambda();
* export const handler = myFunction.handler;
* export const handlerClass = new Lambda();
* export const handler = handlerClass.handler.bind(handlerClass);
* ```
*
* @class
Expand Down Expand Up @@ -279,30 +279,33 @@ class Logger extends Utility implements ClassThatLogs {
/**
* 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 = (event, context, callback) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const loggerRef = this;
// Use a function() {} instead of an () => {} arrow function so that we can
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
descriptor.value = (function (this: Handler, event, context, callback) {

let initialPersistentAttributes = {};
if (options && options.clearState === true) {
initialPersistentAttributes = { ...this.getPersistentLogAttributes() };
initialPersistentAttributes = { ...loggerRef.getPersistentLogAttributes() };
}

Logger.injectLambdaContextBefore(this, event, context, options);
Logger.injectLambdaContextBefore(loggerRef, event, context, options);

/* eslint-disable @typescript-eslint/no-non-null-assertion */
let result: unknown;
try {
result = originalMethod!.apply(target, [ event, context, callback ]);
result = originalMethod!.apply(this, [ event, context, callback ]);
} catch (error) {
throw error;
} finally {
Logger.injectLambdaContextAfterOrOnError(this, initialPersistentAttributes, options);
Logger.injectLambdaContextAfterOrOnError(loggerRef, initialPersistentAttributes, options);
}

return result;
};
});
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,39 @@ import { Logger } from '../../src';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { LambdaInterface } from '@aws-lambda-powertools/commons';

const SAMPLE_RATE = parseFloat(process.env.SAMPLE_RATE);
const LOG_MSG = process.env.LOG_MSG;
const SAMPLE_RATE = parseFloat(process.env.SAMPLE_RATE || '0.1');
const LOG_MSG = process.env.LOG_MSG || 'Hello World';

const logger = new Logger({
sampleRateValue: SAMPLE_RATE,
});

class Lambda implements LambdaInterface {
private readonly logMsg: string;

public constructor() {
this.logMsg = LOG_MSG;
}

// Decorate your handler class method
@logger.injectLambdaContext()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
public async handler(event: APIGatewayProxyEvent, context: Context): Promise<{requestId: string}> {
logger.debug(LOG_MSG);
logger.info(LOG_MSG);
logger.warn(LOG_MSG);
logger.error(LOG_MSG);

this.dummyMethod();

return {
requestId: context.awsRequestId,
};
}

private dummyMethod() : void {
dreamorosi marked this conversation as resolved.
Show resolved Hide resolved
logger.debug(this.logMsg);
logger.info(this.logMsg);
logger.warn(this.logMsg);
logger.error(this.logMsg);
}
}

export const myFunction = new Lambda();
export const handler = myFunction.handler;
export const handler = myFunction.handler.bind(myFunction);
51 changes: 51 additions & 0 deletions packages/logger/tests/unit/Logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,57 @@ describe('Class: Logger', () => {

});

test('when used as decorator the value of `this` is preserved on the decorated method/class', async () => {

// Prepare
const logger = new Logger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], 'info').mockImplementation();

class LambdaFunction implements LambdaInterface {
private readonly memberVariable: string;

public constructor(memberVariable: string) {
this.memberVariable = memberVariable;
}

@logger.injectLambdaContext()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
public handler<TResult>(_event: unknown, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
this.dummyMethod();

return;
}

private dummyMethod(): void {
logger.info({ message: `memberVariable:${this.memberVariable}` });
}
}

// Act
const lambda = new LambdaFunction('someValue');
const handler = lambda.handler.bind(lambda);
await handler({}, dummyContext, () => console.log('Lambda invoked!'));

// Assess
expect(consoleSpy).toBeCalledTimes(1);
expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({
cold_start: true,
function_arn: 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function',
function_memory_size: 128,
function_name: 'foo-bar-function',
function_request_id: 'c6af9ac6-7b61-11e6-9a41-93e812345678',
level: 'INFO',
message: 'memberVariable:someValue',
service: 'hello-world',
timestamp: '2016-06-20T12:08:10.000Z',
xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793',
}));

});

});

describe('Method: refreshSampleRateCalculation', () => {
Expand Down