Skip to content

Commit 23eebe4

Browse files
authored
feat(logger): Flush buffer on uncaught error in Middy middleware (#3690)
1 parent 58f376e commit 23eebe4

File tree

3 files changed

+179
-44
lines changed

3 files changed

+179
-44
lines changed

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

+16
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ class Logger extends Utility implements LoggerInterface {
454454
/* v8 ignore next */
455455
} finally {
456456
if (options?.clearState || options?.resetKeys) loggerRef.resetKeys();
457+
loggerRef.clearBuffer();
457458
}
458459
};
459460
};
@@ -1361,6 +1362,21 @@ class Logger extends Utility implements LoggerInterface {
13611362

13621363
this.#buffer?.delete(traceId);
13631364
}
1365+
1366+
/**
1367+
* Empties the buffer for the current request
1368+
*
1369+
*/
1370+
public clearBuffer(): void {
1371+
const traceId = this.envVarsService.getXrayTraceId();
1372+
1373+
if (traceId === undefined) {
1374+
return;
1375+
}
1376+
1377+
this.#buffer?.delete(traceId);
1378+
}
1379+
13641380
/**
13651381
* Test if the log meets the criteria to be buffered.
13661382
*

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

+25-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
MiddyLikeRequest,
55
} from '@aws-lambda-powertools/commons/types';
66
import { Logger } from '../Logger.js';
7+
import { UncaughtErrorLogMessage } from '../constants.js';
78
import type { InjectLambdaContextOptions } from '../types/Logger.js';
89

910
/**
@@ -76,13 +77,11 @@ const injectLambdaContext = (
7677
const setCleanupFunction = (request: MiddyLikeRequest): void => {
7778
request.internal = {
7879
...request.internal,
79-
[LOGGER_KEY]: injectLambdaContextAfterOrOnError,
80+
[LOGGER_KEY]: after,
8081
};
8182
};
8283

83-
const injectLambdaContextBefore = async (
84-
request: MiddyLikeRequest
85-
): Promise<void> => {
84+
const before = async (request: MiddyLikeRequest): Promise<void> => {
8685
for (const logger of loggers) {
8786
if (isResetStateEnabled) {
8887
setCleanupFunction(request);
@@ -99,18 +98,34 @@ const injectLambdaContext = (
9998
}
10099
};
101100

102-
const injectLambdaContextAfterOrOnError = async (): Promise<void> => {
103-
if (isResetStateEnabled) {
104-
for (const logger of loggers) {
101+
const after = async (): Promise<void> => {
102+
for (const logger of loggers) {
103+
logger.clearBuffer();
104+
105+
if (isResetStateEnabled) {
105106
logger.resetKeys();
106107
}
107108
}
108109
};
109110

111+
const onError = async ({ error }: { error: unknown }): Promise<void> => {
112+
for (const logger of loggers) {
113+
if (options?.flushBufferOnUncaughtError) {
114+
logger.flushBuffer();
115+
logger.error({
116+
message: UncaughtErrorLogMessage,
117+
error,
118+
});
119+
} else {
120+
logger.clearBuffer();
121+
}
122+
}
123+
};
124+
110125
return {
111-
before: injectLambdaContextBefore,
112-
after: injectLambdaContextAfterOrOnError,
113-
onError: injectLambdaContextAfterOrOnError,
126+
before,
127+
after,
128+
onError,
114129
};
115130
};
116131

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

+138-34
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import context from '@aws-lambda-powertools/testing-utils/context';
22
import type { Context } from 'aws-lambda';
3+
import middy from 'middy5';
34
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
45
import { Logger } from '../../src/Logger.js';
56
import { LogLevel, UncaughtErrorLogMessage } from '../../src/constants.js';
7+
import { injectLambdaContext } from '../../src/middleware/middy.js';
68
import type { ConstructorOptions } from '../../src/types/Logger.js';
79

810
describe('Buffer logs', () => {
@@ -195,6 +197,34 @@ describe('Buffer logs', () => {
195197
);
196198
});
197199

200+
it('it safely short circuits when clearBuffer is called without a trace id', () => {
201+
// Prepare
202+
process.env._X_AMZN_TRACE_ID = undefined;
203+
const logger = new Logger({
204+
logLevel: LogLevel.ERROR,
205+
logBufferOptions: { enabled: true, bufferAtVerbosity: LogLevel.DEBUG },
206+
});
207+
208+
// Assess
209+
expect(() => logger.clearBuffer()).not.toThrow();
210+
});
211+
212+
it('it clears the buffer', () => {
213+
// Prepare
214+
const logger = new Logger({
215+
logLevel: LogLevel.ERROR,
216+
logBufferOptions: { enabled: true, bufferAtVerbosity: LogLevel.DEBUG },
217+
});
218+
219+
// Arrange
220+
logger.debug('This is a log message');
221+
logger.clearBuffer();
222+
223+
logger.flushBuffer();
224+
225+
// Assess
226+
expect(console.debug).not.toBeCalled;
227+
});
198228
it('it flushes the buffer when an error in logged', () => {
199229
// Prepare
200230
const logger = new Logger({
@@ -213,39 +243,113 @@ describe('Buffer logs', () => {
213243
expect(console.error).toBeCalledTimes(1);
214244
});
215245

216-
it('flushes the buffer when an uncaught error is thrown', async () => {
217-
// Prepare
218-
const logger = new Logger({ logBufferOptions: { enabled: true } });
219-
class TestClass {
220-
@logger.injectLambdaContext({ flushBufferOnUncaughtError: true })
221-
async handler(_event: unknown, _context: Context) {
222-
logger.debug('This is a log message');
223-
logger.info('This is an info message');
224-
throw new Error('This is an error');
225-
}
246+
it.each([
247+
{
248+
handlerFactory: (logger: Logger) =>
249+
middy()
250+
.use(
251+
injectLambdaContext(logger, { flushBufferOnUncaughtError: true })
252+
)
253+
.handler(async () => {
254+
logger.debug('This is a log message');
255+
logger.info('This is an info message');
256+
throw new Error('This is an error');
257+
}),
258+
case: 'middleware',
259+
},
260+
{
261+
handlerFactory: (logger: Logger) => {
262+
class TestClass {
263+
@logger.injectLambdaContext({ flushBufferOnUncaughtError: true })
264+
async handler(_event: unknown, _context: Context) {
265+
logger.debug('This is a log message');
266+
logger.info('This is an info message');
267+
throw new Error('This is an error');
268+
}
269+
}
270+
const lambda = new TestClass();
271+
return lambda.handler.bind(lambda);
272+
},
273+
case: 'decorator',
274+
},
275+
])(
276+
'flushes the buffer when an uncaught error is thrown ($case)',
277+
async ({ handlerFactory }) => {
278+
// Prepare
279+
const logger = new Logger({ logBufferOptions: { enabled: true } });
280+
const handler = handlerFactory(logger);
281+
282+
// Act & Assess
283+
await expect(() =>
284+
handler(
285+
{
286+
foo: 'bar',
287+
},
288+
context
289+
)
290+
).rejects.toThrow(new Error('This is an error'));
291+
expect(console.debug).toBeCalledTimes(1);
292+
expect(console.info).toBeCalledTimes(1);
293+
expect(console.error).toHaveLogged(
294+
expect.objectContaining({
295+
message: UncaughtErrorLogMessage,
296+
})
297+
);
298+
// If debug is called after info, it means it was buffered and then flushed
299+
expect(console.debug).toHaveBeenCalledAfter(console.info as Mock);
300+
// If error is called after debug, it means the buffer was flushed before the error log
301+
expect(console.debug).toHaveBeenCalledBefore(console.error as Mock);
226302
}
227-
const lambda = new TestClass();
228-
const handler = lambda.handler.bind(lambda);
229-
230-
// Act & Assess
231-
await expect(() =>
232-
handler(
233-
{
234-
foo: 'bar',
235-
},
236-
context
237-
)
238-
).rejects.toThrow(new Error('This is an error'));
239-
expect(console.debug).toBeCalledTimes(1);
240-
expect(console.info).toBeCalledTimes(1);
241-
expect(console.error).toHaveLogged(
242-
expect.objectContaining({
243-
message: UncaughtErrorLogMessage,
244-
})
245-
);
246-
// If debug is called after info, it means it was buffered and then flushed
247-
expect(console.debug).toHaveBeenCalledAfter(console.info as Mock);
248-
// If error is called after debug, it means the buffer was flushed before the error log
249-
expect(console.debug).toHaveBeenCalledBefore(console.error as Mock);
250-
});
303+
);
304+
it.each([
305+
{
306+
handlerFactory: (logger: Logger) =>
307+
middy()
308+
.use(
309+
injectLambdaContext(logger, { flushBufferOnUncaughtError: false })
310+
)
311+
.handler(async () => {
312+
logger.debug('This is a log message');
313+
logger.info('This is an info message');
314+
throw new Error('This is an error');
315+
}),
316+
case: 'middleware',
317+
},
318+
{
319+
handlerFactory: (logger: Logger) => {
320+
class TestClass {
321+
@logger.injectLambdaContext({ flushBufferOnUncaughtError: false })
322+
async handler(_event: unknown, _context: Context) {
323+
logger.debug('This is a log message');
324+
logger.info('This is an info message');
325+
throw new Error('This is an error');
326+
}
327+
}
328+
const lambda = new TestClass();
329+
return lambda.handler.bind(lambda);
330+
},
331+
case: 'decorator',
332+
},
333+
])(
334+
'clears the buffer when an uncaught error is thrown and flushBufferOnUncaughtError is false ($case)',
335+
async ({ handlerFactory }) => {
336+
// Prepare
337+
const logger = new Logger({ logBufferOptions: { enabled: true } });
338+
const handler = handlerFactory(logger);
339+
340+
// Act & Assess
341+
await expect(() =>
342+
handler(
343+
{
344+
foo: 'bar',
345+
},
346+
context
347+
)
348+
).rejects.toThrow(new Error('This is an error'));
349+
350+
// Assess
351+
expect(console.debug).not.toBeCalled;
352+
expect(console.info).not.toBeCalled;
353+
}
354+
);
251355
});

0 commit comments

Comments
 (0)