Fix: add stack trace errors in logging for prod

This commit is contained in:
Joel Biddle
2023-10-09 13:37:16 +00:00
parent bd8c17d720
commit b6ef55783e
3 changed files with 50 additions and 32 deletions
+1
View File
@@ -26,6 +26,7 @@
], ],
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [ "unused-imports/no-unused-vars": [
"warn", "warn",
{ {
+24 -30
View File
@@ -3,7 +3,7 @@ import { ErrorRequestHandler } from "express";
import { TokenExpiredError } from "jsonwebtoken"; import { TokenExpiredError } from "jsonwebtoken";
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { getLogger } from "../utils/logger"; import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError"; import RequestError, { LogLevel, mapToWinstonLogLevel } from "../utils/requestError";
import { getNodeEnv } from "../config"; import { getNodeEnv } from "../config";
export const requestErrorHandler: ErrorRequestHandler = async ( export const requestErrorHandler: ErrorRequestHandler = async (
@@ -14,37 +14,31 @@ export const requestErrorHandler: ErrorRequestHandler = async (
) => { ) => {
if (res.headersSent) return next(); if (res.headersSent) return next();
if (await getNodeEnv() !== "production") { const logAndCaptureException = async (error: RequestError, logLevel: LogLevel) => {
/* eslint-disable no-console */ // log stack trace & error message to the console using Winston
console.error(error); (await getLogger("backend-main")).log(mapToWinstonLogLevel(logLevel), `${error.stack}\n${error.message}`);
}
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError //* Set Sentry user identification if req.user is populated
if (error instanceof TokenExpiredError) { if (req.user !== undefined && req.user !== null) {
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" }); Sentry.setUser({ email: (req.user as any).email });
} else if (!(error instanceof RequestError)) { }
error = InternalServerError({
context: { exception: error.message },
stack: error.stack,
});
(await getLogger("backend-main")).log(
(<RequestError>error).levelName.toLowerCase(),
(<RequestError>error).message
);
}
//* Set Sentry user identification if req.user is populated // eliminate false-positive errors being sent to Sentry
if (req.user !== undefined && req.user !== null) { if ("level" in error && [LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes(error.level)) {
Sentry.setUser({ email: (req.user as any).email }); Sentry.captureException(error);
} }
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' };
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if ( if (error instanceof RequestError || error instanceof TokenExpiredError) {
[LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes( if (error instanceof TokenExpiredError) {
(<RequestError>error).level error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" }) as RequestError;
) }
) {
Sentry.captureException(error); logAndCaptureException((<RequestError>error), LogLevel.INFO);
} else {
// For unexpected errors, throw a 500 error & ensure these are sent to Sentry in prod
error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) as RequestError;
logAndCaptureException((<RequestError>error), (await getNodeEnv() === "production") ? LogLevel.ERROR : LogLevel.DEBUG);
} }
res res
+25 -2
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extra-semi */
import { Request } from "express" import { Request } from "express"
import { getVerboseErrorOutput } from "../config"; import { getVerboseErrorOutput } from "../config";
@@ -12,6 +13,27 @@ export enum LogLevel {
EMERGENCY = 600, EMERGENCY = 600,
} }
export const mapToWinstonLogLevel = (customLogLevel: LogLevel): string => {
switch (customLogLevel) {
case LogLevel.DEBUG:
return "debug";
case LogLevel.INFO:
return "info";
case LogLevel.NOTICE:
return "notice";
case LogLevel.WARNING:
return "warn";
case LogLevel.ERROR:
return "error";
case LogLevel.CRITICAL:
return "crit";
case LogLevel.ALERT:
return "alert";
case LogLevel.EMERGENCY:
return "emerg";
}
}
export type RequestErrorContext = { export type RequestErrorContext = {
logLevel?: LogLevel, logLevel?: LogLevel,
statusCode: number, statusCode: number,
@@ -87,7 +109,8 @@ export default class RequestError extends Error{
}, this.context) }, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer //* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!(await getVerboseErrorOutput())){ const verboseErrorOutput = await getVerboseErrorOutput();
if (verboseErrorOutput !== undefined) {
_context = this._omit(_context, [ _context = this._omit(_context, [
"stacktrace", "stacktrace",
"exception", "exception",
@@ -110,4 +133,4 @@ export default class RequestError extends Error{
return formatObject return formatObject
} }
} }