ntlogger 2.7.0 → 2.8.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/lib/logger.js CHANGED
@@ -14,9 +14,15 @@ const crypto = require('crypto');
14
14
 
15
15
  const { setupSignalHandlers } = require('./signalHandler');
16
16
  const { initPlugins } = require('../plugins/index');
17
+ const { reportPath } = require('./pathReporter');
18
+ const { LogSampler } = require('./logSampler');
19
+ const { LogDeduplicator } = require('./logDeduplicator');
20
+ const { PerformanceTracker } = require('./performanceTracker');
21
+
17
22
  const colors = require('./colors');
18
23
  const levels = require('./levels');
19
24
 
25
+
20
26
  // Define a map to hold the logger instances by their location
21
27
  const loggerInstances = new Map();
22
28
 
@@ -72,6 +78,14 @@ const generateSessionId = () => {
72
78
  const sessionId = generateSessionId(); // Generate session ID once
73
79
  const color = randomBrightColor();
74
80
 
81
+ // Pre-calculate color RGB values and ANSI codes for performance
82
+ const colorRgb = {
83
+ r: parseInt(color.substring(1, 3), 16),
84
+ g: parseInt(color.substring(3, 5), 16),
85
+ b: parseInt(color.substring(5, 7), 16)
86
+ };
87
+ const colorAnsiCode = `\x1b[38;2;${colorRgb.r};${colorRgb.g};${colorRgb.b}m`;
88
+
75
89
  // Custom console formatter with random color for session ID and set color for log level and message
76
90
  const consoleFormatter = (config) => winston.format.combine(
77
91
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
@@ -83,9 +97,15 @@ const consoleFormatter = (config) => winston.format.combine(
83
97
  const levelColor = customSettings.colors[level] || ''; // Default to no color if the level is unknown
84
98
  const resetCode = '\x1b[0m';
85
99
 
100
+ // Build location string (optionally include filePath if reportPath is enabled)
101
+ let locationStr = meta.location || 'Unknown';
102
+ if (config.reportPath && meta.filePath) {
103
+ locationStr += ` ${meta.filePath}`;
104
+ }
105
+
86
106
  // Apply the color to the padded level and the session ID
87
- // Note: The session ID color remains based on the 'color' variable
88
- return `${timestamp} [${levelColor}${paddedLevel}${resetCode}] [\x1b[38;2;${parseInt(color.substring(1, 3), 16)};${parseInt(color.substring(3, 5), 16)};${parseInt(color.substring(5, 7), 16)}mID: ${shortSessionId}${resetCode}] [${meta.location || 'Unknown'}]: ${levelColor}${message}${resetCode}`;
107
+ // Note: The session ID color remains based on the 'color' variable (pre-calculated)
108
+ return `${timestamp} [${levelColor}${paddedLevel}${resetCode}] [${colorAnsiCode}ID: ${shortSessionId}${resetCode}] [${locationStr}]: ${levelColor}${message}${resetCode}`;
89
109
  }),
90
110
  );
91
111
 
