Skip to content

Commit 5ebd1cf

Browse files
authored
fix(logger): fix clearstate bug when lambda handler throws (#1045)
* chore(logger): removed unused var * test(logger): expect lambda handler to throw in unit tests for clear state functionality * chore(logger): fix linting * test(logger): improve readability of injectLambdaContextBefore & injectLambdaContextAfterOrOnError function behaviour - separation of concerns
1 parent c3a20c6 commit 5ebd1cf

File tree

4 files changed

+131
-25
lines changed

4 files changed

+131
-25
lines changed

Diff for: packages/logger/src/Logger.ts

+26-14
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,6 @@ class Logger extends Utility implements ClassThatLogs {
275275
* @returns {HandlerMethodDecorator}
276276
*/
277277
public injectLambdaContext(options?: HandlerOptions): HandlerMethodDecorator {
278-
const isClearStateEnabled = options && options.clearState === true;
279-
280278
return (target, _propertyKey, descriptor) => {
281279
/**
282280
* The descriptor.value is the method this decorator decorates, it cannot be undefined.
@@ -286,30 +284,44 @@ class Logger extends Utility implements ClassThatLogs {
286284

287285
descriptor.value = (event, context, callback) => {
288286

289-
let initialPersistentAttributes: LogAttributes = {};
290-
if (isClearStateEnabled) {
287+
let initialPersistentAttributes = {};
288+
if (options && options.clearState === true) {
291289
initialPersistentAttributes = { ...this.getPersistentLogAttributes() };
292290
}
293291

294-
this.addContext(context);
295-
let shouldLogEvent = undefined;
296-
if ( options && options.hasOwnProperty('logEvent') ) {
297-
shouldLogEvent = options.logEvent;
298-
}
299-
this.logEventIfEnabled(event, shouldLogEvent);
292+
Logger.injectLambdaContextBefore(this, event, context, options);
300293

301294
/* eslint-disable @typescript-eslint/no-non-null-assertion */
302-
const result = originalMethod!.apply(target, [ event, context, callback ]);
303-
304-
if (isClearStateEnabled) {
305-
this.setPersistentLogAttributes(initialPersistentAttributes);
295+
let result: unknown;
296+
try {
297+
result = originalMethod!.apply(target, [ event, context, callback ]);
298+
} catch (error) {
299+
throw error;
300+
} finally {
301+
Logger.injectLambdaContextAfterOrOnError(this, initialPersistentAttributes, options);
306302
}
307303

308304
return result;
309305
};
310306
};
311307
}
312308

309+
public static injectLambdaContextAfterOrOnError(logger: Logger, initialPersistentAttributes: LogAttributes, options?: HandlerOptions): void {
310+
if (options && options.clearState === true) {
311+
logger.setPersistentLogAttributes(initialPersistentAttributes);
312+
}
313+
}
314+
315+
public static injectLambdaContextBefore(logger: Logger, event: unknown, context: Context, options?: HandlerOptions): void {
316+
logger.addContext(context);
317+
318+
let shouldLogEvent = undefined;
319+
if (options && options.hasOwnProperty('logEvent')) {
320+
shouldLogEvent = options.logEvent;
321+
}
322+
logger.logEventIfEnabled(event, shouldLogEvent);
323+
}
324+
313325
/**
314326
* Logs a Lambda invocation event, if it *should*.
315327
*

Diff for: packages/logger/src/middleware/middy.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Logger } from '../Logger';
1+
import { Logger } from '../Logger';
22
import type middy from '@middy/core';
33
import { HandlerOptions, LogAttributes } from '../types';
44

@@ -34,30 +34,25 @@ const injectLambdaContext = (target: Logger | Logger[], options?: HandlerOptions
3434

3535
const injectLambdaContextBefore = async (request: middy.Request): Promise<void> => {
3636
loggers.forEach((logger: Logger) => {
37-
logger.addContext(request.context);
3837
if (options && options.clearState === true) {
3938
persistentAttributes.push({ ...logger.getPersistentLogAttributes() });
4039
}
41-
42-
let shouldLogEvent = undefined;
43-
if ( options && options.hasOwnProperty('logEvent') ) {
44-
shouldLogEvent = options.logEvent;
45-
}
46-
logger.logEventIfEnabled(request.event, shouldLogEvent);
40+
Logger.injectLambdaContextBefore(logger, request.event, request.context, options);
4741
});
4842
};
4943

50-
const injectLambdaContextAfter = async (): Promise<void> => {
44+
const injectLambdaContextAfterOrOnError = async (): Promise<void> => {
5145
if (options && options.clearState === true) {
5246
loggers.forEach((logger: Logger, index: number) => {
53-
logger.setPersistentLogAttributes(persistentAttributes[index]);
47+
Logger.injectLambdaContextAfterOrOnError(logger, persistentAttributes[index], options);
5448
});
5549
}
5650
};
5751

5852
return {
5953
before: injectLambdaContextBefore,
60-
after: injectLambdaContextAfter
54+
after: injectLambdaContextAfterOrOnError,
55+
onError: injectLambdaContextAfterOrOnError
6156
};
6257
};
6358

Diff for: packages/logger/tests/unit/Logger.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,52 @@ describe('Class: Logger', () => {
982982

983983
});
984984

985+
test('when used as decorator with the clear state flag enabled and the handler throws an error, the persistent log attributes added in the handler are removed after the handler\'s code is executed', async () => {
986+
987+
// Prepare
988+
const logger = new Logger({
989+
logLevel: 'DEBUG',
990+
persistentLogAttributes: {
991+
foo: 'bar',
992+
biz: 'baz'
993+
}
994+
});
995+
996+
type CustomEvent = { user_id: string };
997+
998+
class LambdaFunction implements LambdaInterface {
999+
1000+
@logger.injectLambdaContext({ clearState: true })
1001+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1002+
// @ts-ignore
1003+
public handler<TResult>(event: CustomEvent, _context: Context, _callback: Callback<TResult>): void | Promise<TResult> {
1004+
// Only add these persistent for the scope of this lambda handler
1005+
logger.appendKeys({
1006+
details: { user_id: event['user_id'] }
1007+
});
1008+
logger.debug('This is a DEBUG log with the user_id');
1009+
logger.debug('This is another DEBUG log with the user_id');
1010+
1011+
throw new Error('Unexpected error occurred!');
1012+
}
1013+
}
1014+
1015+
const persistentAttribs = { ...logger.getPersistentLogAttributes() };
1016+
1017+
// Act & Assess
1018+
const executeLambdaHandler = async (): Promise<void> => {
1019+
await new LambdaFunction().handler({ user_id: '123456' }, dummyContext, () => console.log('Lambda invoked!'));
1020+
};
1021+
await expect(executeLambdaHandler()).rejects.toThrow('Unexpected error occurred!');
1022+
const persistentAttribsAfterInvocation = { ...logger.getPersistentLogAttributes() };
1023+
expect(persistentAttribs).toEqual({
1024+
foo: 'bar',
1025+
biz: 'baz'
1026+
});
1027+
expect(persistentAttribsAfterInvocation).toEqual(persistentAttribs);
1028+
1029+
});
1030+
9851031
test('when used as decorator with the log event flag enabled, it logs the event', async () => {
9861032

9871033
// Prepare

Diff for: packages/logger/tests/unit/middleware/middy.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,59 @@ describe('Middy middleware', () => {
207207

208208
});
209209

210+
test('when enabled and the handler throws an error, the persistent log attributes added within the handler scope are removed after the invocation ends', async () => {
211+
212+
// Prepare
213+
const logger = new Logger({
214+
logLevel: 'DEBUG',
215+
persistentLogAttributes: {
216+
foo: 'bar',
217+
biz: 'baz'
218+
}
219+
});
220+
const context = {
221+
callbackWaitsForEmptyEventLoop: true,
222+
functionVersion: '$LATEST',
223+
functionName: 'foo-bar-function',
224+
memoryLimitInMB: '128',
225+
logGroupName: '/aws/lambda/foo-bar-function',
226+
logStreamName: '2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456',
227+
invokedFunctionArn: 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function',
228+
awsRequestId: 'abcdef123456abcdef123456',
229+
getRemainingTimeInMillis: () => 1234,
230+
done: () => console.log('Done!'),
231+
fail: () => console.log('Failed!'),
232+
succeed: () => console.log('Succeeded!'),
233+
};
234+
235+
const lambdaHandler = (event: { user_id: string }): void => {
236+
// Only add these persistent for the scope of this lambda handler
237+
logger.appendKeys({
238+
details: { user_id: event['user_id'] }
239+
});
240+
logger.debug('This is a DEBUG log with the user_id');
241+
logger.debug('This is another DEBUG log with the user_id');
242+
243+
throw new Error('Unexpected error occurred!');
244+
};
245+
246+
const persistentAttribs = { ...logger.getPersistentLogAttributes() };
247+
const handler = middy(lambdaHandler).use(injectLambdaContext(logger, { clearState: true }));
248+
249+
// Act & Assess
250+
const executeLambdaHandler = async (): Promise<void> => {
251+
await handler({ user_id: '123456' }, context, () => console.log('Lambda invoked!'));
252+
};
253+
await expect(executeLambdaHandler()).rejects.toThrow('Unexpected error occurred!');
254+
const persistentAttribsAfterInvocation = { ...logger.getPersistentLogAttributes() };
255+
expect(persistentAttribs).toEqual({
256+
foo: 'bar',
257+
biz: 'baz'
258+
});
259+
expect(persistentAttribsAfterInvocation).toEqual(persistentAttribs);
260+
261+
});
262+
210263
});
211264

212265
describe('Feature: log event', () => {

0 commit comments

Comments
 (0)