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 CHANGED
@@ -1,9 +1,22 @@
1
1
  # NightTimeLogger
2
2
 
3
- NightTimeLogger is a custom logging wrapper built on top of the Winston logging library. It provides a ready-to-go solution for integrating advanced logging functionalities into Node.js applications with ease.
3
+ NightTimeLogger provides Winston and Pino logging backends with shared logging conventions. Install the backend your application uses.
4
4
 
5
5
  [![Node.js Package](https://github.com/NightSquawk/NightTimeLogger/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/NightSquawk/NightTimeLogger/actions/workflows/main.yml)
6
6
 
7
+ This maintenance update requires Node.js 22.22.2 or newer; Node 22 and 24 are tested.
8
+
9
+ Process signal handling is now **opt-in**. Applications that relied on automatic shutdown must register it once:
10
+
11
+ ```javascript
12
+ const logger = require('ntlogger');
13
+ const unregister = logger.setupSignalHandlers({ timeout: 30000 });
14
+ ```
15
+
16
+ Alternatively, keep your application's own shutdown handlers and `await log.close()` there. Importing the package no longer changes process exception or signal handling. Flush and close reject on delivery errors/timeouts, so callers should handle rejected promises. Close is idempotent, waits for transport cleanup, and removes the logger from its cache. Children share their parent's transports; closing a child flushes but does not close the parent. Log calls after close throw on both backends. See the Pino lifecycle note below for its drain result contract.
17
+
18
+ OpenTelemetry users must upgrade the logs SDK/API/HTTP exporter together to `^0.222.0` and resources to `^2.11.0`. Earlier SDK versions are no longer advertised as compatible. Database transports now own separate pools, use event timestamps in UTC, accept `ssl` configuration, and require simple SQL table identifiers. Existing tables are not altered automatically.
19
+
7
20
  ## Features
8
21
 
9
22
  - Custom log levels for fine-grained control over logging output.
@@ -18,15 +31,57 @@ NightTimeLogger is a custom logging wrapper built on top of the Winston logging
18
31
  - **Performance metrics** - Development-only performance tracking with `time()` and `timeEnd()` methods.
19
32
  - **Non-blocking operations** - All logging operations are asynchronous and won't block your application.
20
33
  - **TypeScript support** - Full TypeScript definitions included for excellent IDE integration (IntelliJ, VS Code, etc.).
34
+ - [Log Contract](docs/log-contract.md) — cross-language field, level, context, redaction, and OTLP mapping contract shared by Node, Python, and Rust emitters.
21
35
 
22
36
  ## Installation
23
37
 
24
- To install NightTimeLogger, use npm:
38
+ For Pino or Fastify:
25
39
 
26
40
  ```bash
27
- npm install ntlogger
41
+ npm install ntlogger pino
28
42
  ```
29
43
 
44
+ Import helpers from `ntlogger/pino`. This installation does not pull in Winston or
45
+ its transports. The small `split2` dependency supports the development formatter.
46
+
47
+ For the Winston API and its plugins:
48
+
49
+ ```bash
50
+ npm install ntlogger winston winston-transport
51
+ ```
52
+
53
+ Import `ntlogger` as before. Both backends are optional peers, so install the one
54
+ you use; applications using both entry points should install all three peers.
55
+
56
+ **Upgrade note:** Winston and `winston-transport` were previously installed by
57
+ ntlogger automatically. Existing Winston consumers must add them as direct
58
+ production dependencies using the command above before upgrading. This is a
59
+ breaking installation change in v4.0.0. See [release notes](docs/releases/v4.0.0.md)
60
+ for the complete migration checklist.
61
+
62
+ ### Optional plugin backends
63
+
64
+ Plugin backends are **optional peer dependencies** - they are not installed for you.
65
+ Install only the ones whose plugins you actually enable:
66
+
67
+ | Plugin | Install |
68
+ | --- | --- |
69
+ | Sentry | `npm install @sentry/node` |
70
+ | MySQL | `npm install mysql2` |
71
+ | Postgres | `npm install pg` |
72
+ | OpenTelemetry | `npm install @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions` |
73
+ | Discord, Teams, OpenObserve, Syslog, Jest | none - no extra packages required |
74
+
75
+ Plugin modules are resolved lazily, so a missing plugin backend never breaks
76
+ `require('ntlogger')` when the Winston peers are installed. The loader reports an actionable error naming the package and
77
+ the install command, skips that one plugin, and keeps every other plugin working.
78
+ See [plugins/README.md](https://github.com/NightSquawk/NightTimeLogger/blob/main/plugins/README.md)
79
+ for per-plugin configuration.
80
+
81
+ > **Upgrading:** `@sentry/node`, `mysql2` and `pg` used to be hard dependencies of
82
+ > `ntlogger`. If you relied on them being installed transitively, add them to your own
83
+ > `package.json`.
84
+
30
85
  ## Usage
31
86
 
32
87
  ```javascript
@@ -62,9 +117,13 @@ Check out [Full Configuration](https://github.com/NightSquawk/NightTimeLogger/bl
62
117
  - `console`: Whether to enable console logging. Defaults to `true`.
63
118
  - `file`: Whether to enable file logging. Defaults to `true`.
64
119
  - `path`: The directory path where log files will be saved.
120
+ - `filename`: Combined log filename (default: `combined.log`); error/fatal files retain their names.
121
+ - `shutdownTimeout`: Maximum wait in milliseconds for flush or stream shutdown (default: `30000`).
65
122
  - `maxSize`: The maximum size (in bytes) for each log file.
66
123
  - `maxFiles`: The maximum number of log files to retain (rotating file strategy).
67
124
  - `timestamp`: Whether to include timestamps in log messages. Defaults to `true`.
125
+ - `skipCache`: Whether to bypass the logger instance cache and always build a new instance for this location name. Defaults to `false` (instances are cached per location name; child loggers always skip the cache).
126
+ - `plugins`: Array of plugin configurations, each `{ name, enabled, config }`. All three fields are required - `enabled` must be `true` and `config` must be an object (use `{}` when the plugin takes no options), otherwise the plugin is skipped. Backends whose optional peer dependency is missing are reported and skipped without affecting the others. Defaults to `[]`.
68
127
  - `debug`: Whether to enable debug mode, which logs internal messages. Defaults to `false`.
69
128
  - `reportPath`: Whether to enable call site path reporting. When enabled, automatically captures the file path, line number, column number, and call chain where each log statement is executed. The path is added as metadata (JSON field `filePath`), not in the formatted message string. Defaults to `false`.
70
129
  - `sampling`: Object with level-based sampling rates (e.g., `{ debug: 0.01, trace: 0.001 }`). Values between 0.0 and 1.0, where 1.0 = log all, 0.1 = log 10%. Defaults to `{}` (no sampling).
@@ -204,6 +263,400 @@ await log.flush();
204
263
  await log.close();
205
264
  ```
206
265
 
266
+ ## Pino Support
267
+
268
+ `ntlogger/pino` is a separate entry point for projects that already use [Pino](https://getpino.io).
269
+ It provides an NTL-styled transport plus `createLogger()`, which wires the NTL conventions
270
+ (module label, structured context, secret redaction, sampling, graceful shutdown) onto a real
271
+ Pino logger.
272
+
273
+ `pino` is an optional peer dependency, so Winston-only users do not install it.
274
+ `split2` is a small optional dependency installed by default for the Pino formatter.
275
+
276
+ ### Transport only
277
+
278
+ ```javascript
279
+ const pino = require('pino');
280
+
281
+ const log = pino({
282
+ transport: { target: 'ntlogger/pino', options: { colorize: true, defaultModule: 'MyApp' } },
283
+ });
284
+
285
+ log.info({ tenantId: 'acme' }, 'server started');
286
+ // 2025-01-01 12:00:00 [info ] [MyApp]: {tenantId=acme} server started
287
+ ```
288
+
289
+ The transport renders a dim `{key=value ...}` suffix for the top-level context keys
290
+ `tenantId`, `userId`, `correlationId`, `traceId`, `spanId`, `jobId`, `agentUuid`, `commandId`
291
+ (exported as `CONTEXT_KEYS`), and pairs pino-http/Fastify request/response lines.
292
+
293
+ ### `createLogger(opts)`
294
+
295
+ ```javascript
296
+ const { createLogger } = require('ntlogger/pino');
297
+
298
+ const log = createLogger({ module: 'OrderService', service: 'checkout-api' });
299
+
300
+ log.info({ tenantId: 'acme' }, 'order received');
301
+ ```
302
+
303
+ `createLogger()` returns a **real Pino logger**, so the call signature is object-first:
304
+ `log.info({ tenantId }, 'message')` — not Winston's `log.info('message', meta)`.
305
+
306
+ In development it attaches the NTL transport for human-readable output; with
307
+ `NODE_ENV=production` it writes plain NDJSON so log shippers get clean JSON.
308
+
309
+ | Option | Type | Default | Purpose |
310
+ | --- | --- | --- | --- |
311
+ | `level` | `string` | `LOG_LEVEL`, else `debug` (dev) / `info` (production) | Pino level. |
312
+ | `module` | `string` | — | Adds `module` to every line and labels the transport output. |
313
+ | `defaultModule` | `string` | — | Transport label when `module` is not set. |
314
+ | `colorize` | `boolean` | transport default | Forwarded to the NTL transport. |
315
+ | `service` | `string` | — | Constant top-level `service` field (a core field of the log contract). |
316
+ | `context` | `object` | — | Static top-level fields merged into every line. |
317
+ | `contextProvider` | `() => object` | — | Called per log call; its result is merged at the top level. |
318
+ | `contextKeys` | `string[]` | — | When set, only these keys are taken from the provider result. |
319
+ | `redact` | `false \| string[] \| object` | defaults on | Secret redaction (see below). |
320
+ | `sampling` | `object` | — | Per-level sampling rates, same shape as the Winston path. |
321
+ | `rateLimit` | `object` | — | Per-level rate limits, same shape as the Winston path. |
322
+ | `deduplication` | `object \| boolean` | — | Duplicate-message collapsing, same shape as the Winston path. |
323
+ | `silent` | `boolean` | see below | Force silent mode on or off. |
324
+ | `processHandlers` | `boolean \| object` | `false` | Opt-in crash/signal handlers. |
325
+ | `destination` | Pino destination | — | Test/advanced escape hatch; replaces the NTL transport. |
326
+
327
+ The returned logger additionally exposes `getStats()`, `resetStats()`, `close(timeout?)` and —
328
+ when `processHandlers` is used — `uninstallProcessHandlers()`. These are own properties of the
329
+ returned instance, so `logger.child()` inherits them without anything being written to Pino's
330
+ shared prototype.
331
+
332
+ ### Request context with AsyncLocalStorage
333
+
334
+ ```javascript
335
+ const { AsyncLocalStorage } = require('async_hooks');
336
+ const { createLogger } = require('ntlogger/pino');
337
+
338
+ const als = new AsyncLocalStorage();
339
+
340
+ const log = createLogger({
341
+ module: 'API',
342
+ context: { region: 'us-east-1' }, // constant
343
+ contextProvider: () => als.getStore(), // per call
344
+ contextKeys: ['tenantId', 'userId', 'correlationId'],
345
+ });
346
+
347
+ app.use((req, res, next) => als.run({ tenantId: req.tenantId, correlationId: req.id }, next));
348
+
349
+ log.info('handled'); // -> { module: 'API', region: 'us-east-1', tenantId: ..., correlationId: ... }
350
+ log.info({ tenantId: 'override' }, 'handled');
351
+ ```
352
+
353
+ Precedence, lowest to highest:
354
+
355
+ `service`/`pid`/`hostname` → `module` → `context` → `contextProvider` → fields passed on the log call.
356
+
357
+ `contextKeys` lets an application hand the provider a large store and expose only a few fields.
358
+ A provider that throws never breaks logging: the context is dropped and a single
359
+ `NTLoggerContextWarning` is emitted via `process.emitWarning`. Non-object return values
360
+ (including arrays and `null`) are ignored.
361
+
362
+ ### Secret redaction
363
+
364
+ Redaction is **on by default**. It covers the message string, string and plain-object
365
+ interpolation arguments, the merge object (recursively), `Error` messages and stacks, and child
366
+ bindings. Keys such as `password`, `token`, `authorization`, `apiKey` and `cookie` are replaced
367
+ wholesale; known secret shapes (Bearer/Basic headers, JWTs, AWS/GitHub/Slack/Stripe keys, PEM
368
+ blocks, URL credentials, Discord webhook tokens) are replaced inside free text.
369
+
370
+ ```javascript
371
+ createLogger({ redact: false }); // disable entirely
372
+ createLogger({ redact: ['deviceFingerprint'] }); // add app-specific key names
373
+ createLogger({ redact: ['payload.cardNumber'] }); // Pino-style path -> Pino's own redact
374
+ createLogger({ redact: { replacement: '***', extraPatterns: [/CUST-\d{8}/g] } });
375
+ ```
376
+
377
+ A string array is split: entries containing `.`, `[`, `]` or `*` are forwarded to Pino's native
378
+ `redact` option (which replaces with Pino's `[Redacted]`), everything else becomes `extraKeys`
379
+ for ntlogger's redactor (which replaces with `[REDACTED]`). An object is passed straight to
380
+ `createRedactor()` and accepts `keys`, `extraKeys`, `patterns`, `extraPatterns`, `replacement`,
381
+ `maxDepth` and `maxStringLength`.
382
+
383
+ ```javascript
384
+ log.error(new Error('payment failed for Bearer abc123def456...'));
385
+ // -> { "err": { "type": "Error", "message": "payment failed for Bearer [REDACTED]", "stack": "..." },
386
+ // "msg": "payment failed for Bearer [REDACTED]" }
387
+ ```
388
+
389
+ ### Sampling, rate limiting and deduplication
390
+
391
+ The same configuration shapes as the Winston path (see *Log Sampling and Rate Limiting* and
392
+ *Log Deduplication*), applied through Pino's single `hooks.logMethod`.
393
+
394
+ ```javascript
395
+ const log = createLogger({
396
+ module: 'Ingest',
397
+ sampling: { debug: 0.1 },
398
+ rateLimit: { info: { max: 100, window: 60000 } },
399
+ deduplication: { enabled: true, threshold: 3, window: 60000 },
400
+ });
401
+
402
+ log.getStats(); // { sampling: {...}, deduplication: {...}, levels: {...}, hookErrors: 0 }
403
+ log.resetStats();
404
+ await log.close(); // releases the sampler/dedup timers, then flushes the destination
405
+ ```
406
+
407
+ `fatal` is never sampled, rate limited or deduplicated unless the configuration names it
408
+ explicitly. Per-level counts are collected even without sampling or deduplication.
409
+
410
+ ### Silent mode
411
+
412
+ Resolution order:
413
+
414
+ 1. `opts.silent`, when it is a boolean
415
+ 2. `NTLOGGER_SILENT` — `1`/`true`/`yes` → silent, `0`/`false`/`no` → not silent
416
+ 3. `NODE_ENV === 'test'` or nonempty `NODE_TEST_CONTEXT` → silent
417
+
418
+ A silent logger runs at Pino level `silent`, attaches no transport and emits no
419
+ records, counts, or observer callbacks. `isSilent(opts)` is exported for testing the resolution.
420
+
421
+ ```javascript
422
+ const log = createLogger({ module: 'API' }); // silent under Jest (NODE_ENV=test)
423
+ const log = createLogger({ module: 'API', silent: false }); // force output in a test
424
+ ```
425
+
426
+ Node's built-in test runner is detected through `NODE_TEST_CONTEXT`; `node --test`
427
+ scripts no longer need `NTLOGGER_SILENT=1` for the Pino API.
428
+
429
+ ### Fastify integration
430
+
431
+ Use the plain options factory to retain Fastify's normal route logger types:
432
+
433
+ ```javascript
434
+ const Fastify = require('fastify');
435
+ const { createPinoOptions } = require('ntlogger/pino');
436
+
437
+ const app = Fastify({
438
+ logger: createPinoOptions({
439
+ service: 'api',
440
+ contextProvider: () => requestContext.getStore(), // your AsyncLocalStorage
441
+ serializers: {
442
+ req: req => ({ method: req.method, url: req.url, headers: req.headers }),
443
+ res: reply => ({ statusCode: reply.statusCode, headers: reply.getHeaders() }),
444
+ },
445
+ }),
446
+ });
447
+ app.addHook('preHandler', async req => {
448
+ req.log.debug({ body: req.body }, 'parsed request body');
449
+ });
450
+ ```
451
+
452
+ The factory emits JSON options without creating a transport or installing process
453
+ handlers. Fastify owns the logger and shutdown. It shares `createLogger()`'s
454
+ service/module fields, context provider, silent-mode resolution, and redaction.
455
+ Default request/response serializers retain Fastify's summary fields; headers and
456
+ bodies are opt-in. Bodies are available after parsing, not in the initial request
457
+ log. See [Fastify's logging documentation](https://fastify.dev/docs/latest/Reference/Logging/).
458
+
459
+ Pass custom serializers **into the factory** so redaction runs on their output.
460
+ Replacing returned formatters, hooks, or serializers (including route-level serializer
461
+ overrides) can bypass protection. Ordinary log objects, message strings, and serialized
462
+ request/response/error fields are redacted. Arbitrary child bindings are not covered by
463
+ the factory's general deep redactor: keep them to identifiers such as `reqId` and
464
+ `tenantId`, or protect specific binding paths with `redact: ['account.secret']`.
465
+ `createLogger()` additionally redacts arbitrary child bindings.
466
+
467
+ Sampling, rate limiting, deduplication, destinations, and process handlers are not
468
+ accepted by this factory; use `createLogger()` for those features. Framework-owned
469
+ loggers do not gain ntlogger's `getStats()`, `resetStats()`, or `close()` methods.
470
+
471
+ ### Capturing test logs
472
+
473
+ ```javascript
474
+ const { createTestLogger } = require('ntlogger/pino');
475
+ const { logger, records } = createTestLogger();
476
+ logger.child({ jobId: 'j1' }).warn({ password: 'secret' }, 'retry');
477
+ // records[0] includes level: 40, jobId: 'j1', password: '[REDACTED]', msg: 'retry'
478
+ await logger.close();
479
+ ```
480
+
481
+ Records are captured synchronously after Pino serialization and redaction, with no
482
+ console output or worker transport. The helper defaults to level `trace` and overrides
483
+ automatic/environment silence; explicit `silent: true` still captures nothing.
484
+ Sampling and level filtering apply normally. Clear captured records with
485
+ `records.length = 0` when needed.
486
+
487
+ ### Pino lifecycle
488
+
489
+ Pino's `close(timeout?)` marks the logger closed immediately, releases root feature
490
+ timers, and drains the destination and pending observer work. It is idempotent and
491
+ resolves to `{ drained: true }` or `{ drained: false, error }`; inspect that result.
492
+ Winston's close continues to reject on flush failures.
493
+
494
+ Calls after close throw, including disabled levels, previously extracted log methods,
495
+ and descendants of a closed logger. Closing a child leaves its parent and siblings
496
+ open and keeps shared feature timers running. Changing a closed logger's level does
497
+ not reopen it. Shutdown does not end a caller-supplied destination or shut down an
498
+ application-owned OTel provider.
499
+
500
+ ### Tee callbacks and emitted counts
501
+
502
+ Both `createLogger()` from `ntlogger/pino` and the Winston configuration accept `onLog`:
503
+
504
+ ```javascript
505
+ const { createLogger } = require('ntlogger/pino');
506
+ const log = createLogger({
507
+ onLog: record => dashboard.publish(record), // your dashboard/WebSocket adapter
508
+ });
509
+ log.child({ jobId: 'j1' }).warn({ deviceId: 'd1' }, 'retry');
510
+ console.log(log.getStats().levels.warn); // 1
511
+ await log.close();
512
+ ```
513
+
514
+ The callback receives a detached record after redaction, context merging and filtering.
515
+ Pino supplies its final serialized fields, including numeric `level` and `msg`. Winston
516
+ supplies its record before transport-specific formatting, with a level name and `message`. Winston now also applies the default redactor before dispatching to its
517
+ transports; `redact: false` explicitly disables protection on either backend.
518
+
519
+ `getStats().levels` contains zero-initialized trace/debug/info/warn/error/fatal/internal
520
+ counts shared with children. These count records prepared for output, not successful
521
+ network delivery. Sampling/rate-limit/dedup drops and disabled levels do not count.
522
+ Winston's asynchronous calls are reflected after `await log.flush()`.
523
+ `resetStats()` clears level, sampling, dedup and `hookErrors` counters.
524
+
525
+ Callback exceptions and rejected promises do not interrupt primary logging; they
526
+ increment `hookErrors`. Logs made by callbacks still reach primary output but do not
527
+ invoke observers again, including across asynchronous continuations. Async callbacks
528
+ are awaited by Pino close and Winston flush/close, bounded by the shutdown timeout.
529
+ `onLogMaxPending` caps in-flight callback promises (default 100; zero disables callbacks).
530
+ At capacity, the newest callback is dropped; the primary log and OTel export continue.
531
+ Already-running promises are never cancelled. `getStats().onLog` reports `pending`,
532
+ `dropped`, and `limit`; reset clears drops but preserves the live pending gauge.
533
+ Callback mutation cannot change the primary log or its OTel export. Return the delivery
534
+ promise if it needs to be included in shutdown; detached background work is not tracked.
535
+
536
+ Pino **9.14 or newer** is required for the serialized-output hook. This raises the old
537
+ Pino 8 minimum as part of the v4.0.0 installation migration.
538
+
539
+ ### Scoped credential values
540
+
541
+ Scanner defaults include `community`, `authPassword`, `privPassword`, and `passphrase`,
542
+ including case/hyphen/underscore aliases and short or quoted free-text values. For an
543
+ opaque credential that might appear in an error without a recognizable key:
544
+
545
+ ```javascript
546
+ const { createLogger, withSecretValues } = require('ntlogger/pino');
547
+ const log = createLogger({ module: 'scanner' });
548
+ await withSecretValues([credentialValue], async () => {
549
+ log.info({ runId, workId }, 'scan started');
550
+ await runScan();
551
+ });
552
+ ```
553
+
554
+ `withSecretValues` is also exported by the Winston entry point. It scopes literal-value
555
+ redaction to the callback and its async descendants, restores the parent afterward,
556
+ and handles JSON-escaped forms. Use nonempty strings, at most 128 distinct values of
557
+ 65536 characters each. Short values can obscure unrelated text. Descendant tasks inherit
558
+ secrets until they finish; avoid unrelated background work inside the scope. Explicit
559
+ `redact: false` disables this protection along with other redaction. Context fields
560
+ `runId`, `workId`, `stageId`, `deviceId`, and `collectionCycleId` now render in Pino's
561
+ pretty context suffix as well as remaining searchable in JSON.
562
+
563
+ ### Python companion and shared contract
564
+
565
+ A dependency-free Python 3.10+ companion is maintained in [python/](python/README.md).
566
+ It provides `protect_handler`, `NdjsonFormatter`, `SafeFormatter`, `log_context`,
567
+ `secret_values`, `SnapshotFilter`, and `create_test_logger`. Existing agent handlers,
568
+ per-mode filenames, rotation and permissions remain application-owned. The companion
569
+ adds sanitized structured records and exception details without requiring Node.
570
+ Install locally with `python -m pip install ./python`; the new Python package is not
571
+ published. Agent adoption is documented rather than applied to its repository.
572
+
573
+ Shared redaction snapshots and conformance fixtures are exported at
574
+ `ntlogger/contract/redaction.json` and `ntlogger/contract/conformance.json`. The Python
575
+ wheel bundles the same defaults. `npm run test:contract` verifies generation; both
576
+ language suites run the fixtures, including Python TRACE 5 → Pino 10.
577
+
578
+ ### OpenTelemetry correlation and export
579
+
580
+ Use your existing logs SDK provider and processors; ntlogger does not create a global
581
+ provider or a second pipeline. With a batch processor and OTLP exporter already attached:
582
+
583
+ ```javascript
584
+ const { createLogger } = require('ntlogger/pino');
585
+ const log = createLogger({ otel: { loggerProvider, name: 'worker' } });
586
+ // Inside your application's active span:
587
+ log.info({ jobId: 'j1' }, 'job started');
588
+ const result = await log.close(); // calls the supplied provider's forceFlush(), if present
589
+ // Shut down the application-owned provider separately when all producers have stopped.
590
+ ```
591
+
592
+ Enable a context manager/tracing SDK in the application so the active span is available.
593
+ The bridge adds `traceId`, `spanId` and `traceFlags` to Pino JSON and supplies the active
594
+ context to the OTel SDK, with correct severity mapping and redacted attributes. Nested
595
+ attributes are JSON strings; Pino error fields also map to OTel exception attributes.
596
+ No active span means no invented trace IDs. `otel: true` uses the globally registered
597
+ logs provider; without a configured SDK provider that provider is a no-op. Install
598
+ `@opentelemetry/api` and `@opentelemetry/api-logs` plus your chosen SDK/exporter packages
599
+ only when enabling this option. Both backends support the option; do not also enable
600
+ the Winston OTel plugin for the same output or records will be exported twice.
601
+
602
+ `createPinoOptions()` also accepts `onLog` and `otel`. Fastify still owns its lifecycle;
603
+ track async callback promises and flush the provider in your application shutdown hook.
604
+ It does not gain ntlogger's counters or close method. SDK exporters can report delivery
605
+ errors through their own diagnostics rather than rejecting provider flush; a successful
606
+ drain alone is not a delivery acknowledgment.
607
+
608
+ The library tests verify real SDK records and HTTP OTLP delivery to a local collector.
609
+ The API/worker deployments still need adoption and a check of their own collector routing.
610
+
611
+ ### Structured logging lint rule
612
+
613
+ The optional ESLint plugin supports ESLint 9+ and has no runtime logger dependencies.
614
+ Enable it for Pino call sites in a flat configuration:
615
+
616
+ ```javascript
617
+ const ntlogger = require('ntlogger/eslint');
618
+ module.exports = [{
619
+ files: ['src/**/*.js'],
620
+ plugins: { ntlogger },
621
+ rules: {
622
+ 'ntlogger/prefer-object-first': ['warn', {
623
+ loggerNames: ['log', 'logger', 'req.log', 'request.log'],
624
+ }],
625
+ },
626
+ }];
627
+ ```
628
+
629
+ It reports interpolated first-argument template strings. `--fix` adds searchable fields
630
+ while retaining the original message only when each expression is a known primitive
631
+ `const` identifier and there are no other arguments. Calls, getters, mutable values and
632
+ complex expressions are reported without a fix. Convert those manually, for example
633
+ `log.info({ deviceId: device.id }, 'scan started')`. Receiver matching is configurable
634
+ and syntactic; scope the rule to Pino code, since Winston's message-first methods have
635
+ a different contract. Consumer call-site migrations are not performed in this repository.
636
+
637
+ ### Process handlers
638
+
639
+ Opt-in only — importing the library never installs process listeners.
640
+
641
+ ```javascript
642
+ const log = createLogger({
643
+ module: 'API',
644
+ processHandlers: {
645
+ timeout: 4000,
646
+ signals: ['SIGTERM', 'SIGINT'],
647
+ onShutdown: async reason => { await server.close(); },
648
+ },
649
+ });
650
+
651
+ // later
652
+ log.uninstallProcessHandlers();
653
+ ```
654
+
655
+ `installProcessHandlers(logger, opts)` and `drainLogger(logger, timeout)` are also exported
656
+ directly for loggers not built by `createLogger()`.
657
+
658
+ See `examples/pino-advanced.js` for all of the above in one runnable file.
659
+
207
660
  ## Custom Levels and Colors
208
661
  NightTimeLogger provides custom log levels and colors for enhanced logging experience:
209
662
 
@@ -215,7 +668,7 @@ NightTimeLogger provides custom log levels and colors for enhanced logging exper
215
668
  - warn: 2
216
669
  - error: 1
217
670
  - fatal: 0
218
- - internal: -1
671
+ - internal: 6
219
672
 
220
673
  ### Colors:
221
674
 
@@ -231,4 +684,11 @@ NightTimeLogger provides custom log levels and colors for enhanced logging exper
231
684
  NightTimeLogger supports both file and console log formatters. File-formatted logs are stored in the project's root `/logs` directory.
232
685
 
233
686
  ## License
234
- NightTimeLogger is licensed under the [GPL-3.0 License](https://opensource.org/licenses/GPL-3.0).
687
+ NightTimeLogger is licensed under the [GPL-3.0 License](https://opensource.org/licenses/GPL-3.0).
688
+ ## Maintenance checks
689
+
690
+ Run `npm ci` and `npm run test:coverage` for the complete suite. MySQL and PostgreSQL integration tests start disposable Docker containers; CI sets `REQUIRE_DOCKER_TESTS=1` so they cannot silently skip. `npm run test:unit` excludes those two Docker suites for local work. `npm audit` checks all dependencies; `npm audit --omit=dev` checks production dependencies.
691
+
692
+ HTTP transports (Discord, Teams, OpenObserve) accept `timeout` (5000 ms per attempt), `maxRetries` (3), `retryDelay` (1000 ms), and `maxPending` (100 outstanding deliveries). They retry connection failures, HTTP 429 and 5xx with bounded backoff. Queue overflow, terminal HTTP errors, and timeouts surface through callbacks/flush. Delivery is best-effort with retries, not durable storage; an ambiguous network failure can lead to a duplicate on retry. OpenObserve retains batching and flush now waits for responses. Increase `shutdownTimeout` if you configure longer retries.
693
+
694
+ Packed installation checks: run `npm run test:package`. This requires npm registry access and uses temporary consumer directories to verify Pino-only and Winston-only installs.
@@ -0,0 +1,133 @@
1
+ {
2
+ "version": 1,
3
+ "levels": [
4
+ {
5
+ "python": 5,
6
+ "pino": 10,
7
+ "name": "trace"
8
+ },
9
+ {
10
+ "python": 10,
11
+ "pino": 20,
12
+ "name": "debug"
13
+ },
14
+ {
15
+ "python": 20,
16
+ "pino": 30,
17
+ "name": "info"
18
+ },
19
+ {
20
+ "python": 30,
21
+ "pino": 40,
22
+ "name": "warn"
23
+ },
24
+ {
25
+ "python": 40,
26
+ "pino": 50,
27
+ "name": "error"
28
+ },
29
+ {
30
+ "python": 50,
31
+ "pino": 60,
32
+ "name": "fatal"
33
+ }
34
+ ],
35
+ "redaction": [
36
+ {
37
+ "input": {
38
+ "community": "public",
39
+ "authPassword": "ab",
40
+ "priv_password": "private",
41
+ "passphrase": "with spaces",
42
+ "privateKey": "key",
43
+ "runId": "r1"
44
+ },
45
+ "expected": {
46
+ "community": "[REDACTED]",
47
+ "authPassword": "[REDACTED]",
48
+ "priv_password": "[REDACTED]",
49
+ "passphrase": "[REDACTED]",
50
+ "privateKey": "[REDACTED]",
51
+ "runId": "r1"
52
+ }
53
+ },
54
+ {
55
+ "input": "community=public authPassword=ab passphrase=\"with spaces\"",
56
+ "expected": "community=[REDACTED] authPassword=[REDACTED] passphrase=[REDACTED]"
57
+ },
58
+ {
59
+ "input": "{'privPassword': 'ab', 'community': 'x'}",
60
+ "expected": "{'privPassword': [REDACTED], 'community': [REDACTED]}"
61
+ },
62
+ {
63
+ "input": {
64
+ "nested": [
65
+ {
66
+ "authorization": "Bearer example-secret",
67
+ "X-Device-Installation-Secret": "key"
68
+ }
69
+ ],
70
+ "agentUuid": "a1",
71
+ "token": null
72
+ },
73
+ "expected": {
74
+ "nested": [
75
+ {
76
+ "authorization": "[REDACTED]",
77
+ "X-Device-Installation-Secret": "[REDACTED]"
78
+ }
79
+ ],
80
+ "agentUuid": "a1",
81
+ "token": null
82
+ }
83
+ },
84
+ {
85
+ "input": "https://user:secret@example.test/path Bearer abcdefghijklmnop",
86
+ "expected": "https://user:[REDACTED]@example.test/path Bearer [REDACTED]"
87
+ },
88
+ {
89
+ "input": "-----BEGIN PRIVATE KEY-----\nexample\n-----END PRIVATE KEY-----",
90
+ "expected": "[REDACTED]"
91
+ },
92
+ {
93
+ "input": "password=secret",
94
+ "expected": "password=[REDACTED]"
95
+ },
96
+ {
97
+ "input": "AKIA1234567890ABCDEF",
98
+ "expected": "[REDACTED]"
99
+ },
100
+ {
101
+ "input": "eyJabcdef.abcdefgh.abcdefgh",
102
+ "expected": "[REDACTED]"
103
+ },
104
+ {
105
+ "input": "ghp_aaaaaaaaaaaaaaaaaaaaaaaa",
106
+ "expected": "[REDACTED]"
107
+ },
108
+ {
109
+ "input": "xoxb-aaaaaaaaaaaa",
110
+ "expected": "[REDACTED]"
111
+ },
112
+ {
113
+ "input": "sk_live_aaaaaaaaaaaa",
114
+ "expected": "[REDACTED]"
115
+ },
116
+ {
117
+ "input": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
118
+ "expected": "Basic [REDACTED]"
119
+ },
120
+ {
121
+ "input": "https://discord.com/api/webhooks/123456789/abcXYZ-secret",
122
+ "expected": "https://discord.com/api/webhooks/123456789/[REDACTED]"
123
+ },
124
+ {
125
+ "input": "passphrase=\"unterminated with spaces",
126
+ "expected": "passphrase=[REDACTED]"
127
+ },
128
+ {
129
+ "input": "community=[private]",
130
+ "expected": "community=[REDACTED]"
131
+ }
132
+ ]
133
+ }