Skip to content

Commit ad5a337

Browse files
DIME-8714 stacktrace now contains the recursive cause in one string (#76)
Duplicated lines in stack traces are omitted and the whole string is truncated
1 parent fbd47a8 commit ad5a337

File tree

4 files changed

+103
-39
lines changed

4 files changed

+103
-39
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ The separate steps of the logging process can be configured here.
193193
These modifications affect all the instances of the library.
194194
With transformers we can alter the data to be logged before passing to the formatter and then to the output.
195195
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.
196+
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
197+
with a significantly improved truncation limit.
196198

197199
```javascript
198200
const { createLogger } = require('@emartech/json-logger');
@@ -201,7 +203,8 @@ createLogger.configure({
201203
formatter: JSON.stringify,
202204
output: console.log,
203205
transformers: [],
204-
outputFormat: 'ecs'
206+
outputFormat: 'ecs',
207+
enhancedStackTrace: true
205208
});
206209

207210
```

package-lock.json

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/logger/logger.spec.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ describe('Logger', () => {
250250
expect(logArguments.http.response.body.content).to.eql(JSON.stringify(error.response?.data));
251251
});
252252

253-
it('should log causes', () => {
253+
it('should include error causes in stack trace when enhanced stack trace is enabled', () => {
254254
const rootCause = new Error('Root cause error');
255255
rootCause.name = 'RootCauseError';
256256
rootCause.stack = 'RootCauseError: Root cause error\n at rootFunction()';
@@ -265,21 +265,16 @@ describe('Logger', () => {
265265
mainError.stack = 'MainError: Main error occurred\n at mainFunction()';
266266
mainError.cause = intermediateError;
267267

268+
Logger.configure({ enhancedStackTrace: true });
269+
268270
logger.fromError('test_action', mainError, { details: 'test details' });
269271

270272
const logArguments = JSON.parse(outputStub.args[0][0]);
271273
expect(logArguments.error.type).to.eql('MainError');
272274
expect(logArguments.error.message).to.eql('Main error occurred');
273-
expect(logArguments.error.cause).to.eql({
274-
type: 'IntermediateError',
275-
message: 'Intermediate error',
276-
stack_trace: intermediateError.stack,
277-
cause: {
278-
type: 'RootCauseError',
279-
message: 'Root cause error',
280-
stack_trace: rootCause.stack,
281-
},
282-
});
275+
expect(logArguments.error.stack_trace).to.eql(
276+
[mainError.stack, 'Caused by: ' + intermediateError.stack, 'Caused by: ' + rootCause.stack].join('\n'),
277+
);
283278
});
284279

285280
describe('#customError', () => {
@@ -342,9 +337,9 @@ describe('Logger', () => {
342337
});
343338
});
344339

345-
it('should log error properties from custom error object', () => {
340+
it('should log error properties from custom error object with normal stack trace', () => {
346341
const errorObject = { name: 'Error', message: 'My custom error', stack: 'Stack', data: { value: 1 } };
347-
342+
Logger.configure({ enhancedStackTrace: false });
348343
logger.customError('error', 'hi', errorObject, { details: 'here' });
349344

350345
const logArguments = JSON.parse(outputStub.args[0][0]);
@@ -355,6 +350,20 @@ describe('Logger', () => {
355350
expect(logArguments.error.context).to.eql(JSON.stringify(errorObject.data));
356351
});
357352

353+
it('should log error properties from custom error object with enhanced stack trace', () => {
354+
const errorObject = { name: 'Error', message: 'My custom error', stack: 'Stack', data: { value: 1 } };
355+
356+
Logger.configure({ enhancedStackTrace: true });
357+
logger.customError('error', 'hi', errorObject, { details: 'here' });
358+
359+
const logArguments = JSON.parse(outputStub.args[0][0]);
360+
361+
expect(logArguments.error.type).to.eql(errorObject.name);
362+
expect(logArguments.error.stack_trace).to.eql([errorObject.name, errorObject.stack].join('\n'));
363+
expect(logArguments.error.message).to.eql(errorObject.message);
364+
expect(logArguments.error.context).to.eql(JSON.stringify(errorObject.data));
365+
});
366+
358367
it('should not log additional or missing error properties from custom error object', () => {
359368
const errorObject = { color: 'color', value: 'value' };
360369

@@ -414,7 +423,7 @@ describe('Logger', () => {
414423
throw new Error('should throw');
415424
} catch (e) {
416425
expect((e as Error).message).to.eql(
417-
'Only the following keys are allowed: output,formatter,transformers,outputFormat',
426+
'Only the following keys are allowed: output,formatter,transformers,outputFormat,enhancedStackTrace',
418427
);
419428
}
420429
});

src/logger/logger.ts

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { consoleOutput } from '../output/console';
55
import { Timer } from '../timer/timer';
66

77
const STACK_TRACE_LIMIT = 3000;
8+
const ENHANCED_STACK_TRACE_LIMIT = 15000;
89
const DATA_LIMIT = 3000;
9-
const allowedKeys = ['output', 'formatter', 'transformers', 'outputFormat'];
10+
const allowedKeys = ['output', 'formatter', 'transformers', 'outputFormat', 'enhancedStackTrace'];
1011

1112
interface ErrorWithData extends Error {
1213
data: unknown;
@@ -32,6 +33,7 @@ export interface LoggerConfig {
3233
output: Function;
3334
transformers: Function[];
3435
outputFormat: string;
36+
enhancedStackTrace: boolean;
3537
}
3638
/* eslint-enable @typescript-eslint/no-unsafe-function-type */
3739

@@ -62,6 +64,7 @@ export class Logger {
6264
output: consoleOutput,
6365
transformers: [],
6466
outputFormat: 'ecs',
67+
enhancedStackTrace: false,
6568
};
6669

6770
isEnabled() {
@@ -171,32 +174,94 @@ export class Logger {
171174
}
172175

173176
private getBaseErrorDetails(error: Error) {
177+
const shortenedData = this.shortenData((error as ErrorWithData).data);
178+
174179
if (Logger.config.outputFormat === 'legacy') {
175180
return {
176181
error_name: error.name,
177182
error_stack: this.shortenStackTrace(error.stack || ''),
178183
error_message: error.message,
179-
error_data: this.shortenData((error as ErrorWithData).data),
184+
error_data: shortenedData,
180185
};
181186
}
182187

188+
const stackTrace = Logger.config.enhancedStackTrace
189+
? this.getEnhancedStackTrace(error)
190+
: this.shortenStackTrace(error.stack || '');
191+
183192
return {
184-
error: this.extractError(error),
193+
error: {
194+
type: error.name,
195+
message: error.message,
196+
...(shortenedData && { context: shortenedData }),
197+
stack_trace: stackTrace,
198+
},
185199
event: {
186200
reason: error.message,
187201
},
188202
};
189203
}
190204

191-
private extractError(error: Error): unknown {
192-
const shortenedData = this.shortenData((error as ErrorWithData).data);
193-
return {
194-
type: error.name,
195-
message: error.message,
196-
...(shortenedData && { context: shortenedData }),
197-
stack_trace: this.shortenStackTrace(error.stack || ''),
198-
...(error.cause instanceof Error && { cause: this.extractError(error.cause) }),
205+
private getEnhancedStackTrace(error: Error): string {
206+
const getNumberOfCommonFrames = (ownTrace: string[], enclosingTrace: string[]): number => {
207+
let m = ownTrace.length - 1;
208+
let n = enclosingTrace.length - 1;
209+
210+
while (m > 0 && n > 0 && ownTrace[m] === enclosingTrace[n]) {
211+
m--;
212+
n--;
213+
}
214+
return ownTrace.length - 1 - m;
215+
};
216+
217+
const getEnclosedStackTrace = (error: Error, enclosingTrace: string[], caption: string): string[] => {
218+
const output: string[] = [];
219+
220+
let errorName: string;
221+
let errorStack: string[];
222+
const errorStackLines = error.stack ? error.stack.split('\n') : [];
223+
const firstLine = errorStackLines.at(0);
224+
225+
if (error.stack) {
226+
if (firstLine?.includes(error.name)) {
227+
errorName = firstLine!;
228+
errorStack = errorStackLines.slice(1);
229+
} else {
230+
errorName = error.name;
231+
errorStack = errorStackLines;
232+
}
233+
} else {
234+
errorName = error.name;
235+
errorStack = [];
236+
}
237+
238+
const commonFrames = getNumberOfCommonFrames(errorStack, enclosingTrace);
239+
const uniqueFrames = errorStack.length - commonFrames;
240+
241+
output.push(caption + errorName);
242+
errorStack.slice(0, uniqueFrames).forEach((line) => output.push(line));
243+
if (commonFrames > 0) {
244+
output.push(`\t... ${commonFrames} more`);
245+
}
246+
247+
if (error.cause instanceof Error) {
248+
output.push(...getEnclosedStackTrace(error.cause, errorStackLines, 'Caused by: '));
249+
}
250+
251+
return output;
199252
};
253+
254+
const stackTrace = getEnclosedStackTrace(error, [], '');
255+
const joinedStackTrace = stackTrace.join('\n');
256+
let resultStackTraceStr: string;
257+
258+
if (joinedStackTrace.length > ENHANCED_STACK_TRACE_LIMIT) {
259+
resultStackTraceStr = joinedStackTrace.substring(0, ENHANCED_STACK_TRACE_LIMIT) + ' ...';
260+
} else {
261+
resultStackTraceStr = joinedStackTrace;
262+
}
263+
264+
return resultStackTraceStr;
200265
}
201266

202267
private getAxiosErrorDetails(error: AxiosError) {

0 commit comments

Comments
 (0)