@zenvia/logger 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/logger.js +219 -56
- package/test/lib/logger.spec.js +384 -19
package/package.json
CHANGED
package/src/lib/logger.js
CHANGED
|
@@ -29,7 +29,9 @@ const customFormatJson = winston.format((info) => {
|
|
|
29
29
|
environment: process.env.NODE_ENV,
|
|
30
30
|
host: process.env.HOST || process.env.HOSTNAME,
|
|
31
31
|
message: info.message || '',
|
|
32
|
-
level: ['verbose', 'silly'].includes(info.level)
|
|
32
|
+
level: ['verbose', 'silly'].includes(info.level)
|
|
33
|
+
? 'DEBUG'
|
|
34
|
+
: info.level.toUpperCase(),
|
|
33
35
|
stack_trace: stack,
|
|
34
36
|
traceId: rTrace.id(),
|
|
35
37
|
};
|
|
@@ -51,7 +53,9 @@ const customCombineJson = winston.format.combine(
|
|
|
51
53
|
|
|
52
54
|
const customCombineSimple = winston.format.combine(
|
|
53
55
|
winston.format.timestamp(),
|
|
54
|
-
winston.format.printf(
|
|
56
|
+
winston.format.printf(
|
|
57
|
+
(info) => `${info.timestamp} - ${info.level}: ${info.message}`,
|
|
58
|
+
),
|
|
55
59
|
);
|
|
56
60
|
|
|
57
61
|
const createConsoleTransport = () => new winston.transports.Console({
|
|
@@ -70,75 +74,234 @@ const baseLogger = winston.createLogger({
|
|
|
70
74
|
},
|
|
71
75
|
level: process.env.LOGGING_LEVEL || 'debug',
|
|
72
76
|
handleExceptions: true,
|
|
73
|
-
format:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
format:
|
|
78
|
+
process.env.LOGGING_FORMATTER_DISABLED
|
|
79
|
+
&& process.env.LOGGING_FORMATTER_DISABLED === 'true'
|
|
80
|
+
? customCombineSimple
|
|
81
|
+
: customCombineJson,
|
|
82
|
+
transports: [createConsoleTransport()],
|
|
77
83
|
exitOnError: false,
|
|
78
84
|
});
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
// HYBRID APPROACH: Monkey-Patching with Structured Functions
|
|
87
|
+
//
|
|
88
|
+
// Architecture: We use monkey-patching for backward compatibility (ensures
|
|
89
|
+
// `logger instanceof winston.Logger` === true), but extract all logic into
|
|
90
|
+
// isolated, testable functions (Wrapper pattern structure).
|
|
91
|
+
//
|
|
92
|
+
// This provides:
|
|
93
|
+
// - Full backward compatibility (exports real Winston instance)
|
|
94
|
+
// - High maintainability (isolated, testable functions)
|
|
95
|
+
// - Easy future migration to pure Wrapper pattern in major versions
|
|
96
|
+
|
|
97
|
+
// Preserve original log method before patching
|
|
98
|
+
const originalLog = baseLogger.log.bind(baseLogger);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Determines if a log call should be silenced based on arguments.
|
|
102
|
+
*
|
|
103
|
+
* @param {Array} args - The arguments passed to the log function
|
|
104
|
+
* @returns {boolean} - true if should log, false if should silence
|
|
105
|
+
*/
|
|
106
|
+
function shouldLog(args) {
|
|
107
|
+
// Case 1: No arguments - silence
|
|
108
|
+
if (args.length === 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Case 2: Single string argument that's just a level - silence
|
|
113
|
+
if (
|
|
114
|
+
args.length === 1
|
|
115
|
+
&& typeof args[0] === 'string'
|
|
116
|
+
&& !args[0].includes(' ')
|
|
117
|
+
) {
|
|
118
|
+
const possibleLevel = args[0].toLowerCase();
|
|
119
|
+
if (
|
|
120
|
+
['fatal', 'error', 'warn', 'info', 'debug', 'verbose', 'silly'].includes(
|
|
121
|
+
possibleLevel,
|
|
122
|
+
)
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Case 3: logger.log('info') - only level without message - silence
|
|
129
|
+
if (args.length === 1 && typeof args[0] === 'string') {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
83
134
|
}
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Deep clones an object to prevent mutation.
|
|
138
|
+
* Special handling for Error objects to preserve Winston's concatenation behavior.
|
|
139
|
+
*
|
|
140
|
+
* @param {*} obj - The object to clone
|
|
141
|
+
* @returns {*} - The cloned object
|
|
142
|
+
*/
|
|
143
|
+
function deepClone(obj) {
|
|
144
|
+
if (obj === null || typeof obj !== 'object') {
|
|
145
|
+
return obj;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (obj instanceof Error) {
|
|
149
|
+
// Create new Error instance to preserve Winston's error handling
|
|
150
|
+
// (Winston concatenates message with error string)
|
|
151
|
+
const cloned = new Error(obj.message);
|
|
152
|
+
cloned.stack = obj.stack;
|
|
153
|
+
|
|
154
|
+
// Copy additional properties
|
|
155
|
+
Object.keys(obj).forEach((key) => {
|
|
156
|
+
if (key !== 'message' && key !== 'stack') {
|
|
157
|
+
cloned[key] = obj[key];
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return cloned;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(obj)) {
|
|
164
|
+
return obj.map(deepClone);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const cloned = {};
|
|
168
|
+
Object.keys(obj).forEach((key) => {
|
|
169
|
+
cloned[key] = deepClone(obj[key]);
|
|
170
|
+
});
|
|
171
|
+
return cloned;
|
|
89
172
|
}
|
|
90
173
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Injects AsyncLocalStorage context metadata into log arguments.
|
|
176
|
+
*
|
|
177
|
+
* @param {Array} args - The log arguments
|
|
178
|
+
* @returns {Array} - Arguments with context injected
|
|
179
|
+
*/
|
|
180
|
+
function injectContext(args) {
|
|
181
|
+
const store = loggerStorage.getStore();
|
|
182
|
+
if (!store || !store.context) {
|
|
183
|
+
return args;
|
|
95
184
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
185
|
+
|
|
186
|
+
const newArgs = Array.from(args);
|
|
187
|
+
let metaIndex = -1;
|
|
188
|
+
|
|
189
|
+
// Determine where metadata object is in arguments
|
|
190
|
+
if (newArgs.length === 1) {
|
|
191
|
+
// logger.info(obj) or logger.info(error)
|
|
192
|
+
if (typeof newArgs[0] === 'object') {
|
|
193
|
+
metaIndex = 0;
|
|
194
|
+
}
|
|
195
|
+
} else if (newArgs.length === 2) {
|
|
196
|
+
// logger.info('msg', obj) or logger.log('level', obj)
|
|
197
|
+
if (typeof newArgs[1] === 'object') {
|
|
198
|
+
metaIndex = 1;
|
|
103
199
|
}
|
|
200
|
+
} else {
|
|
201
|
+
// logger.log('level', 'msg', obj)
|
|
202
|
+
metaIndex = 2;
|
|
104
203
|
}
|
|
105
|
-
return currentLogger.log(...arguments);
|
|
106
|
-
}
|
|
107
204
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
205
|
+
if (metaIndex >= 0 && newArgs[metaIndex]) {
|
|
206
|
+
// Merge context into existing metadata object
|
|
207
|
+
newArgs[metaIndex] = {
|
|
208
|
+
...store.context,
|
|
209
|
+
...newArgs[metaIndex],
|
|
210
|
+
};
|
|
211
|
+
} else {
|
|
212
|
+
// Add context as new argument
|
|
213
|
+
newArgs.push(store.context);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return newArgs;
|
|
112
217
|
}
|
|
113
218
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
runWithContext: (context, fn) => {
|
|
128
|
-
const parentLogger = loggerStorage.getStore()?.current || baseLogger;
|
|
129
|
-
const child = parentLogger.child(context);
|
|
130
|
-
return loggerStorage.run({ current: child }, fn);
|
|
131
|
-
},
|
|
219
|
+
/**
|
|
220
|
+
* Controlled log function with validation, cloning, and context injection.
|
|
221
|
+
* This is the core logic that all log methods delegate to.
|
|
222
|
+
*
|
|
223
|
+
* @param {...*} args - Log arguments (level, message, metadata)
|
|
224
|
+
* @returns {Logger} - The logger instance for chaining
|
|
225
|
+
*/
|
|
226
|
+
function controlledLog(...args) {
|
|
227
|
+
// Step 1: Validate - should we log?
|
|
228
|
+
if (!shouldLog(args)) {
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
132
231
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
} else {
|
|
138
|
-
baseLogger.warn('Attempted to call addContext outside of an AsyncLocalStorage context');
|
|
232
|
+
// Step 2: Clone arguments to prevent mutation
|
|
233
|
+
let clonedArgs = args.map((arg) => {
|
|
234
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
235
|
+
return deepClone(arg);
|
|
139
236
|
}
|
|
237
|
+
return arg;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Step 3: Inject AsyncLocalStorage context
|
|
241
|
+
clonedArgs = injectContext(clonedArgs);
|
|
242
|
+
|
|
243
|
+
// Step 4: Delegate to original Winston log
|
|
244
|
+
return originalLog(...clonedArgs);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// MONKEY-PATCHING: Apply controlled logic to Winston instance
|
|
248
|
+
|
|
249
|
+
// Override .log() method
|
|
250
|
+
baseLogger.log = controlledLog;
|
|
251
|
+
|
|
252
|
+
// Override convenience methods (info, error, warn, etc.)
|
|
253
|
+
['fatal', 'error', 'warn', 'info', 'debug', 'verbose', 'silly'].forEach(
|
|
254
|
+
(level) => {
|
|
255
|
+
baseLogger[level] = function leveledLog(...args) {
|
|
256
|
+
return controlledLog.call(this, level, ...args);
|
|
257
|
+
};
|
|
140
258
|
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// CUSTOM METHODS: Context management and utilities
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if FATAL level is enabled.
|
|
265
|
+
*
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
baseLogger.isFatalEnabled = function isFatalEnabled() {
|
|
269
|
+
return this.isLevelEnabled('fatal');
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Execute a function within a logging context.
|
|
274
|
+
* Context metadata is automatically injected into all logs within the function.
|
|
275
|
+
*
|
|
276
|
+
* @param {Object} context - Metadata to inject (e.g., { requestId: '123' })
|
|
277
|
+
* @param {Function} fn - Function to execute within context
|
|
278
|
+
* @returns {*} - Return value of fn
|
|
279
|
+
*/
|
|
280
|
+
baseLogger.runWithContext = function runWithContext(context, fn) {
|
|
281
|
+
const parentStore = loggerStorage.getStore();
|
|
282
|
+
const mergedContext = parentStore && parentStore.context
|
|
283
|
+
? { ...parentStore.context, ...context }
|
|
284
|
+
: context;
|
|
285
|
+
|
|
286
|
+
return loggerStorage.run({ context: mergedContext }, fn);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Add metadata to the current logging context.
|
|
291
|
+
* This mutates the current context (useful for progressive enrichment).
|
|
292
|
+
*
|
|
293
|
+
* @param {Object} context - Additional metadata to merge
|
|
294
|
+
*/
|
|
295
|
+
baseLogger.addContext = function addContext(context) {
|
|
296
|
+
const store = loggerStorage.getStore();
|
|
297
|
+
if (store) {
|
|
298
|
+
store.context = { ...store.context, ...context };
|
|
299
|
+
} else {
|
|
300
|
+
baseLogger.warn(
|
|
301
|
+
'Attempted to call addContext outside of an AsyncLocalStorage context',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
141
304
|
};
|
|
142
305
|
|
|
143
|
-
module.exports =
|
|
144
|
-
module.exports.default =
|
|
306
|
+
module.exports = baseLogger;
|
|
307
|
+
module.exports.default = baseLogger;
|
package/test/lib/logger.spec.js
CHANGED
|
@@ -9,7 +9,9 @@ describe('Logger test', () => {
|
|
|
9
9
|
before(() => {
|
|
10
10
|
process.env.APP_NAME = 'application-name';
|
|
11
11
|
stdMocks.use({ print: true });
|
|
12
|
-
brokenClock = sinon.useFakeTimers(
|
|
12
|
+
brokenClock = sinon.useFakeTimers(
|
|
13
|
+
new Date('2018-06-05T18:20:42.345Z').getTime(),
|
|
14
|
+
);
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
beforeEach(() => {
|
|
@@ -124,10 +126,16 @@ describe('Logger test', () => {
|
|
|
124
126
|
logger.info('some message', new Error('some reason'));
|
|
125
127
|
|
|
126
128
|
const actualOutput = JSON.parse(stdMocks.flush().stdout[0]);
|
|
127
|
-
actualOutput.should.have
|
|
129
|
+
actualOutput.should.have
|
|
130
|
+
.property('@timestamp')
|
|
131
|
+
.and.be.equal('2018-06-05T18:20:42.345Z');
|
|
128
132
|
actualOutput.should.have.property('@version').and.be.equal(1);
|
|
129
|
-
actualOutput.should.have
|
|
130
|
-
|
|
133
|
+
actualOutput.should.have
|
|
134
|
+
.property('application')
|
|
135
|
+
.and.be.equal('application-name');
|
|
136
|
+
actualOutput.should.have
|
|
137
|
+
.property('message')
|
|
138
|
+
.and.be.equal('some message some reason');
|
|
131
139
|
actualOutput.should.have.property('level').and.be.equal('INFO');
|
|
132
140
|
actualOutput.should.have.property('stack_trace');
|
|
133
141
|
});
|
|
@@ -136,9 +144,13 @@ describe('Logger test', () => {
|
|
|
136
144
|
logger.info(new Error('some reason'));
|
|
137
145
|
|
|
138
146
|
const actualOutput = JSON.parse(stdMocks.flush().stdout[0]);
|
|
139
|
-
actualOutput.should.have
|
|
147
|
+
actualOutput.should.have
|
|
148
|
+
.property('@timestamp')
|
|
149
|
+
.and.be.equal('2018-06-05T18:20:42.345Z');
|
|
140
150
|
actualOutput.should.have.property('@version').and.be.equal(1);
|
|
141
|
-
actualOutput.should.have
|
|
151
|
+
actualOutput.should.have
|
|
152
|
+
.property('application')
|
|
153
|
+
.and.be.equal('application-name');
|
|
142
154
|
actualOutput.should.have.property('message').and.be.equal('some reason');
|
|
143
155
|
actualOutput.should.have.property('level').and.be.equal('INFO');
|
|
144
156
|
actualOutput.should.have.property('stack_trace');
|
|
@@ -186,7 +198,11 @@ describe('Logger test', () => {
|
|
|
186
198
|
|
|
187
199
|
describe('Logging level', () => {
|
|
188
200
|
it('should log with LogEntry', () => {
|
|
189
|
-
const obj = {
|
|
201
|
+
const obj = {
|
|
202
|
+
level: 'info',
|
|
203
|
+
message: 'some message',
|
|
204
|
+
property: 'some value',
|
|
205
|
+
};
|
|
190
206
|
logger.log(obj);
|
|
191
207
|
const actualOutput = JSON.parse(stdMocks.flush().stdout[0]);
|
|
192
208
|
actualOutput.should.have.property('level').and.be.equal('INFO');
|
|
@@ -245,7 +261,7 @@ describe('Logger test', () => {
|
|
|
245
261
|
delete require.cache[require.resolve('../../src/lib/logger')];
|
|
246
262
|
process.env.LOGGING_LEVEL = 'INFO';
|
|
247
263
|
// eslint-disable-next-line
|
|
248
|
-
const newLogger = require(
|
|
264
|
+
const newLogger = require("../../src/lib/logger");
|
|
249
265
|
newLogger.debug('should not log this message');
|
|
250
266
|
delete process.env.LOGGING_LEVEL;
|
|
251
267
|
delete require.cache[require.resolve('../../src/lib/logger')];
|
|
@@ -260,13 +276,15 @@ describe('Logger test', () => {
|
|
|
260
276
|
delete require.cache[require.resolve('../../src/lib/logger')];
|
|
261
277
|
process.env.LOGGING_FORMATTER_DISABLED = 'true';
|
|
262
278
|
// eslint-disable-next-line
|
|
263
|
-
const newLogger = require(
|
|
279
|
+
const newLogger = require("../../src/lib/logger");
|
|
264
280
|
newLogger.debug('some message');
|
|
265
281
|
delete process.env.LOGGING_FORMATTER_DISABLED;
|
|
266
282
|
delete require.cache[require.resolve('../../src/lib/logger')];
|
|
267
283
|
|
|
268
284
|
const actualOutput = stdMocks.flush().stdout[0];
|
|
269
|
-
actualOutput.should.be.equal(
|
|
285
|
+
actualOutput.should.be.equal(
|
|
286
|
+
'2018-06-05T18:20:42.345Z - debug: some message\n',
|
|
287
|
+
);
|
|
270
288
|
});
|
|
271
289
|
});
|
|
272
290
|
|
|
@@ -302,16 +320,21 @@ describe('Logger test', () => {
|
|
|
302
320
|
|
|
303
321
|
describe('Contextual Logging (AsyncLocalStorage)', () => {
|
|
304
322
|
it('should maintain context metadata across async calls using runWithContext', async () => {
|
|
305
|
-
await logger.runWithContext(
|
|
306
|
-
|
|
323
|
+
await logger.runWithContext(
|
|
324
|
+
{ partition: 1024, topic: 'my-topic' },
|
|
325
|
+
async () => {
|
|
326
|
+
await Promise.resolve();
|
|
307
327
|
|
|
308
|
-
|
|
328
|
+
logger.info('message inside context');
|
|
309
329
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
331
|
+
output.should.have.property('partition').and.be.equal(1024);
|
|
332
|
+
output.should.have.property('topic').and.be.equal('my-topic');
|
|
333
|
+
output.should.have
|
|
334
|
+
.property('message')
|
|
335
|
+
.and.be.equal('message inside context');
|
|
336
|
+
},
|
|
337
|
+
);
|
|
315
338
|
});
|
|
316
339
|
|
|
317
340
|
it('should update context metadata using addContext (mutation)', async () => {
|
|
@@ -336,7 +359,11 @@ describe('Logger test', () => {
|
|
|
336
359
|
const parsedOutput = JSON.parse(actualOutput);
|
|
337
360
|
|
|
338
361
|
parsedOutput.should.have.property('level').and.be.equal('WARN');
|
|
339
|
-
parsedOutput.should.have
|
|
362
|
+
parsedOutput.should.have
|
|
363
|
+
.property('message')
|
|
364
|
+
.and.be.equal(
|
|
365
|
+
'Attempted to call addContext outside of an AsyncLocalStorage context',
|
|
366
|
+
);
|
|
340
367
|
});
|
|
341
368
|
|
|
342
369
|
it('should isolate contexts between concurrent executions', async () => {
|
|
@@ -383,5 +410,343 @@ describe('Logger test', () => {
|
|
|
383
410
|
});
|
|
384
411
|
});
|
|
385
412
|
});
|
|
413
|
+
|
|
414
|
+
it('should inject context when using logger.log passing a LogEntry object', async () => {
|
|
415
|
+
await logger.runWithContext({ requestId: 'req-100' }, async () => {
|
|
416
|
+
logger.log({
|
|
417
|
+
level: 'info',
|
|
418
|
+
message: 'context check',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
422
|
+
output.should.have.property('requestId').and.be.equal('req-100');
|
|
423
|
+
output.should.have.property('message').and.be.equal('context check');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('Edge Cases and Coverage', () => {
|
|
429
|
+
describe('shouldLog function coverage', () => {
|
|
430
|
+
it('should silence log when single argument is a level without spaces', () => {
|
|
431
|
+
logger.log('error');
|
|
432
|
+
stdMocks.flush().stderr.length.should.be.equal(0);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should silence log when single argument is "fatal" level', () => {
|
|
436
|
+
logger.log('fatal');
|
|
437
|
+
stdMocks.flush().stderr.length.should.be.equal(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should silence log when single argument is "warn" level', () => {
|
|
441
|
+
logger.log('warn');
|
|
442
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should silence log when single argument is "debug" level', () => {
|
|
446
|
+
logger.log('debug');
|
|
447
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should silence log when single argument is "verbose" level', () => {
|
|
451
|
+
logger.log('verbose');
|
|
452
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should silence log when single argument is "silly" level', () => {
|
|
456
|
+
logger.log('silly');
|
|
457
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should silence log when called via logger.log() with no arguments', () => {
|
|
461
|
+
logger.log();
|
|
462
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
463
|
+
stdMocks.flush().stderr.length.should.be.equal(0);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should silence log when called via logger.log() with a non-level string', () => {
|
|
467
|
+
logger.log('simple message without level');
|
|
468
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should silence log when single string argument is NOT a valid level', () => {
|
|
472
|
+
logger.log('NotALevelAndNoSpaces');
|
|
473
|
+
stdMocks.flush().stdout.length.should.be.equal(0);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('deepClone function coverage', () => {
|
|
478
|
+
it('should clone null values', () => {
|
|
479
|
+
logger.info('message', { nullValue: null });
|
|
480
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
481
|
+
output.should.have.property('nullValue').and.be.equal(null);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should clone primitive values (numbers)', () => {
|
|
485
|
+
logger.info('message', { count: 42 });
|
|
486
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
487
|
+
output.should.have.property('count').and.be.equal(42);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should clone primitive values (booleans)', () => {
|
|
491
|
+
logger.info('message', { active: true });
|
|
492
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
493
|
+
output.should.have.property('active').and.be.equal(true);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should clone arrays', () => {
|
|
497
|
+
const originalArray = [1, 2, 3];
|
|
498
|
+
logger.info('message', { items: originalArray });
|
|
499
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
500
|
+
output.should.have.property('items').and.deep.equal([1, 2, 3]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should clone nested arrays', () => {
|
|
504
|
+
logger.info('message', {
|
|
505
|
+
matrix: [
|
|
506
|
+
[1, 2],
|
|
507
|
+
[3, 4],
|
|
508
|
+
],
|
|
509
|
+
});
|
|
510
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
511
|
+
output.should.have.property('matrix').and.deep.equal([
|
|
512
|
+
[1, 2],
|
|
513
|
+
[3, 4],
|
|
514
|
+
]);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should clone Error objects with additional properties', () => {
|
|
518
|
+
const error = new Error('test error');
|
|
519
|
+
error.code = 'ERR_TEST';
|
|
520
|
+
error.statusCode = 500;
|
|
521
|
+
|
|
522
|
+
logger.info('error occurred', error);
|
|
523
|
+
|
|
524
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
525
|
+
output.should.have.property('code').and.be.equal('ERR_TEST');
|
|
526
|
+
output.should.have.property('statusCode').and.be.equal(500);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('should not mutate nested objects', () => {
|
|
530
|
+
const nested = { inner: { value: 'test' } };
|
|
531
|
+
logger.info('message', nested);
|
|
532
|
+
nested.should.not.have.property('level');
|
|
533
|
+
nested.inner.should.not.have.property('level');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should clone Error objects preventing duplication of message/stack if enumerable', () => {
|
|
537
|
+
const err = new Error('custom error');
|
|
538
|
+
|
|
539
|
+
Object.defineProperty(err, 'message', {
|
|
540
|
+
enumerable: true,
|
|
541
|
+
value: 'custom msg',
|
|
542
|
+
});
|
|
543
|
+
Object.defineProperty(err, 'stack', {
|
|
544
|
+
enumerable: true,
|
|
545
|
+
value: 'custom stack',
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
logger.info(err);
|
|
549
|
+
|
|
550
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
551
|
+
output.should.have.property('message').and.include('custom msg');
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe('injectContext function coverage', () => {
|
|
556
|
+
it('should handle single object argument with context', async () => {
|
|
557
|
+
stdMocks.flush(); // Clear previous outputs
|
|
558
|
+
await logger.runWithContext({ ctxKey: 'ctxValue' }, async () => {
|
|
559
|
+
logger.info({ message: 'test message', extraKey: 'extraValue' });
|
|
560
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
561
|
+
output.should.have.property('ctxKey').and.be.equal('ctxValue');
|
|
562
|
+
output.should.have.property('extraKey').and.be.equal('extraValue');
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should handle two arguments (message + object) with context', async () => {
|
|
567
|
+
stdMocks.flush(); // Clear previous outputs
|
|
568
|
+
await logger.runWithContext({ requestId: '123' }, async () => {
|
|
569
|
+
logger.info('test message', { userId: 'abc' });
|
|
570
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
571
|
+
output.should.have.property('requestId').and.be.equal('123');
|
|
572
|
+
output.should.have.property('userId').and.be.equal('abc');
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should add context as new argument when no metadata object exists', async () => {
|
|
577
|
+
stdMocks.flush(); // Clear previous outputs
|
|
578
|
+
await logger.runWithContext({ sessionId: 'session-123' }, async () => {
|
|
579
|
+
logger.info('just a message');
|
|
580
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
581
|
+
output.should.have.property('sessionId').and.be.equal('session-123');
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should handle single Error argument with context', async () => {
|
|
586
|
+
stdMocks.flush(); // Clear previous outputs
|
|
587
|
+
await logger.runWithContext({ errorContext: 'handler-1' }, async () => {
|
|
588
|
+
logger.error(new Error('error message'));
|
|
589
|
+
const output = JSON.parse(stdMocks.flush().stderr[0]);
|
|
590
|
+
output.should.have.property('errorContext').and.be.equal('handler-1');
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should handle message + error with context', async () => {
|
|
595
|
+
stdMocks.flush(); // Clear previous outputs
|
|
596
|
+
await logger.runWithContext({ errorCtx: 'value' }, async () => {
|
|
597
|
+
logger.error('error occurred', new Error('failure'));
|
|
598
|
+
const output = JSON.parse(stdMocks.flush().stderr[0]);
|
|
599
|
+
output.should.have.property('errorCtx').and.be.equal('value');
|
|
600
|
+
output.should.have.property('message').and.include('error occurred');
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should append context as new argument when single argument is primitive', async () => {
|
|
605
|
+
await logger.runWithContext({ ctx: 'test-val' }, async () => {
|
|
606
|
+
try {
|
|
607
|
+
logger.log(12345);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
error.message.should.match(/toUpperCase/);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe('controlledLog edge cases', () => {
|
|
616
|
+
it('should handle log with object containing level property', () => {
|
|
617
|
+
logger.log({ level: 'info', message: 'test', customField: 'value' });
|
|
618
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
619
|
+
output.should.have.property('level').and.be.equal('INFO');
|
|
620
|
+
output.should.have.property('customField').and.be.equal('value');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should handle multiple metadata objects via spread', () => {
|
|
624
|
+
logger.info('message', {
|
|
625
|
+
key1: 'value1',
|
|
626
|
+
key2: 'value2',
|
|
627
|
+
key3: 'value3',
|
|
628
|
+
});
|
|
629
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
630
|
+
output.should.have.property('key1').and.be.equal('value1');
|
|
631
|
+
output.should.have.property('key2').and.be.equal('value2');
|
|
632
|
+
output.should.have.property('key3').and.be.equal('value3');
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
describe('Custom methods coverage', () => {
|
|
637
|
+
it('should check isFatalEnabled returns correct value', () => {
|
|
638
|
+
const isEnabled = logger.isFatalEnabled();
|
|
639
|
+
isEnabled.should.be.a('boolean');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('should handle runWithContext with no parent context', async () => {
|
|
643
|
+
// Ensure we're outside any context
|
|
644
|
+
await logger.runWithContext({ newContext: 'value' }, async () => {
|
|
645
|
+
logger.info('test');
|
|
646
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
647
|
+
output.should.have.property('newContext').and.be.equal('value');
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should handle addContext when context already exists', async () => {
|
|
652
|
+
await logger.runWithContext({ initial: 'context' }, async () => {
|
|
653
|
+
logger.addContext({ added: 'field' });
|
|
654
|
+
logger.addContext({ another: 'field' });
|
|
655
|
+
|
|
656
|
+
logger.info('test');
|
|
657
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
658
|
+
output.should.have.property('initial').and.be.equal('context');
|
|
659
|
+
output.should.have.property('added').and.be.equal('field');
|
|
660
|
+
output.should.have.property('another').and.be.equal('field');
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('All log levels coverage', () => {
|
|
666
|
+
it('should log fatal level with metadata', () => {
|
|
667
|
+
logger.fatal('fatal message', { fatalMeta: 'value' });
|
|
668
|
+
const output = JSON.parse(stdMocks.flush().stderr[0]);
|
|
669
|
+
output.should.have.property('level').and.be.equal('FATAL');
|
|
670
|
+
output.should.have.property('fatalMeta').and.be.equal('value');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should log error level with metadata', () => {
|
|
674
|
+
logger.error('error message', { errorMeta: 'value' });
|
|
675
|
+
const output = JSON.parse(stdMocks.flush().stderr[0]);
|
|
676
|
+
output.should.have.property('level').and.be.equal('ERROR');
|
|
677
|
+
output.should.have.property('errorMeta').and.be.equal('value');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should log warn level with metadata', () => {
|
|
681
|
+
logger.warn('warn message', { warnMeta: 'value' });
|
|
682
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
683
|
+
output.should.have.property('level').and.be.equal('WARN');
|
|
684
|
+
output.should.have.property('warnMeta').and.be.equal('value');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should log verbose level with metadata', () => {
|
|
688
|
+
logger.verbose('verbose message', { verboseMeta: 'value' });
|
|
689
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
690
|
+
output.should.have.property('level').and.be.equal('DEBUG');
|
|
691
|
+
output.should.have.property('verboseMeta').and.be.equal('value');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should log silly level with metadata', () => {
|
|
695
|
+
logger.silly('silly message', { sillyMeta: 'value' });
|
|
696
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
697
|
+
output.should.have.property('level').and.be.equal('DEBUG');
|
|
698
|
+
output.should.have.property('sillyMeta').and.be.equal('value');
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
describe('Complex scenarios', () => {
|
|
703
|
+
it('should handle deeply nested contexts', async () => {
|
|
704
|
+
await logger.runWithContext({ level1: 'a' }, async () => {
|
|
705
|
+
await logger.runWithContext({ level2: 'b' }, async () => {
|
|
706
|
+
await logger.runWithContext({ level3: 'c' }, async () => {
|
|
707
|
+
logger.info('deeply nested');
|
|
708
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
709
|
+
output.should.have.property('level1').and.be.equal('a');
|
|
710
|
+
output.should.have.property('level2').and.be.equal('b');
|
|
711
|
+
output.should.have.property('level3').and.be.equal('c');
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should handle context with addContext in nested scenarios', async () => {
|
|
718
|
+
await logger.runWithContext({ outer: 'context' }, async () => {
|
|
719
|
+
logger.addContext({ outerAdded: 'field1' });
|
|
720
|
+
|
|
721
|
+
await logger.runWithContext({ inner: 'context' }, async () => {
|
|
722
|
+
logger.addContext({ innerAdded: 'field2' });
|
|
723
|
+
|
|
724
|
+
logger.info('nested with additions');
|
|
725
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
726
|
+
output.should.have.property('outer').and.be.equal('context');
|
|
727
|
+
output.should.have.property('inner').and.be.equal('context');
|
|
728
|
+
output.should.have.property('innerAdded').and.be.equal('field2');
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('should handle logging with null metadata object', () => {
|
|
734
|
+
logger.info('message', null);
|
|
735
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
736
|
+
output.should.have.property('message').and.be.equal('message');
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should handle logging with undefined metadata', () => {
|
|
740
|
+
logger.info('message', undefined);
|
|
741
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
742
|
+
output.should.have.property('message').and.be.equal('message');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('should properly clone objects with circular references removed', () => {
|
|
746
|
+
const obj = { message: 'test', regular: 'field' };
|
|
747
|
+
logger.info(obj);
|
|
748
|
+
obj.should.not.have.property('level');
|
|
749
|
+
});
|
|
750
|
+
});
|
|
386
751
|
});
|
|
387
752
|
});
|