ntlogger 2.9.1 → 3.0.0

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/lib/logger.js CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  const winston = require('winston');
13
13
  const crypto = require('crypto');
14
+ const { withTimeout, drain, trackClose } = require('./lifecycle');
14
15
 
15
16
  const { setupSignalHandlers } = require('./signalHandler');
16
17
  const { initPlugins } = require('../plugins/index');
@@ -26,8 +27,7 @@ const levels = require('./levels');
26
27
  // Define a map to hold the logger instances by their location
27
28
  const loggerInstances = new Map();
28
29
 
29
- // Call the signal handler setup function
30
- setupSignalHandlers(loggerInstances);
30
+ const activeLoggers = new Set();
31
31
 
32
32
  const customSettings = {
33
33
  levels: levels,
@@ -116,7 +116,7 @@ const consoleFormatter = (config) => winston.format.combine(
116
116
 
117
117
  // Apply the color to the padded level and the session ID
118
118
  // Note: The session ID color remains based on the 'color' variable (pre-calculated)
119
- return `${timestamp} [${levelColor}${paddedLevel}${resetCode}] [${colorAnsiCode}ID: ${shortSessionId}${resetCode}] [${locationStr}]: ${levelColor}${message}${resetCode}`;
119
+ return `${config.timestamp === false ? "" : timestamp + " "}[${levelColor}${paddedLevel}${resetCode}] [${colorAnsiCode}ID: ${shortSessionId}${resetCode}] [${locationStr}]: ${levelColor}${message}${resetCode}`;
120
120
  }),
121
121
  );
122
122
 
