32d9d0503331330612db821e5219b700dbd9f690
3 const jsonReplacers
= require('./json-replacers');
4 const dataSanitizers
= require('./data-sanitizers');
6 const nop
= () => undefined;
11 * @param {any} ...args values to log
12 * @returns {void} nothing
15 * @typedef {object} ConsoleLike
16 * @property {LogFn} error error level
17 * @property {LogFn} warn warn level
18 * @property {LogFn} info info level
19 * @property {LogFn} log log level
20 * @property {LogFn} debug debug level
23 * @typedef AsyncLocalStorage
24 * @property {Function} getStore local storage
27 * @typedef {object} LoggerOptions
28 * @property {string} ignoreBelowLevel minimum level to log, e.g. 'info'
31 * Wrap backend calls with payload normalization and metadata.
32 * @param {LoggerOptions} options options
33 * @param {object} commonObject object to be merged into logged json
34 * @param {AsyncLocalStorage} asyncLocalStorage async storage for an object to be merged into logged json
35 * @param {ConsoleLike} backend default is console
37 constructor(options
, commonObject
, asyncLocalStorage
, backend
) {
38 const logLevels
= Object
.keys(Logger
.nullLogger
);
39 const ignoreBelowLevel
= options
?.ignoreBelowLevel
|| 'debug';
40 this.backend
= backend
|| console
;
41 this.commonObject
= commonObject
|| {};
42 this.jsonReplacers
= Object
.values(jsonReplacers
);
43 this.dataSanitizers
= Object
.values(dataSanitizers
);
45 if (asyncLocalStorage
) {
46 // Override the class getter.
47 Object
.defineProperty(this, 'asyncLogObject', {
49 get: asyncLocalStorage
.getStore
.bind(asyncLocalStorage
),
53 if (!logLevels
.includes(ignoreBelowLevel
)) {
54 throw new RangeError(`unrecognized minimum log level '${ignoreBelowLevel}'`);
56 const ignoreLevelIdx
= logLevels
.indexOf(ignoreBelowLevel
);
57 logLevels
.forEach((level
) => {
58 const includeBackendLevel
= (logLevels
.indexOf(level
) > ignoreLevelIdx
) && this.backend
[level
]; // eslint-disable-line security/detect-object-injection
59 // eslint-disable-next-line security/detect-object-injection
60 this[level
] = (includeBackendLevel
) ?
62 (...args
) => this.backend
[level
](this.payload(level
, ...args
)); // eslint-disable-line security/detect-object-injection
68 * All the expected Console levels, but do nothing.
69 * Ordered from highest priority to lowest.
70 * @returns {object} no-op backend
72 static get nullLogger() {
84 * Default of empty object when no asyncLocalStorage is defined.
85 * Overridden on instance when asyncLocalStorage is defined.
86 * @returns {object} empty
88 // eslint-disable-next-line class-methods-use-this
89 get asyncLogObject() {
95 * Structure all expected log data into JSON string.
96 * @param {string} level log level
97 * @param {string} scope scope
98 * @param {string} message message
99 * @param {object} data data
100 * @param {...any} other more data merged into payload
101 * @returns {string} JSON string
103 payload(level
, scope
, message
, data
, ...other
) {
104 const replacer
= this.getReplacer();
106 if (this.sanitizationNeeded(data
)) {
107 // Create copy of data so we are not changing anything important.
109 data
= structuredClone(data
);
110 } catch (e
) { // eslint-disable-line no-unused-vars
111 data
= JSON
.parse(JSON
.stringify(data
, replacer
));
116 const now
= new Date();
118 ...this.commonObject
,
119 timestamp: now
.toISOString(),
120 timestampMs: now
.getTime(),
122 scope: scope
|| '[unknown]',
123 message: message
|| '',
125 ...(other
.length
&& { other
}),
126 ...this.asyncLogObject
,
128 return JSON
.stringify(logPayload
, replacer
);
133 * Determine if data needs sanitizing.
134 * @param {object} data data
135 * @returns {boolean} needs sanitization
137 sanitizationNeeded(data
) {
138 return this.dataSanitizers
.some((sanitizer
) => sanitizer(data
, false));
144 * @param {object} data data
147 this.dataSanitizers
.forEach((sanitizer
) => sanitizer(data
));
152 * Return a replacer function which does de-cycling, as well as the rest of our replacers.
153 * @returns {Function} replacer
156 const ancestors
= [];
157 const loggerReplacers
= this.jsonReplacers
;
158 return function cycleReplacer(key
, value
) {
159 loggerReplacers
.every((replacer
) => {
160 const oldValue
= value
;
161 value
= replacer(key
, value
);
162 return oldValue
=== value
;
164 if (typeof value
=== 'object' && value
!== null) {
165 // 'this' is object where key/value came from
166 while (ancestors
.length
> 0 && ancestors
.at(-1) !== this) {
169 if (ancestors
.includes(value
)) {
172 ancestors
.push(value
);
182 module
.exports
= Logger
;