Skip to content

Commit 8c7541a

Browse files
committed
feat: add a logUserErrorsAsWarnings option
This adds a `logUserErrorsAsWarnings` option which allows you to change the log level for handled errors. If this option is set to `true` then: * Errors with a `status` or `statusCode` property of `400`-`499` will be logged with a level of "warn" * All other errors will still be logged with a level of "error" I think in future we'll switch this to be the default behaviour and remove the option.
1 parent ebfe3ea commit 8c7541a

File tree

9 files changed

+165
-3
lines changed

9 files changed

+165
-3
lines changed

docs/getting-started/logging-errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ When logging errors it's important to consider the level of the log you send. Th
108108
}
109109
```
110110

111+
You may also want to differentiate between `4xx` and `5xx` errors by logging them with a level of "warn" or "error" respectively to indicate the different criticality of these types of errors.
112+
111113
* If an error is not recoverable at all and throws the app into an unstable state, e.g. an initial database connection cannot be established, then consider a level of "fatal" or "critical" if your logger supports it (currently [n-logger](https://github.com/Financial-Times/n-logger) does not support critical logs but we'll be investigating adding this in future).
112114

113115

packages/log-error/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A method to consistently log error object with optional request information. Thi
1111
* [`options.error`](#optionserror)
1212
* [`options.includeHeaders`](#optionsincludeheaders)
1313
* [`options.logger`](#optionslogger)
14+
* [`options.logUserErrorsAsWarnings`](#optionslogusererrorsaswarnings)
1415
* [`options.request`](#optionsrequest)
1516
* [Migrating](#migrating)
1617
* [Contributing](#contributing)
@@ -213,6 +214,15 @@ Though it's best if they can accept a single object and output results as JSON.
213214

214215
This option defaults to [Reliability Kit logger](https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/logger).
215216

217+
#### `options.logUserErrorsAsWarnings`
218+
219+
> [!NOTE]<br />
220+
> This option is only available in the [`logHandledError`](#loghandlederror) function.
221+
222+
A `boolean` indicating whether to log user errors (those with a `400``499` `status` property) with a level of `warn` rather than `error`. This helps to reduce the amount of error-level logs that you need to focus on.
223+
224+
This option defaults to `false`.
225+
216226
#### `options.request`
217227

218228
A request object (e.g. an instance of `Express.Request` or an object with `method` and `url` properties) to include alongside the error in the log. This will be [automatically serialized with `@dotcom-reliability-kit/serialize-request`](https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/serialize-request#readme).

packages/log-error/lib/index.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,22 @@ function extractErrorMessage(serializedError) {
8282
*
8383
* @type {typeof import('@dotcom-reliability-kit/log-error').logHandledError}
8484
*/
85-
function logHandledError({ error, includeHeaders, logger, request }) {
85+
function logHandledError({
86+
error,
87+
includeHeaders,
88+
logger,
89+
logUserErrorsAsWarnings = false,
90+
request
91+
}) {
8692
const serializedError = serializeError(error);
8793
logError({
8894
error: serializedError,
8995
event: 'HANDLED_ERROR',
9096
includeHeaders,
91-
level: 'error',
97+
level:
98+
logUserErrorsAsWarnings && isUserError(serializedError)
99+
? 'warn'
100+
: 'error',
92101
logger,
93102
request
94103
});
@@ -128,6 +137,18 @@ function logUnhandledError({ error, includeHeaders, logger, request }) {
128137
});
129138
}
130139

140+
/**
141+
* @param {SerializedError} error
142+
* @returns {boolean}
143+
*/
144+
function isUserError(error) {
145+
return (
146+
error.statusCode !== null &&
147+
error.statusCode >= 400 &&
148+
error.statusCode < 500
149+
);
150+
}
151+
131152
exports.logHandledError = logHandledError;
132153
exports.logRecoverableError = logRecoverableError;
133154
exports.logUnhandledError = logUnhandledError;

packages/log-error/test/unit/lib/index.spec.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,99 @@ describe('@dotcom-reliability-kit/log-error', () => {
293293
});
294294
});
295295

296+
describe('when the logUserErrorsAsWarnings option is set to true', () => {
297+
describe('and the serialized error has a status code in the 4xx range', () => {
298+
beforeEach(() => {
299+
serializeError.mockClear();
300+
serializeError.mockReturnValueOnce({
301+
name: 'MockError',
302+
message: 'mock error',
303+
statusCode: 456
304+
});
305+
logger.error.mockReset();
306+
logger.warn.mockReset();
307+
logHandledError({
308+
error,
309+
request,
310+
logUserErrorsAsWarnings: true
311+
});
312+
});
313+
314+
it('logs with a level of "warn" rather than "error"', () => {
315+
expect(logger.error).toHaveBeenCalledTimes(0);
316+
expect(logger.warn).toHaveBeenCalledWith(
317+
expect.objectContaining({
318+
error: {
319+
name: 'MockError',
320+
message: 'mock error',
321+
statusCode: 456
322+
}
323+
})
324+
);
325+
});
326+
});
327+
328+
describe('and the serialized error has a status code in the 5xx range', () => {
329+
beforeEach(() => {
330+
serializeError.mockClear();
331+
serializeError.mockReturnValueOnce({
332+
name: 'MockError',
333+
message: 'mock error',
334+
statusCode: 500
335+
});
336+
logger.error.mockReset();
337+
logger.warn.mockReset();
338+
logHandledError({
339+
error,
340+
request,
341+
logUserErrorsAsWarnings: true
342+
});
343+
});
344+
345+
it('still logs with a level of "error"', () => {
346+
expect(logger.warn).toHaveBeenCalledTimes(0);
347+
expect(logger.error).toHaveBeenCalledWith(
348+
expect.objectContaining({
349+
error: {
350+
name: 'MockError',
351+
message: 'mock error',
352+
statusCode: 500
353+
}
354+
})
355+
);
356+
});
357+
});
358+
359+
describe('and the serialized error does not have a status code', () => {
360+
beforeEach(() => {
361+
serializeError.mockClear();
362+
serializeError.mockReturnValueOnce({
363+
name: 'MockError',
364+
message: 'mock error'
365+
});
366+
logger.error.mockReset();
367+
logger.warn.mockReset();
368+
logHandledError({
369+
error,
370+
request,
371+
logUserErrorsAsWarnings: true
372+
});
373+
});
374+
375+
it('still logs with a level of "error"', () => {
376+
expect(logger.warn).toHaveBeenCalledTimes(0);
377+
expect(logger.error).toHaveBeenCalledWith(
378+
expect.objectContaining({
379+
error: {
380+
name: 'MockError',
381+
message: 'mock error'
382+
}
383+
})
384+
);
385+
});
386+
});
387+
});
388+
296389
describe('when logging fails', () => {
297390
let loggingError;
298391

packages/log-error/types/index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ declare module '@dotcom-reliability-kit/log-error' {
1616
request?: string | Request;
1717
};
1818

19-
export function logHandledError(options: ErrorLoggingOptions): void;
19+
export type HandledErrorLoggingOptions = ErrorLoggingOptions & {
20+
logUserErrorsAsWarnings?: boolean;
21+
};
22+
23+
export function logHandledError(options: HandledErrorLoggingOptions): void;
2024

2125
export function logRecoverableError(options: ErrorLoggingOptions): void;
2226

packages/middleware-log-errors/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Express middleware to consistently log errors. This module is part of [FT.com Re
99
* [`options.filter`](#optionsfilter)
1010
* [`options.includeHeaders`](#optionsincludeheaders)
1111
* [`options.logger`](#optionslogger)
12+
* [`options.logUserErrorsAsWarnings`](#optionslogusererrorsaswarnings)
1213
* [Migrating](#migrating)
1314
* [Contributing](#contributing)
1415
* [License](#license)
@@ -167,6 +168,12 @@ type LogMethod = (...logData: any) => any;
167168
168169
This is passed directly onto the relevant log-error method, [see the documentation for that package for more details](../log-error/README.md#optionslogger).
169170
171+
#### `options.logUserErrorsAsWarnings`
172+
173+
A `boolean` indicating whether to log user errors (those with a `400``499` `status` property) with a level of `warn` rather than `error`. This helps to reduce the amount of error-level logs that you need to focus on.
174+
175+
This option defaults to `false`.
176+
170177
171178
## Migrating
172179

packages/middleware-log-errors/lib/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const {
1515
* @returns {ExpressErrorHandler}
1616
*/
1717
function createErrorLoggingMiddleware(options = {}) {
18+
const { logUserErrorsAsWarnings } = options;
19+
1820
// Validate the included headers (this stops the request serializer from erroring
1921
// on every request if the included headers are invalid)
2022
const includeHeaders = options?.includeHeaders;
@@ -67,6 +69,7 @@ function createErrorLoggingMiddleware(options = {}) {
6769
error,
6870
includeHeaders,
6971
logger: options.logger,
72+
logUserErrorsAsWarnings,
7073
request
7174
});
7275
} catch (_) {

packages/middleware-log-errors/test/unit/lib/index.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,27 @@ describe('@dotcom-reliability-kit/middleware-log-errors', () => {
217217
});
218218
});
219219

220+
describe('when the logUserErrorsAsWarnings option is set', () => {
221+
beforeEach(() => {
222+
middleware = createErrorLoggingMiddleware({
223+
logUserErrorsAsWarnings: true
224+
});
225+
middleware(error, request, response, next);
226+
});
227+
228+
it('it passes the option down to the error logging function', () => {
229+
expect(logHandledError).toBeCalledWith({
230+
error,
231+
request,
232+
logUserErrorsAsWarnings: true
233+
});
234+
});
235+
236+
it('calls `next` with the original error', () => {
237+
expect(next).toBeCalledWith(error);
238+
});
239+
});
240+
220241
describe('when logging fails', () => {
221242
beforeEach(() => {
222243
logHandledError.mockImplementation(() => {

packages/middleware-log-errors/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ declare module '@dotcom-reliability-kit/middleware-log-errors' {
1111
filter?: ErrorLoggingFilter;
1212
includeHeaders?: string[];
1313
logger?: Logger & { [key: string]: any };
14+
logUserErrorsAsWarnings?: boolean;
1415
};
1516

1617
declare function createErrorLoggingMiddleware(

0 commit comments

Comments
 (0)