Skip to content

Commit 28db2e3

Browse files
authored
feat(logger): flush buffer on uncaught error decorator (#3676)
1 parent 8c8d6b2 commit 28db2e3

File tree

4 files changed

+299
-226
lines changed

4 files changed

+299
-226
lines changed

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

+66-31
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import type {
99
import type { Context, Handler } from 'aws-lambda';
1010
import merge from 'lodash.merge';
1111
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
12-
import { LogJsonIndent, LogLevelThreshold, ReservedKeys } from './constants.js';
12+
import {
13+
LogJsonIndent,
14+
LogLevelThreshold,
15+
ReservedKeys,
16+
UncaughtErrorLogMessage,
17+
} from './constants.js';
1318
import type { LogFormatter } from './formatter/LogFormatter.js';
1419
import type { LogItem } from './formatter/LogItem.js';
1520
import { PowertoolsLogFormatter } from './formatter/PowertoolsLogFormatter.js';
@@ -39,6 +44,7 @@ import type {
3944
* * Capture key fields from AWS Lambda context, cold start, and structure log output as JSON
4045
* * Append additional keys to one or all log items
4146
* * Switch log level to `DEBUG` based on a sample rate value for a percentage of invocations
47+
* * Ability to buffer logs in memory and flush them when there's an error
4248
*
4349
* After initializing the Logger class, you can use the methods to log messages at different levels.
4450
*
@@ -54,7 +60,7 @@ import type {
5460
* };
5561
* ```
5662
*
57-
* To enrich the log items with information from the Lambda context, you can use the {@link Logger.addContext | addContext()} method.
63+
* To enrich the log items with information from the Lambda context, you can use the {@link Logger.addContext | `addContext()`} method.
5864
*
5965
* @example
6066
* ```typescript
@@ -69,7 +75,7 @@ import type {
6975
* };
7076
* ```
7177
*
72-
* You can also add additional attributes to all log items using the {@link Logger.appendKeys | appendKeys()} method.
78+
* You can also add additional attributes to all log items using the {@link Logger.appendKeys | `appendKeys()`} method.
7379
*
7480
* @example
7581
* ```typescript
@@ -82,10 +88,10 @@ import type {
8288
* };
8389
*```
8490
*
85-
* If you write your functions as classes and use TypeScript, you can use the {@link Logger.injectLambdaContext} class method decorator
91+
* If you write your functions as classes and use TypeScript, you can use the {@link Logger.injectLambdaContext | `injectLambdaContext()`} class method decorator
8692
* to automatically add context to your logs and clear the state after the invocation.
8793
*
88-
* If instead you use Middy.js middlewares, you use the {@link "middleware/middy".injectLambdaContext | injectLambdaContext()} middleware.
94+
* If instead you use Middy.js middlewares, you use the {@link "middleware/middy".injectLambdaContext | `injectLambdaContext()`} middleware.
8995
*
9096
* @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/
9197
*/
@@ -97,7 +103,7 @@ class Logger extends Utility implements LoggerInterface {
97103
* full control over the output of the logs. In testing environments, we use the
98104
* default console instance.
99105
*
100-
* This property is initialized in the constructor in setOptions().
106+
* This property is initialized in the constructor in `setOptions()`.
101107
*/
102108
private console!: Console;
103109
/**
@@ -191,7 +197,7 @@ class Logger extends Utility implements LoggerInterface {
191197
#maxBufferBytesSize = 20480;
192198

193199
/**
194-
* Contains buffered logs, grouped by _X_AMZN_TRACE_ID, each group with a max size of `maxBufferBytesSize`
200+
* Contains buffered logs, grouped by `_X_AMZN_TRACE_ID`, each group with a max size of `maxBufferBytesSize`
195201
*/
196202
#buffer?: CircularMap<string>;
197203

@@ -379,8 +385,8 @@ class Logger extends Utility implements LoggerInterface {
379385
* Class method decorator that adds the current Lambda function context as extra
380386
* information in all log items.
381387
*
382-
* This decorator is useful when you want to add the Lambda context to all log items
383-
* and it works only when decorating a class method that is a Lambda function handler.
388+
* This decorator is useful when you want to enrich your logs with information
389+
* from the function context, such as the function name, version, and request ID, and more.
384390
*
385391
* @example
386392
* ```typescript
@@ -401,7 +407,18 @@ class Logger extends Utility implements LoggerInterface {
401407
* export const handler = handlerClass.handler.bind(handlerClass);
402408
* ```
403409
*
410+
* The decorator can also be used to log the Lambda invocation event; this can be configured both via
411+
* the `logEvent` parameter and the `POWERTOOLS_LOGGER_LOG_EVENT` environment variable. When both
412+
* are set, the `logEvent` parameter takes precedence.
413+
*
414+
* Additionally, the decorator can be used to reset the temporary keys added with the `appendKeys()` method
415+
* after the invocation, or to flush the buffer when an uncaught error is thrown in the handler.
416+
*
404417
* @see https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators
418+
*
419+
* @param options.logEvent - When `true` the logger will log the event.
420+
* @param options.resetKeys - When `true` the logger will clear temporary keys added with {@link Logger.appendKeys() `appendKeys()`} method.
421+
* @param options.flushBufferOnUncaughtError - When `true` the logger will flush the buffer when an uncaught error is thrown in the handler.
405422
*/
406423
public injectLambdaContext(
407424
options?: InjectLambdaContextOptions
@@ -420,16 +437,24 @@ class Logger extends Utility implements LoggerInterface {
420437
callback
421438
) {
422439
loggerRef.refreshSampleRateCalculation();
423-
Logger.injectLambdaContextBefore(loggerRef, event, context, options);
440+
loggerRef.addContext(context);
441+
loggerRef.logEventIfEnabled(event, options?.logEvent);
424442

425-
let result: unknown;
426443
try {
427-
result = await originalMethod.apply(this, [event, context, callback]);
444+
return await originalMethod.apply(this, [event, context, callback]);
445+
} catch (error) {
446+
if (options?.flushBufferOnUncaughtError) {
447+
loggerRef.flushBuffer();
448+
loggerRef.error({
449+
message: UncaughtErrorLogMessage,
450+
error,
451+
});
452+
}
453+
throw error;
454+
/* v8 ignore next */
428455
} finally {
429456
if (options?.clearState || options?.resetKeys) loggerRef.resetKeys();
430457
}
431-
432-
return result;
433458
};
434459
};
435460
}
@@ -450,7 +475,7 @@ class Logger extends Utility implements LoggerInterface {
450475
/**
451476
* @deprecated - This method is deprecated and will be removed in the next major version.
452477
*/
453-
public static injectLambdaContextBefore(
478+
/* v8 ignore start */ public static injectLambdaContextBefore(
454479
logger: Logger,
455480
event: unknown,
456481
context: Context,
@@ -463,7 +488,7 @@ class Logger extends Utility implements LoggerInterface {
463488
shouldLogEvent = options.logEvent;
464489
}
465490
logger.logEventIfEnabled(event, shouldLogEvent);
466-
}
491+
} /* v8 ignore stop */
467492

468493
/**
469494
* Log the AWS Lambda event payload for the current invocation if the environment variable `POWERTOOLS_LOGGER_LOG_EVENT` is set to `true`.
@@ -1239,6 +1264,11 @@ class Logger extends Utility implements LoggerInterface {
12391264
persistentKeys && this.appendPersistentKeys(persistentKeys);
12401265
}
12411266

1267+
/**
1268+
* Configure the buffer settings for the Logger instance.
1269+
*
1270+
* @param options - Options to configure the Logger instance
1271+
*/
12421272
#setLogBuffering(
12431273
options: NonNullable<ConstructorOptions['logBufferOptions']>
12441274
) {
@@ -1269,30 +1299,34 @@ class Logger extends Utility implements LoggerInterface {
12691299
}
12701300

12711301
/**
1272-
* Add a log to the buffer
1273-
* @param xrayTraceId - _X_AMZN_TRACE_ID of the request
1302+
* Add a log to the buffer.
1303+
*
1304+
* @param xrayTraceId - `_X_AMZN_TRACE_ID` of the request
12741305
* @param log - Log to be buffered
1275-
* @param logLevel - level of log to be buffered
1306+
* @param logLevel - The level of log to be buffered
12761307
*/
12771308
protected bufferLogItem(
12781309
xrayTraceId: string,
12791310
log: LogItem,
12801311
logLevel: number
12811312
): void {
12821313
log.prepareForPrint();
1283-
1284-
const stringified = JSON.stringify(
1285-
log.getAttributes(),
1286-
this.getJsonReplacer(),
1287-
this.logIndentation
1314+
this.#buffer?.setItem(
1315+
xrayTraceId,
1316+
JSON.stringify(
1317+
log.getAttributes(),
1318+
this.getJsonReplacer(),
1319+
this.logIndentation
1320+
),
1321+
logLevel
12881322
);
1289-
1290-
this.#buffer?.setItem(xrayTraceId, stringified, logLevel);
12911323
}
12921324

12931325
/**
1294-
* Flushes all items of the respective _X_AMZN_TRACE_ID within
1295-
* the buffer.
1326+
* Flush all logs in the request buffer.
1327+
*
1328+
* This is called automatically when you use the {@link injectLambdaContext | `@logger.injectLambdaContext()`} decorator and
1329+
* your function throws an error.
12961330
*/
12971331
public flushBuffer(): void {
12981332
const traceId = this.envVarsService.getXrayTraceId();
@@ -1328,9 +1362,10 @@ class Logger extends Utility implements LoggerInterface {
13281362
this.#buffer?.delete(traceId);
13291363
}
13301364
/**
1331-
* Tests if the log meets the criteria to be buffered
1332-
* @param traceId - _X_AMZN_TRACE_ID of the request
1333-
* @param logLevel - The level of the log being considered
1365+
* Test if the log meets the criteria to be buffered.
1366+
*
1367+
* @param traceId - `_X_AMZN_TRACE_ID` of the request
1368+
* @param logLevel - The level of the log being considered
13341369
*/
13351370
protected shouldBufferLog(
13361371
traceId: string | undefined,

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,16 @@ const ReservedKeys = [
5050
'timestamp',
5151
];
5252

53-
export { LogJsonIndent, LogLevel, LogLevelThreshold, ReservedKeys };
53+
/**
54+
* Message logged when an uncaught error occurs in a Lambda function.
55+
*/
56+
const UncaughtErrorLogMessage =
57+
'Uncaught error detected, flushing log buffer before exit';
58+
59+
export {
60+
LogJsonIndent,
61+
LogLevel,
62+
LogLevelThreshold,
63+
ReservedKeys,
64+
UncaughtErrorLogMessage,
65+
};

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ type InjectLambdaContextOptions = {
4848
* If `true`, the logger will reset the keys added via {@link LoggerInterface.appendKeys()}
4949
*/
5050
resetKeys?: boolean;
51+
/**
52+
* Whether to flush the log buffer when an uncaught error is logged.
53+
*
54+
* @default `false`
55+
*/
56+
flushBufferOnUncaughtError?: boolean;
5157
};
5258

5359
/**
@@ -197,7 +203,7 @@ type LogBufferOption = {
197203
/**
198204
* The threshold to buffer logs. Logs with a level below
199205
* this threshold will be buffered
200-
* @default `'DEBUG'`
206+
* @default `DEBUG`
201207
*/
202208
bufferAtVerbosity?: Omit<
203209
LogLevel,

0 commit comments

Comments
 (0)