diff --git a/README.md b/README.md index 0ec2a7f..e88481d 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ The separate steps of the logging process can be configured here. These modifications affect all the instances of the library. With transformers we can alter the data to be logged before passing to the formatter and then to the output. It is a perfect place to add the name of the machine is running on or the request id associated with the current thread stored on a continuation local storage. +With the 'enhancedStackTrace' option set to true, the error stack traces will include the causes of the errors as well (if any) including their stack traces +with a significantly improved truncation limit. ```javascript const { createLogger } = require('@emartech/json-logger'); @@ -201,7 +203,8 @@ createLogger.configure({ formatter: JSON.stringify, output: console.log, transformers: [], - outputFormat: 'ecs' + outputFormat: 'ecs', + enhancedStackTrace: true }); ``` diff --git a/package-lock.json b/package-lock.json index 19a90c9..78dd468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -437,7 +437,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -930,7 +929,6 @@ "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1006,7 +1004,6 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -1212,7 +1209,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1480,7 +1476,6 @@ "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2425,7 +2420,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2528,7 +2522,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3960,7 +3953,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -6757,7 +6749,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7406,7 +7397,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7993,7 +7983,6 @@ "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", @@ -8463,7 +8452,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8606,7 +8594,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/logger/logger.spec.ts b/src/logger/logger.spec.ts index 0990bee..37d0a79 100644 --- a/src/logger/logger.spec.ts +++ b/src/logger/logger.spec.ts @@ -250,7 +250,7 @@ describe('Logger', () => { expect(logArguments.http.response.body.content).to.eql(JSON.stringify(error.response?.data)); }); - it('should log causes', () => { + it('should include error causes in stack trace when enhanced stack trace is enabled', () => { const rootCause = new Error('Root cause error'); rootCause.name = 'RootCauseError'; rootCause.stack = 'RootCauseError: Root cause error\n at rootFunction()'; @@ -265,21 +265,16 @@ describe('Logger', () => { mainError.stack = 'MainError: Main error occurred\n at mainFunction()'; mainError.cause = intermediateError; + Logger.configure({ enhancedStackTrace: true }); + logger.fromError('test_action', mainError, { details: 'test details' }); const logArguments = JSON.parse(outputStub.args[0][0]); expect(logArguments.error.type).to.eql('MainError'); expect(logArguments.error.message).to.eql('Main error occurred'); - expect(logArguments.error.cause).to.eql({ - type: 'IntermediateError', - message: 'Intermediate error', - stack_trace: intermediateError.stack, - cause: { - type: 'RootCauseError', - message: 'Root cause error', - stack_trace: rootCause.stack, - }, - }); + expect(logArguments.error.stack_trace).to.eql( + [mainError.stack, 'Caused by: ' + intermediateError.stack, 'Caused by: ' + rootCause.stack].join('\n'), + ); }); describe('#customError', () => { @@ -342,9 +337,9 @@ describe('Logger', () => { }); }); - it('should log error properties from custom error object', () => { + it('should log error properties from custom error object with normal stack trace', () => { const errorObject = { name: 'Error', message: 'My custom error', stack: 'Stack', data: { value: 1 } }; - + Logger.configure({ enhancedStackTrace: false }); logger.customError('error', 'hi', errorObject, { details: 'here' }); const logArguments = JSON.parse(outputStub.args[0][0]); @@ -355,6 +350,20 @@ describe('Logger', () => { expect(logArguments.error.context).to.eql(JSON.stringify(errorObject.data)); }); + it('should log error properties from custom error object with enhanced stack trace', () => { + const errorObject = { name: 'Error', message: 'My custom error', stack: 'Stack', data: { value: 1 } }; + + Logger.configure({ enhancedStackTrace: true }); + logger.customError('error', 'hi', errorObject, { details: 'here' }); + + const logArguments = JSON.parse(outputStub.args[0][0]); + + expect(logArguments.error.type).to.eql(errorObject.name); + expect(logArguments.error.stack_trace).to.eql([errorObject.name, errorObject.stack].join('\n')); + expect(logArguments.error.message).to.eql(errorObject.message); + expect(logArguments.error.context).to.eql(JSON.stringify(errorObject.data)); + }); + it('should not log additional or missing error properties from custom error object', () => { const errorObject = { color: 'color', value: 'value' }; @@ -414,7 +423,7 @@ describe('Logger', () => { throw new Error('should throw'); } catch (e) { expect((e as Error).message).to.eql( - 'Only the following keys are allowed: output,formatter,transformers,outputFormat', + 'Only the following keys are allowed: output,formatter,transformers,outputFormat,enhancedStackTrace', ); } }); diff --git a/src/logger/logger.ts b/src/logger/logger.ts index 31d33c5..077ce4a 100644 --- a/src/logger/logger.ts +++ b/src/logger/logger.ts @@ -5,8 +5,9 @@ import { consoleOutput } from '../output/console'; import { Timer } from '../timer/timer'; const STACK_TRACE_LIMIT = 3000; +const ENHANCED_STACK_TRACE_LIMIT = 15000; const DATA_LIMIT = 3000; -const allowedKeys = ['output', 'formatter', 'transformers', 'outputFormat']; +const allowedKeys = ['output', 'formatter', 'transformers', 'outputFormat', 'enhancedStackTrace']; interface ErrorWithData extends Error { data: unknown; @@ -32,6 +33,7 @@ export interface LoggerConfig { output: Function; transformers: Function[]; outputFormat: string; + enhancedStackTrace: boolean; } /* eslint-enable @typescript-eslint/no-unsafe-function-type */ @@ -62,6 +64,7 @@ export class Logger { output: consoleOutput, transformers: [], outputFormat: 'ecs', + enhancedStackTrace: false, }; isEnabled() { @@ -171,32 +174,94 @@ export class Logger { } private getBaseErrorDetails(error: Error) { + const shortenedData = this.shortenData((error as ErrorWithData).data); + if (Logger.config.outputFormat === 'legacy') { return { error_name: error.name, error_stack: this.shortenStackTrace(error.stack || ''), error_message: error.message, - error_data: this.shortenData((error as ErrorWithData).data), + error_data: shortenedData, }; } + const stackTrace = Logger.config.enhancedStackTrace + ? this.getEnhancedStackTrace(error) + : this.shortenStackTrace(error.stack || ''); + return { - error: this.extractError(error), + error: { + type: error.name, + message: error.message, + ...(shortenedData && { context: shortenedData }), + stack_trace: stackTrace, + }, event: { reason: error.message, }, }; } - private extractError(error: Error): unknown { - const shortenedData = this.shortenData((error as ErrorWithData).data); - return { - type: error.name, - message: error.message, - ...(shortenedData && { context: shortenedData }), - stack_trace: this.shortenStackTrace(error.stack || ''), - ...(error.cause instanceof Error && { cause: this.extractError(error.cause) }), + private getEnhancedStackTrace(error: Error): string { + const getNumberOfCommonFrames = (ownTrace: string[], enclosingTrace: string[]): number => { + let m = ownTrace.length - 1; + let n = enclosingTrace.length - 1; + + while (m > 0 && n > 0 && ownTrace[m] === enclosingTrace[n]) { + m--; + n--; + } + return ownTrace.length - 1 - m; + }; + + const getEnclosedStackTrace = (error: Error, enclosingTrace: string[], caption: string): string[] => { + const output: string[] = []; + + let errorName: string; + let errorStack: string[]; + const errorStackLines = error.stack ? error.stack.split('\n') : []; + const firstLine = errorStackLines.at(0); + + if (error.stack) { + if (firstLine?.includes(error.name)) { + errorName = firstLine!; + errorStack = errorStackLines.slice(1); + } else { + errorName = error.name; + errorStack = errorStackLines; + } + } else { + errorName = error.name; + errorStack = []; + } + + const commonFrames = getNumberOfCommonFrames(errorStack, enclosingTrace); + const uniqueFrames = errorStack.length - commonFrames; + + output.push(caption + errorName); + errorStack.slice(0, uniqueFrames).forEach((line) => output.push(line)); + if (commonFrames > 0) { + output.push(`\t... ${commonFrames} more`); + } + + if (error.cause instanceof Error) { + output.push(...getEnclosedStackTrace(error.cause, errorStackLines, 'Caused by: ')); + } + + return output; }; + + const stackTrace = getEnclosedStackTrace(error, [], ''); + const joinedStackTrace = stackTrace.join('\n'); + let resultStackTraceStr: string; + + if (joinedStackTrace.length > ENHANCED_STACK_TRACE_LIMIT) { + resultStackTraceStr = joinedStackTrace.substring(0, ENHANCED_STACK_TRACE_LIMIT) + ' ...'; + } else { + resultStackTraceStr = joinedStackTrace; + } + + return resultStackTraceStr; } private getAxiosErrorDetails(error: AxiosError) {