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/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
- * @returns {object} - The same logger, decorated.
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 define = (name, value) => {
265
- Object.defineProperty(logger, name, {
266
- value,
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
- define('resetStats', function resetStats() {
278
- if (features) features.resetStats();
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
- // Releases the sampler/deduplicator timers, then flushes whatever the
282
- // destination still holds. Calling it on a child tears down the state shared
283
- // with its parent — close the root logger.
284
- define('close', function close(timeout) {
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
- if (redactor) {
290
- const baseChild = logger.child;
291
- define('child', function child(bindings, options) {
292
- const safe = bindings !== null && typeof bindings === 'object'
293
- ? redactor.redactObject(bindings)
294
- : bindings;
295
- return baseChild.call(this, safe, options);
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 createLogger(opts = {}) {
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 loggerOpts = { level };
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 = { logMethod };
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;
@@ -62,6 +62,11 @@ const CONTEXT_KEYS = [
62
62
  'jobId',
63
63
  'agentUuid',
64
64
  'commandId',
65
+ 'collectionCycleId',
66
+ 'runId',
67
+ 'workId',
68
+ 'stageId',
69
+ 'deviceId',
65
70
  ];
66
71
 
67
72
  /**