Skip to content

Commit

Permalink
feat: filterable error logging (#69)
Browse files Browse the repository at this point in the history
- allow logging only specific types of errors
- make exposing stack trace in response body more flexible
  • Loading branch information
domeq authored Mar 16, 2022
1 parent 518de1b commit ad8a9c5
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 10 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ npm install --save @schibsted/middy-error-handler

- `logger` (defaults to `console`) - a logging function that is invoked with the current error as an argument. You can pass `false` if you don't want the logging to happen.
- `level` (defaults to `error`) - log level to use for the error log entry
- `exposeStackTrace` (defaults to `false`) - if `true`, the stack trace will be exposed in the response body
- `filter` (function, defaults to always returning `true`) - a function that is invoked with the current error as an argument. If it returns `true`, the error is logged and its stack trace returned as long as `exposeStackTrace` is also true, otherwise it is not.

## Sample usage

Expand All @@ -43,14 +45,17 @@ const handler = middy(() => {
throw new createError.NotFound('File not found');
});

handler.use(errorHandler());
handler.use(errorHandler({
exposeStackTrace: process.env !== 'production', # don't return stack trace in response body in production
filter: (err) => err.statusCode !== 404, # don't log 404 errors, they happen a lot
}));

handler({}, {}).then((response) => {
console.log(response);

// {
// statusCode: 404,
// body: '{"statusCode":404,"message":"File not found","stack":"NotFoundError: File not found\\n at /Users/wojciechiskra/Code/pent/pent-api/test.js:11:11\\n at runRequest (/Users/wojciechiskra/Code/pent/pent-api/node_modules/@middy/core/index.js:86:32)"}'
// body: '{"statusCode":404,"message":"File not found","stack":"..."}'
// stack: '...'
// }
});
Expand Down
6 changes: 3 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const R = require('ramda');

module.exports = ({ logger = console, level = 'error' } = {}) => ({
module.exports = ({ logger = console, level = 'error', exposeStackTrace = false, filter = () => true } = {}) => ({
onError: async (handler) => {
const { error } = handler;

// if there are a `statusCode` and an `error` field
// this is a valid http error object
if (typeof logger[level] === 'function') {
if (filter(error) && typeof logger[level] === 'function') {
logger[level](
{
error: R.pick(['name', 'message', 'stack', 'details', 'status', 'statusCode', 'expose'], error),
Expand All @@ -24,7 +24,7 @@ module.exports = ({ logger = console, level = 'error' } = {}) => ({
statusCode: R.prop('statusCode', error),
message: R.prop('message', error),
details: R.prop('details', error),
stack: !['test', 'production'].includes(process.env.NODE_ENV) && R.prop('stack', error),
stack: exposeStackTrace && filter(error) && R.prop('stack', error),
})
),
};
Expand Down
42 changes: 37 additions & 5 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test('Middleware works with logging disabled', async () => {
});
});

test('Middleware returns error details', async () => {
test("Middleware doesn't log 404 errors - they happen a lot", async () => {
const mockLogger = {
error: jest.fn(() => {}),
};
Expand All @@ -26,14 +26,46 @@ test('Middleware returns error details', async () => {
throw new createError.NotFound('File not found');
});

handler.use(middleware({ logger: mockLogger }));
handler.use(middleware({ logger: mockLogger, filter: (err) => err.statusCode >= 500 }));

await expect(handler({}, {})).resolves.toMatchObject({
body: JSON.stringify({ statusCode: 404, message: 'File not found' }),
statusCode: 404,
const response = await handler({}, {});
const { statusCode, message, stack } = JSON.parse(response.body);

expect(statusCode).toBe(404);
expect(message).toBe('File not found');
expect(stack).toBeUndefined();

expect(mockLogger.error).toHaveBeenCalledTimes(0);
});

test('Middleware logs and returns all error details', async () => {
const mockLogger = {
error: jest.fn(() => {}),
};

const handler = middy(() => {
throw new createError.ServiceUnavailable('Service not available');
});

handler.use(middleware({ logger: mockLogger, exposeStackTrace: true }));

const response = await handler({}, {});
const { statusCode, message, stack } = JSON.parse(response.body);

expect(statusCode).toBe(503);
expect(message).toBe('Service not available');
expect(stack).toBeDefined();

expect(mockLogger.error).toHaveBeenCalledTimes(1);

const [errorObject, errorMessage] = mockLogger.error.mock.calls[mockLogger.error.mock.calls.length - 1];
expect(errorMessage).toBe('ServiceUnavailableError: Service not available');
expect(errorObject.error.name).toBe('ServiceUnavailableError');
expect(errorObject.error.message).toBe('Service not available');
expect(errorObject.error.status).toBe(503);
expect(errorObject.error.statusCode).toBe(503);
expect(errorObject.error.expose).toBe(false);
expect(errorObject.error.stack).not.toBeNull();
});

test('Keep data already present in response', async () => {
Expand Down

0 comments on commit ad8a9c5

Please sign in to comment.