diff --git a/docs/core/logger.md b/docs/core/logger.md index 6883accf92..b304ba69a8 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -155,9 +155,12 @@ Key | Example } - export const myFunction = new Lambda(); - export const handler = myFunction.handler; + const myFunction = new Lambda(); + 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" @@ -233,10 +236,12 @@ This is disabled by default to prevent sensitive info being logged } - export const myFunction = new Lambda(); - export const handler = myFunction.handler; + const myFunction = new Lambda(); + 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: @@ -398,10 +403,12 @@ If you want to make sure that persistent attributes added **inside the handler f } - export const myFunction = new Lambda(); - export const handler = myFunction.handler; + const myFunction = new Lambda(); + 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" diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index ece63b3bd5..2bbdb3fcc1 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -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'; @@ -99,8 +99,8 @@ import type { * } * } * - * export const myFunction = new Lambda(); - * export const handler = myFunction.handler; + * const handlerClass = new Lambda(); + * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * @class @@ -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; - }; + }); }; } diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts index bab978b23a..72c13f07b4 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts @@ -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.printLogInAllLevels(); + return { requestId: context.awsRequestId, }; } + + private printLogInAllLevels() : void { + 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; \ No newline at end of file +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); \ No newline at end of file diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index 43cf6fc0c1..14efa09204 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -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(_event: unknown, _context: Context, _callback: Callback): void | Promise { + 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', () => {