@@ -95,13 +115,188 @@ const fileFormatter = (config) => winston.format.combine(
95
115
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
96
116
  const paddedLevel = level.padEnd(8); // Pad the level to ensure consistent spacing
97
117
 
98
- return `${timestamp} [${paddedLevel}] [ID: ${sessionId}] [${meta.location || 'Unknown'}]: ${message} ${
99
- Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
118
+ // Build location string (optionally include filePath if reportPath is enabled)
119
+ let locationStr = meta.location || 'Unknown';
120
+ if (config.reportPath && meta.filePath) {
121
+ locationStr += ` ${meta.filePath}`;
122
+ }
123
+
124
+ // Include all metadata in JSON (filePath is already shown in locationStr but kept in JSON for structured data)
125
+ const metaKeys = Object.keys(meta);
126
+ return `${timestamp} [${paddedLevel}] [ID: ${sessionId}] [${locationStr}]: ${message}${
127
+ metaKeys.length ? ' ' + JSON.stringify(meta, null, 2) : ''
100
128
  }`;
101
129
  }),
102
130
  );
103
131
 
104
- const createLoggerInstance = (location = "Unknown", config = {}, transports) => {
132
+ /**
133
+ * Wraps a logger method to add all advanced features
134
+ * @param {Function} originalMethod - The original logger method (e.g., logger.info)
135
+ * @param {object} options - Options object with sampler, deduplicator, performanceTracker, etc.
136
+ * @returns {Function} - Wrapped method with all features
137
+ */
138
+ function wrapLoggerMethod(originalMethod, options = {}) {
139
+ const {
140
+ reportPathEnabled = false,
141
+ sampler = null,
142
+ deduplicator = null,
143
+ performanceTracker = null,
144
+ childContext = null,
145
+ loggerInstance = null,
146
+ level = 'info',
147
+ } = options;
148
+
149
+ return function(...args) {
150
+ // Capture stack trace BEFORE setImmediate to get the actual call site
151
+ let capturedCallSite = null;
152
+ let capturedCallerOfCaller = null;
153
+ if (reportPathEnabled) {
154
+ try {
155
+ const originalPrepareStackTrace = Error.prepareStackTrace;
156
+ Error.prepareStackTrace = (_, stack) => stack;
157
+
158
+ const err = new Error();
159
+ const stack = err.stack;
160
+
161
+ Error.prepareStackTrace = originalPrepareStackTrace;
162
+
163
+ // Stack structure: stack[0] = this wrapper function, stack[1] = actual caller
164
+ // Walk up the stack to skip internal Node.js functions
165
+ if (stack && stack.length >= 2) {
166
+ // Helper to check if a call site is internal
167
+ const isInternal = (callSite) => {
168
+ if (!callSite) return true;
169
+ const funcName = callSite.getFunctionName() || callSite.getMethodName() || '';
170
+ const fileName = callSite.getFileName() || '';
171
+ return (funcName.startsWith('_') ||
172
+ fileName.includes('node:internal/') ||
173
+ fileName.includes('node_modules/') ||
174
+ fileName.includes('internal/') ||
175
+ ['_onTimeout', 'listOnTimeout', 'processTimers', 'setImmediate', 'setTimeout', 'setInterval'].includes(funcName));
176
+ };
177
+
178
+ // Start from stack[1] (skip our wrapper)
179
+ let callSiteIndex = 1;
180
+
181
+ // Skip internal functions to find the actual user code
182
+ while (callSiteIndex < stack.length && isInternal(stack[callSiteIndex])) {
183
+ callSiteIndex++;
184
+ }
185
+
186
+ if (callSiteIndex < stack.length) {
187
+ capturedCallSite = stack[callSiteIndex];
188
+
189
+ // Find the caller of the caller, also skipping internals
190
+ let callerIndex = callSiteIndex + 1;
191
+ while (callerIndex < stack.length && isInternal(stack[callerIndex])) {
192
+ callerIndex++;
193
+ }
194
+
195
+ if (callerIndex < stack.length) {
196
+ capturedCallerOfCaller = stack[callerIndex];
197
+ }
198
+ } else {
199
+ // Fallback: use stack[1] if we can't find non-internal
200
+ capturedCallSite = stack[1];
201
+ capturedCallerOfCaller = stack.length > 2 ? stack[2] : null;
202
+ }
203
+ }
204
+ } catch (err) {
205
+ // Silently fail
206
+ }
207
+ }
208
+
209
+ // Use setImmediate to make this non-blocking
210
+ setImmediate(() => {
211
+ const startTime = performanceTracker?.enabled ? performance.now() : null;
212
+
213
+ try {
214
+ // Extract message and metadata (Winston format: method(message, meta) or method(message))
215
+ let message = args[0];
216
+ let meta = {};
217
+
218
+ if (args.length > 1 && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && !Array.isArray(args[args.length - 1])) {
219
+ // Standard format: method(message, meta)
220
+ meta = { ...args[args.length - 1] };
221
+ message = args[0];
222
+ } else if (args.length === 1) {
223
+ if (typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
224
+ // Object format: method({ message: '...', ...other })
225
+ message = args[0].message || JSON.stringify(args[0]);
226
+ meta = { ...args[0] };
227
+ delete meta.message;
228
+ } else {
229
+ // Just a message string
230
+ message = args[0];
231
+ }
232
+ }
233
+
234
+ // Merge child context if available
235
+ if (childContext) {
236
+ meta = { ...childContext, ...meta };
237
+ }
238
+
239
+ // Check sampling and rate limiting
240
+ if (sampler && !sampler.shouldProcess(level)) {
241
+ if (performanceTracker?.enabled && startTime) {
242
+ performanceTracker.recordLogProcessing(performance.now() - startTime);
243
+ }
244
+ return;
245
+ }
246
+
247
+ // Use captured stack trace for filePath
248
+ if (reportPathEnabled && capturedCallSite) {
249
+ const filePath = reportPath(capturedCallSite, capturedCallerOfCaller);
250
+ if (filePath) {
251
+ meta.filePath = filePath;
252
+ }
253
+ }
254
+
255
+ // Check deduplication
256
+ if (deduplicator) {
257
+ const dedupResult = deduplicator.check(level, String(message), meta);
258
+ if (dedupResult) {
259
+ if (!dedupResult.shouldLog) {
260
+ // Suppressed by deduplication
261
+ if (performanceTracker?.enabled && startTime) {
262
+ performanceTracker.recordLogProcessing(performance.now() - startTime);
263
+ }
264
+ return;
265
+ }
266
+ // Update message with count if needed
267
+ if (dedupResult.count > 1 && dedupResult.message !== String(message)) {
268
+ message = dedupResult.message;
269
+ }
270
+ }
271
+ }
272
+
273
+ // Call the original method with modified args (Winston format)
274
+ const transportStartTime = performanceTracker?.enabled ? performance.now() : null;
275
+ const target = loggerInstance || this;
276
+ originalMethod.call(target, message, meta);
277
+
278
+ if (performanceTracker?.enabled) {
279
+ const logTime = performance.now() - startTime;
280
+ performanceTracker.recordLogProcessing(logTime);
281
+ if (transportStartTime) {
282
+ performanceTracker.recordTransport(performance.now() - transportStartTime);
283
+ }
284
+ }
285
+ } catch (err) {
286
+ // Don't break logging if wrapper fails - fall back to original method
287
+ try {
288
+ originalMethod.apply(this, args);
289
+ } catch (fallbackErr) {
290
+ // Last resort - log to console
291
+ console.error('Logger wrapper error:', err);
292
+ console.error('Fallback also failed:', fallbackErr);
293
+ }
294
+ }
295
+ });
296
+ };
297
+ }
298
+
299
+ const createLoggerInstance = (location = "Unknown", config = {}, transports, parentContext = null) => {
105
300
  // Configuration options
106
301
  const {
107
302
  level = 'info',
@@ -114,6 +309,12 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports) =>
114
309
  maxFiles = 5,
115
310
  timestamp = true,
116
311
  skipCache = false,
312
+ reportPath: reportPathEnabled = false,
313
+ sampling = {},
314
+ rateLimit = {},
315
+ deduplication = { enabled: false, threshold: 3, window: 60000 },
316
+ performanceMetrics = process.env.NODE_ENV === 'development',
317
+ statsInterval = 0,
117
318
  } = config;
118
319
 
119
320
  let logTransport = [].filter(Boolean);
@@ -156,24 +357,123 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports) =>
156
357
  }
157
358
  });
158
359
 
360
+ // Initialize feature modules
361
+ const sampler = (Object.keys(sampling).length > 0 || Object.keys(rateLimit).length > 0)
362
+ ? new LogSampler({ sampling, rateLimit })
363
+ : null;
364
+ const deduplicator = deduplication.enabled
365
+ ? new LogDeduplicator(deduplication)
366
+ : null;
367
+ const perfTracker = performanceMetrics
368
+ ? new PerformanceTracker(true)
369
+ : null;
370
+
371
+ // Store child context (merge with parent if exists)
372
+ const childContext = parentContext ? Object.freeze({ ...parentContext }) : null;
373
+
374
+ // Wrap logger methods with all features
375
+ const wrapOptions = {
376
+ reportPathEnabled,
377
+ sampler,
378
+ deduplicator,
379
+ performanceTracker: perfTracker,
380
+ childContext,
381
+ loggerInstance: logger,
382
+ };
383
+
384
+ const boundLogger = logger;
385
+ logger.info = wrapLoggerMethod(boundLogger.info.bind(boundLogger), { ...wrapOptions, level: 'info' });
386
+ logger.warn = wrapLoggerMethod(boundLogger.warn.bind(boundLogger), { ...wrapOptions, level: 'warn' });
387
+ logger.error = wrapLoggerMethod(boundLogger.error.bind(boundLogger), { ...wrapOptions, level: 'error' });
388
+ logger.debug = wrapLoggerMethod(boundLogger.debug.bind(boundLogger), { ...wrapOptions, level: 'debug' });
389
+ logger.trace = wrapLoggerMethod(boundLogger.trace.bind(boundLogger), { ...wrapOptions, level: 'trace' });
390
+ logger.fatal = wrapLoggerMethod(boundLogger.fatal.bind(boundLogger), { ...wrapOptions, level: 'fatal' });
391
+ logger.internal = wrapLoggerMethod(boundLogger.internal.bind(boundLogger), { ...wrapOptions, level: 'internal' });
392
+
393
+ // Add child logger method
394
+ logger.child = function(context) {
395
+ const mergedContext = childContext ? { ...childContext, ...context } : context;
396
+ // Use skipCache for child loggers to avoid cache conflicts
397
+ const childConfig = { ...config, skipCache: true };
398
+ // Generate unique location for child logger
399
+ const childLocation = `${location}:child:${Date.now()}:${Math.random().toString(36).substring(7)}`;
400
+ return createLoggerInstance(childLocation, childConfig, transports, mergedContext);
401
+ };
402
+
403
+ // Add performance tracking methods
404
+ if (perfTracker) {
405
+ logger.time = function(label) {
406
+ perfTracker.time(label);
407
+ };
408
+ logger.timeEnd = function(label) {
409
+ const duration = perfTracker.timeEnd(label);
410
+ if (duration !== null) {
411
+ logger.debug(`Timer '${label}' completed in ${duration.toFixed(2)}ms`);
412
+ }
413
+ return duration;
414
+ };
415
+ }
416
+
417
+ // Add statistics method
418
+ logger.getStats = function() {
419
+ const stats = {
420
+ sampling: sampler ? sampler.getStats() : null,
421
+ deduplication: deduplicator ? deduplicator.getStats() : null,
422
+ performance: perfTracker ? perfTracker.getStats() : null,
423
+ };
424
+ return stats;
425
+ };
426
+
427
+ // Add flush method (non-blocking, returns Promise)
428
+ logger.flush = function() {
429
+ return new Promise((resolve) => {
430
+ setImmediate(() => {
431
+ // Flush all transports that support it
432
+ const flushPromises = logger.transports.map(transport => {
433
+ if (transport.flush && typeof transport.flush === 'function') {
434
+ return Promise.resolve(transport.flush());
435
+ }
436
+ return Promise.resolve();
437
+ });
438
+ Promise.all(flushPromises).then(() => resolve());
439
+ });
440
+ });
441
+ };
442
+
443
+ // Add close method (graceful shutdown)
444
+ logger.close = function() {
445
+ return new Promise((resolve) => {
446
+ setImmediate(() => {
447
+ // Destroy feature modules
448
+ if (sampler) sampler.destroy();
449
+ if (deduplicator) deduplicator.destroy();
450
+
451
+ // Close all transports
452
+ logger.end(() => {
453
+ resolve();
454
+ });
455
+ });
456
+ });
457
+ };
458
+
459
+ // Statistics reporting interval
460
+ if (statsInterval > 0 && (sampler || deduplicator)) {
461
+ setInterval(() => {
462
+ const stats = logger.getStats();
463
+ logger.internal('Logger Statistics:', stats);
464
+ }, statsInterval);
465
+ }
466
+
159
467
  // Cache the newly created logger instance
160
468
  loggerInstances.set(location, logger);
161
469
 
162
470
  if (debug === true) {
163
- // Extract RGB components from the color string
164
- const rgb = {
165
- r: parseInt(color.slice(1, 3), 16),
166
- g: parseInt(color.slice(3, 5), 16),
167
- b: parseInt(color.slice(5, 7), 16)
168
- };
169
-
170
- // Construct ANSI escape code for the RGB color
171
- const colorCode = `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m`;
471
+ // Use pre-calculated color values
172
472
  const resetCode = `\x1b[0m`;
173
473
 
174
- logger.internal(`Logger instance created from ${location} with session ID: ${colorCode}${sessionId.slice(-6)}${resetCode}`);
175
- logger.internal(`Logger instance created from ${location} with color: ${colorCode}${color}${resetCode}`);
176
- logger.internal(`Logger instance created from ${location} with log level: ${colorCode}${logger.level}${resetCode}`);
474
+ logger.internal(`Logger instance created from ${location} with session ID: ${colorAnsiCode}${sessionId.slice(-6)}${resetCode}`);
475
+ logger.internal(`Logger instance created from ${location} with color: ${colorAnsiCode}${color}${resetCode}`);
476
+ logger.internal(`Logger instance created from ${location} with log level: ${colorAnsiCode}${logger.level}${resetCode}`);
177
477
  }
178
478
 
179
479
  return logger;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file /lib/messageNormalizer.js
3
+ * @description Normalizes log messages by replacing dynamic values with placeholders for deduplication.
4
+ */
5
+
6
+ /**
7
+ * Normalizes a message by replacing dynamic values (UUIDs, IDs, numbers) with placeholders
8
+ * @param {string} message - The message to normalize
9
+ * @returns {string} - Normalized message with placeholders
10
+ */
11
+ function normalizeMessage(message) {
12
+ // Convert to string first, then normalize
13
+ let normalized = typeof message === 'string' ? message : String(message);
14
+
15
+ // Replace UUIDs (e.g., a481199f-ed1c-47c6-834d-9cf54cdc394e)
16
+ normalized = normalized.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '*');
17
+
18
+ // Replace long hex strings (32+ chars)
19
+ normalized = normalized.replace(/[0-9a-f]{32,}/gi, '*');
20
+
21
+ // Replace numbers that appear to be IDs (standalone numbers, especially in phrases like "device 1234")
22
+ normalized = normalized.replace(/\b(?:device|id|threshold|ticket|job|request|user|session)\s+(\d+)\b/gi, (match, num) => {
23
+ return match.replace(num, '*');
24
+ });
25
+
26
+ // Replace standalone large numbers (likely IDs) - but keep small numbers (like counts, percentages)
27
+ // Handle both word-boundary cases and standalone numbers (start/end of string)
28
+ normalized = normalized.replace(/(?:^|\b)(\d{4,})(?:\b|$)/g, '*');
29
+
30
+ // Replace timestamps (numbers with dashes or colons)
31
+ normalized = normalized.replace(/\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}/g, '*');
32
+
33
+ return normalized;
34
+ }
35
+
36
+ module.exports = {
37
+ normalizeMessage,
38
+ };
39
+
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file /lib/pathReporter.js
3
+ * @description Utility function for reporting the file path, line number, column number, and call chain where a log statement is executed.
4
+ */
5
+
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Checks if a function name or file path is an internal Node.js function
10
+ * @param {string} functionName - Function name to check
11
+ * @param {string} fileName - File path to check
12
+ * @returns {boolean} - True if internal
13
+ */
14
+ function isInternalFunction(functionName, fileName) {
15
+ if (!functionName && !fileName) return true;
16
+
17
+ // Check for internal Node.js function names
18
+ const internalNames = [
19
+ '_onTimeout',
20
+ 'listOnTimeout',
21
+ 'processTimers',
22
+ 'process.nextTick',
23
+ 'setImmediate',
24
+ 'setTimeout',
25
+ 'setInterval',
26
+ 'Promise.then',
27
+ 'Promise.catch',
28
+ 'Promise.finally',
29
+ ];
30
+
31
+ if (functionName) {
32
+ // Check if function name starts with underscore (internal convention)
33
+ if (functionName.startsWith('_')) {
34
+ return true;
35
+ }
36
+ // Check against known internal names
37
+ if (internalNames.includes(functionName)) {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ // Check if file is from Node.js internals
43
+ if (fileName) {
44
+ if (fileName.includes('node:internal/') ||
45
+ fileName.includes('node_modules/') ||
46
+ fileName.includes('internal/') ||
47
+ fileName.includes('timers.js') ||
48
+ fileName.includes('next_tick.js') ||
49
+ fileName.includes('promise.js')) {
50
+ return true;
51
+ }
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * Reports the file path, line number, column number, and call chain from CallSite objects.
59
+ * @param {Object} callSite - The CallSite object representing where the log was called (stack[1])
60
+ * @param {Object} callerOfCaller - Optional CallSite object representing the caller of the caller (stack[2])
61
+ * @returns {string} - Formatted string like "./path/to/file.js:123:45 [functionName ← callerFunctionName]"
62
+ */
63
+ function reportPath(callSite, callerOfCaller = null) {
64
+ try {
65
+ if (!callSite) {
66
+ return './unknown';
67
+ }
68
+
69
+ const file = callSite.getFileName();
70
+ const line = callSite.getLineNumber();
71
+ const col = callSite.getColumnNumber();
72
+
73
+ if (!file) {
74
+ return './unknown';
75
+ }
76
+
77
+ const callerName = callSite.getFunctionName() || callSite.getMethodName() || '';
78
+ let callerOfCallerName = callerOfCaller
79
+ ? (callerOfCaller.getFunctionName() || callerOfCaller.getMethodName() || '')
80
+ : '';
81
+
82
+ // Filter out internal functions from call chain
83
+ // If caller is internal, don't include it in the chain
84
+ const callerIsInternal = isInternalFunction(callerName, file);
85
+ const callerOfCallerIsInternal = callerOfCaller && isInternalFunction(
86
+ callerOfCallerName,
87
+ callerOfCaller.getFileName()
88
+ );
89
+
90
+ // Get base path (project root)
91
+ const basePath = require.main?.path || process.cwd();
92
+
93
+ // Build relative path and sanitize path traversal attempts
94
+ let relativeDir = path.relative(basePath, path.dirname(file));
95
+
96
+ // Normalize the path (resolves .. and . segments safely)
97
+ relativeDir = path.normalize(relativeDir);
98
+
99
+ // Ensure the normalized path doesn't escape the base directory
100
+ // If normalization results in a path outside basePath, use just the filename
101
+ const resolvedPath = path.resolve(basePath, relativeDir);
102
+ const resolvedBase = path.resolve(basePath);
103
+ if (!resolvedPath.startsWith(resolvedBase)) {
104
+ // Path traversal detected, use only filename
105
+ relativeDir = '';
106
+ }
107
+
108
+ // Convert to forward slashes for consistency
109
+ relativeDir = relativeDir.replace(/\\/g, '/');
110
+
111
+ // Remove any remaining path traversal sequences (defense in depth)
112
+ // Use multiple passes to catch all variations: ../, ..\, .., etc.
113
+ let previousDir = '';
114
+ while (relativeDir !== previousDir) {
115
+ previousDir = relativeDir;
116
+ relativeDir = relativeDir
117
+ .replace(/\.\.\//g, '') // Remove ../
118
+ .replace(/\.\.\\/g, '') // Remove ..\
119
+ .replace(/\/\.\./g, '') // Remove /..
120
+ .replace(/\\\.\./g, '') // Remove \..
121
+ .replace(/^\.\./g, '') // Remove leading ..
122
+ .replace(/\.\.$/g, ''); // Remove trailing ..
123
+ }
124
+
125
+ const shortPath = './' + (relativeDir ? relativeDir + '/' : '') + path.basename(file);
126
+
127
+ // Build the optional "call chain" string only if both are known and not internal
128
+ let callChain = '';
129
+ if (!callerIsInternal && !callerOfCallerIsInternal && callerName && callerOfCallerName) {
130
+ callChain = ` [${callerName} ← ${callerOfCallerName}]`;
131
+ } else if (!callerIsInternal && callerName && callerOfCallerName && !callerOfCallerIsInternal) {
132
+ // Only show caller if callerOfCaller is internal
133
+ callChain = ` [${callerName}]`;
134
+ }
135
+
136
+ return `${shortPath}:${line}:${col}${callChain}`;
137
+ } catch (err) {
138
+ console.error('Error in reportPath:', err.message);
139
+ return './unknown';
140
+ }
141
+ }
142
+
143
+ module.exports = {
144
+ reportPath,
145
+ };
146
+
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file /lib/performanceTracker.js
3
+ * @description Tracks performance metrics for development mode.
4
+ */
5
+
6
+ /**
7
+ * Performance tracker class
8
+ */
9
+ class PerformanceTracker {
10
+ constructor(enabled = false) {
11
+ this.enabled = enabled;
12
+ this.timers = new WeakMap(); // Use WeakMap to avoid memory leaks
13
+ this.timerData = new Map(); // Store timer data with labels as keys
14
+ this.stats = {
15
+ logProcessingTime: [],
16
+ transportTime: [],
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Start a timer
22
+ * @param {string} label - Timer label
23
+ */
24
+ time(label) {
25
+ if (!this.enabled) return;
26
+ this.timerData.set(label, performance.now());
27
+ }
28
+
29
+ /**
30
+ * End a timer and return duration
31
+ * @param {string} label - Timer label
32
+ * @returns {number|null} - Duration in milliseconds or null if timer not found
33
+ */
34
+ timeEnd(label) {
35
+ if (!this.enabled) return null;
36
+ const start = this.timerData.get(label);
37
+ if (start === undefined) {
38
+ return null;
39
+ }
40
+ const duration = performance.now() - start;
41
+ this.timerData.delete(label);
42
+ return duration;
43
+ }
44
+
45
+ /**
46
+ * Record log processing time
47
+ * @param {number} duration - Duration in milliseconds
48
+ */
49
+ recordLogProcessing(duration) {
50
+ if (!this.enabled) return;
51
+ this.stats.logProcessingTime.push(duration);
52
+ // Keep only last 100 entries
53
+ if (this.stats.logProcessingTime.length > 100) {
54
+ this.stats.logProcessingTime.shift();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Record transport execution time
60
+ * @param {number} duration - Duration in milliseconds
61
+ */
62
+ recordTransport(duration) {
63
+ if (!this.enabled) return;
64
+ this.stats.transportTime.push(duration);
65
+ // Keep only last 100 entries
66
+ if (this.stats.transportTime.length > 100) {
67
+ this.stats.transportTime.shift();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get statistics
73
+ * @returns {object} - Statistics object
74
+ */
75
+ getStats() {
76
+ if (!this.enabled) {
77
+ return { enabled: false };
78
+ }
79
+
80
+ const avgLogProcessing = this.stats.logProcessingTime.length > 0
81
+ ? this.stats.logProcessingTime.reduce((a, b) => a + b, 0) / this.stats.logProcessingTime.length
82
+ : 0;
83
+
84
+ const avgTransport = this.stats.transportTime.length > 0
85
+ ? this.stats.transportTime.reduce((a, b) => a + b, 0) / this.stats.transportTime.length
86
+ : 0;
87
+
88
+ return {
89
+ enabled: true,
90
+ avgLogProcessingTime: avgLogProcessing,
91
+ avgTransportTime: avgTransport,
92
+ logProcessingSamples: this.stats.logProcessingTime.length,
93
+ transportSamples: this.stats.transportTime.length,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Reset statistics
99
+ */
100
+ resetStats() {
101
+ this.stats = {
102
+ logProcessingTime: [],
103
+ transportTime: [],
104
+ };
105
+ this.timerData.clear();
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ PerformanceTracker,
111
+ };
112
+