@@ -136,7 +136,7 @@ const fileFormatter = (config) => winston.format.combine(
136
136
 
137
137
  // Include all metadata in JSON (filePath is already shown in locationStr but kept in JSON for structured data)
138
138
  const metaKeys = Object.keys(meta);
139
- return `${timestamp} [${paddedLevel}] [ID: ${sessionId}] [${locationStr}]: ${message}${
139
+ return `${config.timestamp === false ? "" : timestamp + " "}[${paddedLevel}] [ID: ${sessionId}] [${locationStr}]: ${message}${
140
140
  metaKeys.length ? ' ' + JSON.stringify(meta, null, 2) : ''
141
141
  }`;
142
142
  }),
@@ -157,15 +157,20 @@ function wrapLoggerMethod(originalMethod, options = {}) {
157
157
  childContext = null,
158
158
  loggerInstance = null,
159
159
  level = 'info',
160
+ state,
160
161
  } = options;
161
162
 
162
163
  return function(...args) {
164
+ const target = loggerInstance || this;
165
+ if (state.closing) throw new Error('Logger is closed');
166
+ if (!target.isLevelEnabled(level) || target.transports.length === 0) return;
167
+ const eventTimestamp = new Date().toISOString();
163
168
  // Capture stack trace BEFORE setImmediate to get the actual call site
164
169
  let capturedCallSite = null;
165
170
  let capturedCallerOfCaller = null;
166
171
  if (reportPathEnabled) {
172
+ const originalPrepareStackTrace = Error.prepareStackTrace;
167
173
  try {
168
- const originalPrepareStackTrace = Error.prepareStackTrace;
169
174
  Error.prepareStackTrace = (_, stack) => stack;
170
175
 
171
176
  const err = new Error();
@@ -215,28 +220,30 @@ function wrapLoggerMethod(originalMethod, options = {}) {
215
220
  }
216
221
  }
217
222
  } catch (err) {
218
- // Silently fail
223
+ // Path reporting is optional.
224
+ } finally {
225
+ Error.prepareStackTrace = originalPrepareStackTrace;
219
226
  }
220
227
  }
221
228
 
222
- // Use setImmediate to make this non-blocking
229
+ state.pending++;
223
230
  setImmediate(() => {
224
231
  const startTime = performanceTracker?.enabled ? performance.now() : null;
225
232
 
226
233
  try {
227
234
  // Extract message and metadata (Winston format: method(message, meta) or method(message))
228
235
  let message = args[0];
229
- let meta = {};
236
+ let meta = { timestamp: eventTimestamp };
230
237
 
231
238
  if (args.length > 1 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && !Array.isArray(args[args.length - 1])) {
232
239
  // Standard format: method(message, meta)
233
- meta = { ...args[args.length - 1] };
240
+ meta = { ...args[args.length - 1], timestamp: eventTimestamp };
234
241
  message = args[0];
235
242
  } else if (args.length === 1) {
236
243
  if (typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
237
244
  // Object format: method({ message: '...', ...other })
238
- message = args[0].message || JSON.stringify(args[0]);
239
- meta = { ...args[0] };
245
+ message = args[0].message ?? JSON.stringify(args[0]);
246
+ meta = { ...args[0], timestamp: eventTimestamp };
240
247
  delete meta.message;
241
248
  } else {
242
249
  // Just a message string
@@ -244,6 +251,10 @@ function wrapLoggerMethod(originalMethod, options = {}) {
244
251
  }
245
252
  }
246
253
 
254
+ if (args[0] instanceof Error) {
255
+ message = args[0].message;
256
+ meta = { ...meta, name: args[0].name, stack: args[0].stack, cause: args[0].cause };
257
+ }
247
258
  // Merge child context if available
248
259
  if (childContext) {
249
260
  meta = { ...childContext, ...meta };
@@ -303,7 +314,10 @@ function wrapLoggerMethod(originalMethod, options = {}) {
303
314
  // Last resort - log to console
304
315
  console.error('Logger wrapper error:', err);
305
316
  console.error('Fallback also failed:', fallbackErr);
317
+ state.error = fallbackErr;
306
318
  }
319
+ } finally {
320
+ state.pending--;
307
321
  }
308
322
  });
309
323
  };
@@ -352,7 +366,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
352
366
  logTransport.push(
353
367
  new winston.transports.File({ filename: `${path}/fatal.log`, level: 'fatal', format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
354
368
  new winston.transports.File({ filename: `${path}/error.log`, level: 'error', format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
355
- new winston.transports.File({ filename: `${path}/combined.log`, format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
369
+ new winston.transports.File({ filename: `${path}/${config.filename || "combined.log"}`, format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
356
370
  );
357
371
  }
358
372
  if (transports) {logTransport = logTransport.concat
@@ -370,6 +384,12 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
370
384
  }
371
385
  });
372
386
 
387
+ const state = { pending: 0, closing: false, error: null };
388
+ logger.on('error', error => { state.error = error; });
389
+ logger.transports.forEach(trackClose);
390
+ const add = logger.add.bind(logger);
391
+ logger.add = transport => { trackClose(transport); return add(transport); };
392
+
373
393
  // Initialize feature modules
374
394
  const sampler = (Object.keys(sampling).length > 0 || Object.keys(rateLimit).length > 0)
375
395
  ? new LogSampler({ sampling, rateLimit })
@@ -392,6 +412,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
392
412
  performanceTracker: perfTracker,
393
413
  childContext,
394
414
  loggerInstance: logger,
415
+ state,
395
416
  };
396
417
 
397
418
  const boundLogger = logger;
@@ -403,15 +424,30 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
403
424
  logger.fatal = wrapLoggerMethod(boundLogger.fatal.bind(boundLogger), { ...wrapOptions, level: 'fatal' });
404
425
  logger.internal = wrapLoggerMethod(boundLogger.internal.bind(boundLogger), { ...wrapOptions, level: 'internal' });
405
426
 
406
- // Add child logger method
407
- logger.child = function(context) {
408
- const mergedContext = childContext ? { ...childContext, ...context } : context;
409
- // Use skipCache for child loggers to avoid cache conflicts
410
- const childConfig = { ...config, skipCache: true };
411
- // Generate unique location for child logger
412
- const childLocation = `${location}:child:${Date.now()}:${Math.random().toString(36).substring(7)}`;
413
- return createLoggerInstance(childLocation, childConfig, transports, mergedContext);
427
+ // Children share the root stream and transports, but never own their shutdown.
428
+ const makeChild = (context = {}) => {
429
+ const child = Object.create(logger);
430
+ const frozen = Object.freeze({ ...context });
431
+ let closed = false;
432
+ for (const name of Object.keys(levels)) {
433
+ child[name] = (...args) => {
434
+ if (closed) throw new Error('Child logger is closed');
435
+ let message = args[0];
436
+ let meta = { ...frozen, ...(args[1] || {}) };
437
+ if (message && typeof message === 'object' && !(message instanceof Error)) {
438
+ meta = { ...frozen, ...message };
439
+ message = message.message ?? JSON.stringify(message);
440
+ delete meta.message;
441
+ }
442
+ return logger[name](message, meta);
443
+ };
444
+ }
445
+ child.child = extra => makeChild({ ...frozen, ...extra });
446
+ child.flush = () => logger.flush();
447
+ child.close = () => { closed = true; return logger.flush(); };
448
+ return child;
414
449
  };
450
+ logger.child = makeChild;
415
451
 
416
452
  // Add performance tracking methods
417
453
  if (perfTracker) {
@@ -437,48 +473,52 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
437
473
  return stats;
438
474
  };
439
475
 
440
- // Add flush method (non-blocking, returns Promise)
441
- logger.flush = function() {
442
- return new Promise((resolve) => {
443
- setImmediate(() => {
444
- // Flush all transports that support it
445
- const flushPromises = logger.transports.map(transport => {
446
- if (transport.flush && typeof transport.flush === 'function') {
447
- return Promise.resolve(transport.flush());
448
- }
449
- return Promise.resolve();
450
- });
451
- Promise.all(flushPromises).then(() => resolve());
452
- });
453
- });
454
- };
455
-
456
- // Add close method (graceful shutdown)
457
- logger.close = function() {
458
- return new Promise((resolve) => {
459
- setImmediate(() => {
460
- // Destroy feature modules
461
- if (sampler) sampler.destroy();
462
- if (deduplicator) deduplicator.destroy();
463
-
464
- // Close all transports
465
- logger.end(() => {
466
- resolve();
467
- });
468
- });
476
+ const timeout = config.shutdownTimeout ?? 30000;
477
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Invalid shutdownTimeout');
478
+ let closePromise;
479
+ let statsTimer;
480
+ logger.flush = () => withTimeout((async () => {
481
+ await drain(logger, state, timeout);
482
+ await Promise.all(logger.transports.map(t => t.flush?.()));
483
+ await drain(logger, state, timeout);
484
+ })(), timeout, 'Logger flush');
485
+
486
+ logger.close = () => {
487
+ if (closePromise) return closePromise;
488
+ state.closing = true;
489
+ clearInterval(statsTimer);
490
+ if (loggerInstances.get(location) === logger) loggerInstances.delete(location);
491
+ const owned = logger.transports.slice();
492
+ closePromise = withTimeout((async () => {
493
+ let failure;
494
+ try { await logger.flush(); } catch (error) { failure = error; }
495
+ try {
496
+ await withTimeout(new Promise((resolve, reject) => {
497
+ if (logger._writableState.finished) return resolve();
498
+ const onError = error => { logger.removeListener('finish', onFinish); reject(error); };
499
+ const onFinish = () => { logger.removeListener('error', onError); resolve(); };
500
+ logger.once('error', onError);
501
+ logger.once('finish', onFinish);
502
+ logger.end();
503
+ }), timeout, 'Logger stream close');
504
+ } catch (error) { failure ||= error; }
505
+ const results = await Promise.allSettled(owned.map(t => t._ntlClose()));
506
+ failure ||= results.find(result => result.status === 'rejected')?.reason;
507
+ if (failure) throw failure;
508
+ })(), timeout * 2, 'Logger close').finally(() => {
509
+ if (sampler) sampler.destroy();
510
+ if (deduplicator) deduplicator.destroy();
511
+ activeLoggers.delete(logger);
469
512
  });
513
+ return closePromise;
470
514
  };
471
515
 
472
- // Statistics reporting interval
473
516
  if (statsInterval > 0 && (sampler || deduplicator)) {
474
- setInterval(() => {
475
- const stats = logger.getStats();
476
- logger.internal('Logger Statistics:', stats);
477
- }, statsInterval);
517
+ statsTimer = setInterval(() => logger.internal('Logger Statistics:', logger.getStats()), statsInterval);
518
+ statsTimer.unref();
478
519
  }
479
-
480
- // Cache the newly created logger instance
481
- loggerInstances.set(location, logger);
520
+ if (!skipCache) loggerInstances.set(location, logger);
521
+ activeLoggers.add(logger);
482
522
 
483
523
  if (debug === true) {
484
524
  // Use pre-calculated color values
@@ -524,6 +564,7 @@ const logger = (location = "Unknown", config = {}) => {
524
564
  // If cached logger doesn't have the features but config requests them, recreate
525
565
  if (!cachedHasFeatures) {
526
566
  // Remove from cache and recreate with new config
567
+ cachedLogger.close().catch(error => global.console.error('Logger replacement cleanup failed:', error));
527
568
  loggerInstances.delete(location);
528
569
  } else {
529
570
  // Cached logger has features, return it
@@ -547,5 +588,7 @@ const logger = (location = "Unknown", config = {}) => {
547
588
  return createLoggerInstance(location, config, transports);
548
589
  };
549
590
 
591
+ logger.setupSignalHandlers = options => setupSignalHandlers(activeLoggers, options);
592
+
550
593
  // Export a function to create or return the existing logger instance
551
594
  module.exports = logger;