ntlogger 2.10.0 → 4.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/README.md +465 -5
- package/contract/conformance.json +133 -0
- package/contract/redaction.json +115 -0
- package/eslint-rules/prefer-object-first.js +53 -0
- package/eslint.d.ts +3 -0
- package/eslint.js +5 -0
- package/index.d.ts +202 -183
- package/index.js +18 -1
- package/lib/lifecycle.js +53 -0
- package/lib/logObserver.js +55 -0
- package/lib/logger.js +115 -55
- package/lib/pinoHooks.js +446 -0
- package/lib/pinoOtel.js +43 -0
- package/lib/processHandlers.js +305 -0
- package/lib/secretRedaction.js +684 -0
- package/lib/secretScope.js +24 -0
- package/lib/signalHandler.js +31 -48
- package/package.json +79 -26
- package/pino.d.ts +311 -0
- package/pino.js +535 -13
- package/plugins/README.md +129 -1
- package/plugins/discord.js +30 -48
- package/plugins/index.js +27 -12
- package/plugins/lib/httpDelivery.js +98 -0
- package/plugins/lib/syslogClient.js +18 -8
- package/plugins/mysql.js +73 -83
- package/plugins/openobserve.js +33 -207
- package/plugins/otel.js +5 -7
- package/plugins/postgres.js +69 -67
- package/plugins/sentry.js +36 -3
- package/plugins/syslog.js +6 -9
- package/plugins/teams.js +16 -67
- package/transports/pino.js +193 -12
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
3
|
+
const { withTimeout } = require('./lifecycle');
|
|
4
|
+
|
|
5
|
+
// Counts represent serialized records, not confirmed delivery to any destination.
|
|
6
|
+
function createLogObserver({ onLog, otel, onLogMaxPending = 100 } = {}) {
|
|
7
|
+
if (onLog !== undefined && typeof onLog !== 'function') throw new TypeError('onLog must be a function');
|
|
8
|
+
if (!Number.isSafeInteger(onLogMaxPending) || onLogMaxPending < 0) throw new TypeError('onLogMaxPending must be a nonnegative safe integer');
|
|
9
|
+
const scope = new AsyncLocalStorage();
|
|
10
|
+
const pending = new Set();
|
|
11
|
+
const counts = Object.fromEntries(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'internal'].map(k => [k, 0]));
|
|
12
|
+
let hookErrors = 0;
|
|
13
|
+
let dropped = 0;
|
|
14
|
+
const bridge = otel ? require('./pinoOtel').createOtelBridge(otel) : null;
|
|
15
|
+
const levelNames = { 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal' };
|
|
16
|
+
function failure() { hookErrors++; }
|
|
17
|
+
function observe(record) {
|
|
18
|
+
const level = typeof record.level === 'number' ? levelNames[record.level] : record.level;
|
|
19
|
+
if (Object.hasOwn(counts, level)) counts[level]++;
|
|
20
|
+
// Suppress observers for logs made by an observer, including async descendants.
|
|
21
|
+
if (scope.getStore()) return;
|
|
22
|
+
scope.run(true, () => {
|
|
23
|
+
if (bridge) {
|
|
24
|
+
try { bridge.emit(record); } catch (_) { failure(); }
|
|
25
|
+
}
|
|
26
|
+
if (onLog) {
|
|
27
|
+
// In-flight promises cannot be cancelled: drop the newest callback only.
|
|
28
|
+
if (pending.size >= onLogMaxPending) { dropped++; return; }
|
|
29
|
+
try {
|
|
30
|
+
// Mutation of the callback record cannot change output or OTel data.
|
|
31
|
+
const result = onLog(JSON.parse(JSON.stringify(record)));
|
|
32
|
+
if (result && typeof result.then === 'function') {
|
|
33
|
+
const task = Promise.resolve(result).catch(failure);
|
|
34
|
+
pending.add(task);
|
|
35
|
+
task.finally(() => pending.delete(task));
|
|
36
|
+
}
|
|
37
|
+
} catch (_) { failure(); }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
context: () => bridge ? bridge.context() : {},
|
|
43
|
+
observe,
|
|
44
|
+
streamWrite(line) { observe(JSON.parse(line)); return line; },
|
|
45
|
+
stats: () => ({ levels: { ...counts }, hookErrors, onLog: { pending: pending.size, dropped, limit: onLogMaxPending } }),
|
|
46
|
+
reset() { for (const key of Object.keys(counts)) counts[key] = 0; hookErrors = 0; dropped = 0; },
|
|
47
|
+
async flush(timeout = 5000) {
|
|
48
|
+
await withTimeout((async () => {
|
|
49
|
+
while (pending.size) await Promise.all([...pending]);
|
|
50
|
+
if (bridge) await bridge.flush();
|
|
51
|
+
})(), timeout, 'Log observers flush');
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
module.exports = { createLogObserver };
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
-
//
|
|
223
|
+
// Path reporting is optional.
|
|
224
|
+
} finally {
|
|
225
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
|
|
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
|
|
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
|
};
|
|
@@ -330,6 +344,8 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
330
344
|
statsInterval = 0,
|
|
331
345
|
} = config;
|
|
332
346
|
|
|
347
|
+
const observer = require('./logObserver').createLogObserver(config);
|
|
348
|
+
const redactor = config.redact === false ? null : require('./secretRedaction').createRedactor(config.redact);
|
|
333
349
|
let logTransport = [].filter(Boolean);
|
|
334
350
|
|
|
335
351
|
if (console) {
|
|
@@ -352,7 +368,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
352
368
|
logTransport.push(
|
|
353
369
|
new winston.transports.File({ filename: `${path}/fatal.log`, level: 'fatal', format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
|
|
354
370
|
new winston.transports.File({ filename: `${path}/error.log`, level: 'error', format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
|
|
355
|
-
new winston.transports.File({ filename: `${path}
|
|
371
|
+
new winston.transports.File({ filename: `${path}/${config.filename || "combined.log"}`, format: fileFormat, maxsize: maxSize, maxFiles: maxFiles }),
|
|
356
372
|
);
|
|
357
373
|
}
|
|
358
374
|
if (transports) {logTransport = logTransport.concat
|
|
@@ -362,6 +378,13 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
362
378
|
const logger = winston.createLogger({
|
|
363
379
|
level: level,
|
|
364
380
|
levels: customSettings.levels,
|
|
381
|
+
format: winston.format(info => {
|
|
382
|
+
const safe = redactor ? redactor.redactObject(info) : { ...info };
|
|
383
|
+
Object.assign(safe, observer.context());
|
|
384
|
+
Object.assign(info, safe);
|
|
385
|
+
observer.observe(safe);
|
|
386
|
+
return info;
|
|
387
|
+
})(),
|
|
365
388
|
transports: logTransport,
|
|
366
389
|
defaultMeta: {
|
|
367
390
|
location,
|
|
@@ -370,6 +393,12 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
370
393
|
}
|
|
371
394
|
});
|
|
372
395
|
|
|
396
|
+
const state = { pending: 0, closing: false, error: null };
|
|
397
|
+
logger.on('error', error => { state.error = error; });
|
|
398
|
+
logger.transports.forEach(trackClose);
|
|
399
|
+
const add = logger.add.bind(logger);
|
|
400
|
+
logger.add = transport => { trackClose(transport); return add(transport); };
|
|
401
|
+
|
|
373
402
|
// Initialize feature modules
|
|
374
403
|
const sampler = (Object.keys(sampling).length > 0 || Object.keys(rateLimit).length > 0)
|
|
375
404
|
? new LogSampler({ sampling, rateLimit })
|
|
@@ -392,6 +421,7 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
392
421
|
performanceTracker: perfTracker,
|
|
393
422
|
childContext,
|
|
394
423
|
loggerInstance: logger,
|
|
424
|
+
state,
|
|
395
425
|
};
|
|
396
426
|
|
|
397
427
|
const boundLogger = logger;
|
|
@@ -403,15 +433,30 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
403
433
|
logger.fatal = wrapLoggerMethod(boundLogger.fatal.bind(boundLogger), { ...wrapOptions, level: 'fatal' });
|
|
404
434
|
logger.internal = wrapLoggerMethod(boundLogger.internal.bind(boundLogger), { ...wrapOptions, level: 'internal' });
|
|
405
435
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
// Children share the root stream and transports, but never own their shutdown.
|
|
437
|
+
const makeChild = (context = {}) => {
|
|
438
|
+
const child = Object.create(logger);
|
|
439
|
+
const frozen = Object.freeze({ ...context });
|
|
440
|
+
let closed = false;
|
|
441
|
+
for (const name of Object.keys(levels)) {
|
|
442
|
+
child[name] = (...args) => {
|
|
443
|
+
if (closed) throw new Error('Child logger is closed');
|
|
444
|
+
let message = args[0];
|
|
445
|
+
let meta = { ...frozen, ...(args[1] || {}) };
|
|
446
|
+
if (message && typeof message === 'object' && !(message instanceof Error)) {
|
|
447
|
+
meta = { ...frozen, ...message };
|
|
448
|
+
message = message.message ?? JSON.stringify(message);
|
|
449
|
+
delete meta.message;
|
|
450
|
+
}
|
|
451
|
+
return logger[name](message, meta);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
child.child = extra => makeChild({ ...frozen, ...extra });
|
|
455
|
+
child.flush = () => logger.flush();
|
|
456
|
+
child.close = () => { closed = true; return logger.flush(); };
|
|
457
|
+
return child;
|
|
414
458
|
};
|
|
459
|
+
logger.child = makeChild;
|
|
415
460
|
|
|
416
461
|
// Add performance tracking methods
|
|
417
462
|
if (perfTracker) {
|
|
@@ -433,52 +478,64 @@ const createLoggerInstance = (location = "Unknown", config = {}, transports, par
|
|
|
433
478
|
sampling: sampler ? sampler.getStats() : null,
|
|
434
479
|
deduplication: deduplicator ? deduplicator.getStats() : null,
|
|
435
480
|
performance: perfTracker ? perfTracker.getStats() : null,
|
|
481
|
+
...observer.stats(),
|
|
436
482
|
};
|
|
437
483
|
return stats;
|
|
438
484
|
};
|
|
439
485
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
});
|
|
486
|
+
logger.resetStats = () => {
|
|
487
|
+
sampler?.resetStats();
|
|
488
|
+
deduplicator?.resetStats();
|
|
489
|
+
observer.reset();
|
|
454
490
|
};
|
|
455
491
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
492
|
+
const timeout = config.shutdownTimeout ?? 30000;
|
|
493
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Invalid shutdownTimeout');
|
|
494
|
+
let closePromise;
|
|
495
|
+
let statsTimer;
|
|
496
|
+
logger.flush = () => withTimeout((async () => {
|
|
497
|
+
await drain(logger, state, timeout);
|
|
498
|
+
await Promise.all(logger.transports.map(t => t.flush?.()));
|
|
499
|
+
await observer.flush(timeout);
|
|
500
|
+
await drain(logger, state, timeout);
|
|
501
|
+
})(), timeout, 'Logger flush');
|
|
502
|
+
|
|
503
|
+
logger.close = () => {
|
|
504
|
+
if (closePromise) return closePromise;
|
|
505
|
+
state.closing = true;
|
|
506
|
+
clearInterval(statsTimer);
|
|
507
|
+
if (loggerInstances.get(location) === logger) loggerInstances.delete(location);
|
|
508
|
+
const owned = logger.transports.slice();
|
|
509
|
+
closePromise = withTimeout((async () => {
|
|
510
|
+
let failure;
|
|
511
|
+
try { await logger.flush(); } catch (error) { failure = error; }
|
|
512
|
+
try {
|
|
513
|
+
await withTimeout(new Promise((resolve, reject) => {
|
|
514
|
+
if (logger._writableState.finished) return resolve();
|
|
515
|
+
const onError = error => { logger.removeListener('finish', onFinish); reject(error); };
|
|
516
|
+
const onFinish = () => { logger.removeListener('error', onError); resolve(); };
|
|
517
|
+
logger.once('error', onError);
|
|
518
|
+
logger.once('finish', onFinish);
|
|
519
|
+
logger.end();
|
|
520
|
+
}), timeout, 'Logger stream close');
|
|
521
|
+
} catch (error) { failure ||= error; }
|
|
522
|
+
const results = await Promise.allSettled(owned.map(t => t._ntlClose()));
|
|
523
|
+
failure ||= results.find(result => result.status === 'rejected')?.reason;
|
|
524
|
+
if (failure) throw failure;
|
|
525
|
+
})(), timeout * 2, 'Logger close').finally(() => {
|
|
526
|
+
if (sampler) sampler.destroy();
|
|
527
|
+
if (deduplicator) deduplicator.destroy();
|
|
528
|
+
activeLoggers.delete(logger);
|
|
469
529
|
});
|
|
530
|
+
return closePromise;
|
|
470
531
|
};
|
|
471
532
|
|
|
472
|
-
// Statistics reporting interval
|
|
473
533
|
if (statsInterval > 0 && (sampler || deduplicator)) {
|
|
474
|
-
setInterval(() =>
|
|
475
|
-
|
|
476
|
-
logger.internal('Logger Statistics:', stats);
|
|
477
|
-
}, statsInterval);
|
|
534
|
+
statsTimer = setInterval(() => logger.internal('Logger Statistics:', logger.getStats()), statsInterval);
|
|
535
|
+
statsTimer.unref();
|
|
478
536
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
loggerInstances.set(location, logger);
|
|
537
|
+
if (!skipCache) loggerInstances.set(location, logger);
|
|
538
|
+
activeLoggers.add(logger);
|
|
482
539
|
|
|
483
540
|
if (debug === true) {
|
|
484
541
|
// Use pre-calculated color values
|
|
@@ -524,6 +581,7 @@ const logger = (location = "Unknown", config = {}) => {
|
|
|
524
581
|
// If cached logger doesn't have the features but config requests them, recreate
|
|
525
582
|
if (!cachedHasFeatures) {
|
|
526
583
|
// Remove from cache and recreate with new config
|
|
584
|
+
cachedLogger.close().catch(error => global.console.error('Logger replacement cleanup failed:', error));
|
|
527
585
|
loggerInstances.delete(location);
|
|
528
586
|
} else {
|
|
529
587
|
// Cached logger has features, return it
|
|
@@ -547,5 +605,7 @@ const logger = (location = "Unknown", config = {}) => {
|
|
|
547
605
|
return createLoggerInstance(location, config, transports);
|
|
548
606
|
};
|
|
549
607
|
|
|
608
|
+
logger.setupSignalHandlers = options => setupSignalHandlers(activeLoggers, options);
|
|
609
|
+
|
|
550
610
|
// Export a function to create or return the existing logger instance
|
|
551
611
|
module.exports = logger;
|