3 const jsonReplacers
= require('./json-replacers');
4 const dataSanitizers
= require('./data-sanitizers');
6 const nop
= () => { /**/ };
10 * @typedef {Object} ConsoleLike
11 * @property {Function(...):void} error
12 * @property {Function(...):void} warn
13 * @property {Function(...):void} info
14 * @property {Function(...):void} log
15 * @property {Function(...):void} debug
18 * @typedef {Object} LoggerOptions
19 * @property {String} ignoreBelowLevel - minimum level to log, e.g. 'info'
22 * Wrap backend calls with payload normalization and metadata.
23 * @param {LoggerOptions} options
24 * @param {Object} commonObject - object to be merged into logged json
25 * @param {AsyncLocalStorage} asyncLocalStorage - async storage for an object to be merged into logged json
26 * @param {ConsoleLike} backend - default is console
28 constructor(options
, commonObject
, asyncLocalStorage
, backend
) {
29 const logLevels
= Object
.keys(Logger
.nullLogger
);
30 const ignoreBelowLevel
= options
?.ignoreBelowLevel
|| 'debug';
31 this.backend
= backend
|| console
;
32 this.commonObject
= commonObject
|| {};
33 this.jsonReplacers
= Object
.values(jsonReplacers
);
34 this.dataSanitizers
= Object
.values(dataSanitizers
);
36 if (asyncLocalStorage
) {
37 // Override the class getter.
38 Object
.defineProperty(this, 'asyncLogObject', {
40 get: asyncLocalStorage
.getStore
.bind(asyncLocalStorage
),
44 if (!logLevels
.includes(ignoreBelowLevel
)) {
45 throw new RangeError(`unrecognized minimum log level '${ignoreBelowLevel}'`);
47 const ignoreLevelIdx
= logLevels
.indexOf(ignoreBelowLevel
);
48 logLevels
.forEach((level
) => {
49 const includeBackendLevel
= (logLevels
.indexOf(level
) > ignoreLevelIdx
) && this.backend
[level
]; // eslint-disable-line security/detect-object-injection
50 // eslint-disable-next-line security/detect-object-injection
51 this[level
] = (includeBackendLevel
) ?
53 (...args
) => this.backend
[level
](this.payload(level
, ...args
)); // eslint-disable-line security/detect-object-injection
59 * All the expected Console levels, but do nothing.
60 * Ordered from highest priority to lowest.
62 static get nullLogger() {
74 * Default of empty object when no asyncLocalStorage is defined.
75 * Overridden on instance when asyncLocalStorage is defined.
77 // eslint-disable-next-line class-methods-use-this
78 get asyncLogObject() {
84 * Structure all expected log data into JSON string.
85 * @param {String} level
86 * @param {String} scope
87 * @param {String} message
88 * @param {Object} data
89 * @param {...any} other
90 * @returns {String} JSON string
92 payload(level
, scope
, message
, data
, ...other
) {
93 const replacer
= this.getReplacer();
95 if (this.sanitizationNeeded(data
)) {
96 // Create copy of data so we are not changing anything important.
97 data
= structuredClone(data
);
101 const now
= new Date();
103 ...this.commonObject
,
104 timestamp: now
.toISOString(),
105 timestampMs: now
.getTime(),
107 scope: scope
|| '[unknown]',
108 message: message
|| '',
110 ...(other
.length
&& { other
}),
111 ...this.asyncLogObject
,
113 return JSON
.stringify(logPayload
, replacer
);
118 * Determine if data needs sanitizing.
119 * @param {Object} data
122 sanitizationNeeded(data
) {
123 return this.dataSanitizers
.some((sanitizer
) => sanitizer(data
, false));
129 * @param {Object} data
132 this.dataSanitizers
.forEach((sanitizer
) => sanitizer(data
));
137 * Return a replacer function which does de-cycling, as well as the rest of our replacers.
140 const ancestors
= [];
141 const loggerReplacers
= this.jsonReplacers
;
142 return function cycleReplacer(key
, value
) {
143 loggerReplacers
.every((replacer
) => {
144 const oldValue
= value
;
145 value
= replacer(key
, value
);
146 return oldValue
=== value
;
148 if (typeof value
=== 'object' && value
!== null) {
149 // 'this' is object where key/value came from
150 while (ancestors
.length
> 0 && ancestors
.at(-1) !== this) {
153 if (ancestors
.includes(value
)) { // eslint-disable-line security/detect-object-injection
156 ancestors
.push(value
);
166 module
.exports
= Logger
;