ntlogger 3.0.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 +242 -17
- 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 +10 -0
- package/index.js +18 -1
- package/lib/logObserver.js +55 -0
- package/lib/logger.js +17 -0
- package/lib/pinoOtel.js +43 -0
- package/lib/secretRedaction.js +10 -1
- package/lib/secretScope.js +24 -0
- package/package.json +47 -20
- package/pino.d.ts +28 -4
- package/pino.js +144 -43
- package/transports/pino.js +5 -0
package/pino.js
CHANGED
|
@@ -80,7 +80,7 @@ function isPlainContainer(value) {
|
|
|
80
80
|
* Precedence:
|
|
81
81
|
* 1. `opts.silent` when it is a boolean
|
|
82
82
|
* 2. `NTLOGGER_SILENT` env var (`1|true|yes` / `0|false|no`, case-insensitive)
|
|
83
|
-
* 3. `NODE_ENV === 'test'`
|
|
83
|
+
* 3. `NODE_ENV === 'test'` or a nonempty `NODE_TEST_CONTEXT`
|
|
84
84
|
*
|
|
85
85
|
* Anything else (unset, or an unrecognised env value) falls through to the next step.
|
|
86
86
|
*
|
|
@@ -97,7 +97,7 @@ function isSilent(opts = {}) {
|
|
|
97
97
|
if (SILENT_FALSE.has(value)) return false;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
return process.env.NODE_ENV === 'test';
|
|
100
|
+
return process.env.NODE_ENV === 'test' || Boolean(process.env.NODE_TEST_CONTEXT);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -258,45 +258,62 @@ function composeLogMethod(redactor, features) {
|
|
|
258
258
|
* @param {object} logger - Pino logger instance.
|
|
259
259
|
* @param {object|null} features - Feature adapter from lib/pinoHooks.js.
|
|
260
260
|
* @param {object|null} redactor - Redactor from lib/secretRedaction.js.
|
|
261
|
-
* @
|
|
261
|
+
* @param {object} observer - Emitted-record observer and counters.
|
|
262
|
+
* @returns {object} - Pino logger proxy enforcing lifecycle across level changes.
|
|
262
263
|
*/
|
|
263
|
-
function decorateLogger(logger, features, redactor) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
writable: true,
|
|
268
|
-
configurable: true,
|
|
269
|
-
enumerable: false,
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
define('getStats', function getStats() {
|
|
274
|
-
return features ? features.getStats() : { sampling: null, deduplication: null };
|
|
264
|
+
function decorateLogger(logger, features, redactor, observer) {
|
|
265
|
+
const states = new WeakMap();
|
|
266
|
+
const define = (name, value) => Object.defineProperty(logger, name, {
|
|
267
|
+
value, writable: true, configurable: true, enumerable: false,
|
|
275
268
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
269
|
+
define('getStats', () => ({
|
|
270
|
+
...(features ? features.getStats() : { sampling: null, deduplication: null }),
|
|
271
|
+
...observer.stats(),
|
|
272
|
+
}));
|
|
273
|
+
define('resetStats', () => { if (features) features.resetStats(); observer.reset(); });
|
|
274
|
+
define('close', function close(timeout = DEFAULT_CLOSE_TIMEOUT) {
|
|
275
|
+
const state = states.get(this);
|
|
276
|
+
if (state.promise) return state.promise;
|
|
277
|
+
state.closed = true;
|
|
278
|
+
if (!state.parent && features) features.destroy();
|
|
279
|
+
state.promise = (async () => {
|
|
280
|
+
const start = Date.now();
|
|
281
|
+
const result = await drainLogger(this, timeout);
|
|
282
|
+
try { await observer.flush(Math.max(1, timeout - (Date.now() - start))); }
|
|
283
|
+
catch (error) { return { drained: false, error }; }
|
|
284
|
+
return result;
|
|
285
|
+
})();
|
|
286
|
+
return state.promise;
|
|
279
287
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (features) features.destroy();
|
|
286
|
-
return drainLogger(this, typeof timeout === 'number' ? timeout : DEFAULT_CLOSE_TIMEOUT);
|
|
288
|
+
const baseChild = logger.child;
|
|
289
|
+
define('child', function child(bindings, options) {
|
|
290
|
+
const safe = redactor && bindings && typeof bindings === 'object'
|
|
291
|
+
? redactor.redactObject(bindings) : bindings;
|
|
292
|
+
return wrap(baseChild.call(this, safe, options), states.get(this));
|
|
287
293
|
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
function wrap(target, parent = null) {
|
|
295
|
+
const state = { closed: false, parent, promise: null };
|
|
296
|
+
states.set(target, state);
|
|
297
|
+
const assertOpen = () => {
|
|
298
|
+
for (let current = state; current; current = current.parent) {
|
|
299
|
+
if (current.closed) throw new Error('Logger is closed');
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const cache = new Map();
|
|
303
|
+
return new Proxy(target, {
|
|
304
|
+
get(raw, key) {
|
|
305
|
+
const value = Reflect.get(raw, key, raw);
|
|
306
|
+
if (typeof value !== 'function') return value;
|
|
307
|
+
const guarded = key === 'child' || key === 'silent' || Object.hasOwn(raw.levels.values, key);
|
|
308
|
+
// Pino regenerates methods on level changes. Cache only while unchanged.
|
|
309
|
+
if (cache.get(key)?.original === value) return cache.get(key).bound;
|
|
310
|
+
const bound = (...args) => { if (guarded) assertOpen(); return value.apply(raw, args); };
|
|
311
|
+
cache.set(key, { original: value, bound });
|
|
312
|
+
return bound;
|
|
313
|
+
},
|
|
296
314
|
});
|
|
297
315
|
}
|
|
298
|
-
|
|
299
|
-
return logger;
|
|
316
|
+
return wrap(logger);
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
/**
|
|
@@ -323,9 +340,8 @@ function decorateLogger(logger, features, redactor) {
|
|
|
323
340
|
* @param {*} [opts.destination] - For tests/advanced use: a Pino destination stream. Replaces the NTL transport.
|
|
324
341
|
* @returns {object} - Pino logger with `getStats()`, `resetStats()`, `close()` and (optionally) `uninstallProcessHandlers`.
|
|
325
342
|
*/
|
|
326
|
-
function
|
|
343
|
+
function buildPinoOptions(opts = {}) {
|
|
327
344
|
const pino = require('pino');
|
|
328
|
-
const path = require('path');
|
|
329
345
|
const os = require('os');
|
|
330
346
|
|
|
331
347
|
const silent = isSilent(opts);
|
|
@@ -334,7 +350,8 @@ function createLogger(opts = {}) {
|
|
|
334
350
|
? 'silent'
|
|
335
351
|
: (opts.level || process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'));
|
|
336
352
|
|
|
337
|
-
const
|
|
353
|
+
const observer = require('./lib/logObserver').createLogObserver(opts);
|
|
354
|
+
const loggerOpts = { level, hooks: { streamWrite: observer.streamWrite } };
|
|
338
355
|
|
|
339
356
|
// --- constant fields ---------------------------------------------------
|
|
340
357
|
if (typeof opts.service === 'string' && opts.service.length > 0) {
|
|
@@ -345,6 +362,15 @@ function createLogger(opts = {}) {
|
|
|
345
362
|
// --- redaction / features (skipped entirely in silent mode) ------------
|
|
346
363
|
let redactor = null;
|
|
347
364
|
let features = null;
|
|
365
|
+
const secretScope = require('./lib/secretScope');
|
|
366
|
+
loggerOpts.hooks.streamWrite = line => {
|
|
367
|
+
// Interpolation can stringify Errors/Buffers after logMethod has run. Scrub
|
|
368
|
+
// final string fields while a secret scope is active, before observers see them.
|
|
369
|
+
if (redactor && secretScope.hasSecretValues()) {
|
|
370
|
+
line = JSON.stringify(redactor.redactObject(JSON.parse(line))) + '\n';
|
|
371
|
+
}
|
|
372
|
+
return observer.streamWrite(line);
|
|
373
|
+
};
|
|
348
374
|
|
|
349
375
|
if (!silent) {
|
|
350
376
|
const redaction = resolveRedaction(opts.redact);
|
|
@@ -377,7 +403,7 @@ function createLogger(opts = {}) {
|
|
|
377
403
|
});
|
|
378
404
|
|
|
379
405
|
const logMethod = composeLogMethod(redactor, features);
|
|
380
|
-
if (logMethod) loggerOpts.hooks =
|
|
406
|
+
if (logMethod) loggerOpts.hooks.logMethod = logMethod;
|
|
381
407
|
}
|
|
382
408
|
|
|
383
409
|
// --- context mixin ------------------------------------------------------
|
|
@@ -389,7 +415,7 @@ function createLogger(opts = {}) {
|
|
|
389
415
|
? opts.contextKeys.slice()
|
|
390
416
|
: null;
|
|
391
417
|
|
|
392
|
-
if (!silent && (opts.module || staticContext || provider)) {
|
|
418
|
+
if (!silent && (opts.module || staticContext || provider || opts.otel)) {
|
|
393
419
|
// Precedence, lowest to highest: base/service < module < context < contextProvider
|
|
394
420
|
// < fields passed on the log call itself (Pino's default mixin merge strategy
|
|
395
421
|
// assigns the merge object over the mixin output).
|
|
@@ -418,10 +444,18 @@ function createLogger(opts = {}) {
|
|
|
418
444
|
}
|
|
419
445
|
}
|
|
420
446
|
|
|
421
|
-
return out;
|
|
447
|
+
return Object.assign(out, observer.context());
|
|
422
448
|
};
|
|
423
449
|
}
|
|
424
450
|
|
|
451
|
+
return { loggerOpts, features, redactor, silent, isDev, observer };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function createLogger(opts = {}) {
|
|
455
|
+
const pino = require('pino');
|
|
456
|
+
const path = require('path');
|
|
457
|
+
const { loggerOpts, features, redactor, silent, isDev, observer } = buildPinoOptions(opts);
|
|
458
|
+
|
|
425
459
|
// --- destination / transport -------------------------------------------
|
|
426
460
|
// `opts.destination` and `transport` are mutually exclusive in Pino, so an
|
|
427
461
|
// explicit destination always wins and the NTL formatter is skipped.
|
|
@@ -437,8 +471,7 @@ function createLogger(opts = {}) {
|
|
|
437
471
|
};
|
|
438
472
|
}
|
|
439
473
|
|
|
440
|
-
const logger = destination ? pino(loggerOpts, destination) : pino(loggerOpts);
|
|
441
|
-
decorateLogger(logger, features, redactor);
|
|
474
|
+
const logger = decorateLogger(destination ? pino(loggerOpts, destination) : pino(loggerOpts), features, redactor, observer);
|
|
442
475
|
|
|
443
476
|
if (opts.processHandlers) {
|
|
444
477
|
const handlerOpts = typeof opts.processHandlers === 'object' ? opts.processHandlers : {};
|
|
@@ -455,3 +488,71 @@ function createLogger(opts = {}) {
|
|
|
455
488
|
|
|
456
489
|
module.exports.createLogger = createLogger;
|
|
457
490
|
module.exports.isSilent = isSilent;
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Plain Pino options for framework-owned loggers. No transport, process
|
|
494
|
+
* handlers, or feature timers are created; the framework owns the lifecycle.
|
|
495
|
+
* Supply custom serializers here so their output is redacted too.
|
|
496
|
+
*/
|
|
497
|
+
function createPinoOptions(opts = {}) {
|
|
498
|
+
for (const key of ['sampling', 'rateLimit', 'deduplication', 'destination', 'processHandlers']) {
|
|
499
|
+
if (opts[key] !== undefined) {
|
|
500
|
+
throw new TypeError(`createPinoOptions does not support ${key}; use createLogger instead`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const pino = require('pino');
|
|
504
|
+
const { loggerOpts, redactor } = buildPinoOptions(opts);
|
|
505
|
+
const serializers = {
|
|
506
|
+
req: req => ({
|
|
507
|
+
method: req.method,
|
|
508
|
+
url: req.url,
|
|
509
|
+
version: req.headers && req.headers['accept-version'],
|
|
510
|
+
host: req.host,
|
|
511
|
+
remoteAddress: req.ip || (req.socket && req.socket.remoteAddress),
|
|
512
|
+
remotePort: req.socket && req.socket.remotePort,
|
|
513
|
+
}),
|
|
514
|
+
res: res => ({ statusCode: res.statusCode }),
|
|
515
|
+
err: pino.stdSerializers.err,
|
|
516
|
+
...opts.serializers,
|
|
517
|
+
};
|
|
518
|
+
loggerOpts.serializers = {};
|
|
519
|
+
for (const [key, serialize] of Object.entries(serializers)) {
|
|
520
|
+
if (typeof serialize !== 'function') throw new TypeError(`Invalid serializer: ${key}`);
|
|
521
|
+
loggerOpts.serializers[key] = redactor
|
|
522
|
+
? value => redactor.redactObject(serialize(value))
|
|
523
|
+
: serialize;
|
|
524
|
+
}
|
|
525
|
+
if (redactor) {
|
|
526
|
+
// Pino invokes formatters before serializers. Preserve their input objects
|
|
527
|
+
// (including prototypes/getters) and redact the serialized output instead.
|
|
528
|
+
loggerOpts.formatters.log = obj => {
|
|
529
|
+
const plain = {};
|
|
530
|
+
for (const key of Object.keys(obj)) {
|
|
531
|
+
if (!Object.hasOwn(serializers, key)) plain[key] = obj[key];
|
|
532
|
+
}
|
|
533
|
+
const safe = redactor.redactObject(plain);
|
|
534
|
+
for (const key of Object.keys(obj)) {
|
|
535
|
+
if (Object.hasOwn(serializers, key)) safe[key] = obj[key];
|
|
536
|
+
}
|
|
537
|
+
return safe;
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
return loggerOpts;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Capture the actual JSON records without a console or worker transport. */
|
|
544
|
+
function createTestLogger(opts = {}) {
|
|
545
|
+
const records = [];
|
|
546
|
+
const logger = createLogger({
|
|
547
|
+
...opts,
|
|
548
|
+
silent: opts.silent ?? false,
|
|
549
|
+
level: opts.level ?? 'trace',
|
|
550
|
+
destination: { write(line) { records.push(JSON.parse(line)); } },
|
|
551
|
+
});
|
|
552
|
+
return { logger, records };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
module.exports.createPinoOptions = createPinoOptions;
|
|
556
|
+
module.exports.createTestLogger = createTestLogger;
|
|
557
|
+
|
|
558
|
+
module.exports.withSecretValues = require('./lib/secretScope').withSecretValues;
|