ntlogger 2.7.0 → 2.8.1
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/README.md +144 -0
- package/index.d.ts +225 -0
- package/lib/logDeduplicator.js +146 -0
- package/lib/logSampler.js +139 -0
- package/lib/logger.js +317 -17
- package/lib/messageNormalizer.js +39 -0
- package/lib/pathReporter.js +120 -0
- package/lib/performanceTracker.js +112 -0
- package/package.json +27 -26
- package/plugins/README.md +41 -0
- package/plugins/index.js +9 -8
- package/plugins/openobserve.js +331 -0
- package/plugins/smsMail.js +91 -0
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}] [
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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: ${
|
|
175
|
-
logger.internal(`Logger instance created from ${location} with color: ${
|
|
176
|
-
logger.internal(`Logger instance created from ${location} with log level: ${
|
|
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,120 @@
|
|
|
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
|
|
94
|
+
const relativeDir = path
|
|
95
|
+
.relative(basePath, path.dirname(file))
|
|
96
|
+
.replace(/\\/g, '/')
|
|
97
|
+
.replace(/(\.\.\/)+/g, '');
|
|
98
|
+
|
|
99
|
+
const shortPath = './' + (relativeDir ? relativeDir + '/' : '') + path.basename(file);
|
|
100
|
+
|
|
101
|
+
// Build the optional "call chain" string only if both are known and not internal
|
|
102
|
+
let callChain = '';
|
|
103
|
+
if (!callerIsInternal && !callerOfCallerIsInternal && callerName && callerOfCallerName) {
|
|
104
|
+
callChain = ` [${callerName} ← ${callerOfCallerName}]`;
|
|
105
|
+
} else if (!callerIsInternal && callerName && callerOfCallerName && !callerOfCallerIsInternal) {
|
|
106
|
+
// Only show caller if callerOfCaller is internal
|
|
107
|
+
callChain = ` [${callerName}]`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `${shortPath}:${line}:${col}${callChain}`;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('Error in reportPath:', err.message);
|
|
113
|
+
return './unknown';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
reportPath,
|
|
119
|
+
};
|
|
120
|
+
|
|
@@ -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
|
+
|