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 CHANGED
@@ -1,6 +1,6 @@
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
 
@@ -13,7 +13,7 @@ const logger = require('ntlogger');
13
13
  const unregister = logger.setupSignalHandlers({ timeout: 30000 });
14
14
  ```
15
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 an error.
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
17
 
18
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
19
 
@@ -35,14 +35,29 @@ OpenTelemetry users must upgrade the logs SDK/API/HTTP exporter together to `^0.
35
35
 
36
36
  ## Installation
37
37
 
38
- To install NightTimeLogger, use npm:
38
+ For Pino or Fastify:
39
39
 
40
40
  ```bash
41
- npm install ntlogger
41
+ npm install ntlogger pino
42
42
  ```
43
43
 
44
- The core package depends only on `winston` and `winston-transport`, so a default
45
- install stays small.
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.
46
61
 
47
62
  ### Optional plugin backends
48
63
 
@@ -57,8 +72,8 @@ Install only the ones whose plugins you actually enable:
57
72
  | OpenTelemetry | `npm install @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions` |
58
73
  | Discord, Teams, OpenObserve, Syslog, Jest | none - no extra packages required |
59
74
 
60
- Plugin modules are resolved lazily, so a missing peer never breaks
61
- `require('ntlogger')`. The loader reports an actionable error naming the package and
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
62
77
  the install command, skips that one plugin, and keeps every other plugin working.
63
78
  See [plugins/README.md](https://github.com/NightSquawk/NightTimeLogger/blob/main/plugins/README.md)
64
79
  for per-plugin configuration.
@@ -255,8 +270,8 @@ It provides an NTL-styled transport plus `createLogger()`, which wires the NTL c
255
270
  (module label, structured context, secret redaction, sampling, graceful shutdown) onto a real
256
271
  Pino logger.
257
272
 
258
- `pino` is an optional peer dependency and `split2` an optional dependency Winston-only users
259
- install neither.
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.
260
275
 
261
276
  ### Transport only
262
277
 
@@ -384,13 +399,13 @@ const log = createLogger({
384
399
  deduplication: { enabled: true, threshold: 3, window: 60000 },
385
400
  });
386
401
 
387
- log.getStats(); // { sampling: {...}, deduplication: {...} }
402
+ log.getStats(); // { sampling: {...}, deduplication: {...}, levels: {...}, hookErrors: 0 }
388
403
  log.resetStats();
389
404
  await log.close(); // releases the sampler/dedup timers, then flushes the destination
390
405
  ```
391
406
 
392
407
  `fatal` is never sampled, rate limited or deduplicated unless the configuration names it
393
- explicitly. Zero-config loggers install no feature hook at all.
408
+ explicitly. Per-level counts are collected even without sampling or deduplication.
394
409
 
395
410
  ### Silent mode
396
411
 
@@ -398,18 +413,226 @@ Resolution order:
398
413
 
399
414
  1. `opts.silent`, when it is a boolean
400
415
  2. `NTLOGGER_SILENT` — `1`/`true`/`yes` → silent, `0`/`false`/`no` → not silent
401
- 3. `NODE_ENV === 'test'` → silent
416
+ 3. `NODE_ENV === 'test'` or nonempty `NODE_TEST_CONTEXT` → silent
402
417
 
403
- A silent logger runs at Pino level `silent`, attaches no transport and installs no hooks, so
404
- test suites pay nothing for logging. `isSilent(opts)` is exported for testing the resolution.
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.
405
420
 
406
421
  ```javascript
407
422
  const log = createLogger({ module: 'API' }); // silent under Jest (NODE_ENV=test)
408
423
  const log = createLogger({ module: 'API', silent: false }); // force output in a test
409
424
  ```
410
425
 
411
- Node's built-in test runner does **not** set `NODE_ENV`, so projects using `node --test` should
412
- export `NTLOGGER_SILENT=1` in their test script instead.
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.
413
636
 
414
637
  ### Process handlers
415
638
 
@@ -467,3 +690,5 @@ NightTimeLogger is licensed under the [GPL-3.0 License](https://opensource.org/l
467
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.
468
691
 
469
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
+ }
@@ -0,0 +1,115 @@
1
+ {
2
+ "version": 1,
3
+ "keys": [
4
+ "password",
5
+ "passwd",
6
+ "pass",
7
+ "pwd",
8
+ "secret",
9
+ "token",
10
+ "apikey",
11
+ "api_key",
12
+ "x-api-key",
13
+ "authorization",
14
+ "proxy-authorization",
15
+ "cookie",
16
+ "set-cookie",
17
+ "x-device-installation-secret",
18
+ "installationsecret",
19
+ "privatekey",
20
+ "private_key",
21
+ "clientsecret",
22
+ "client_secret",
23
+ "accesstoken",
24
+ "access_token",
25
+ "refreshtoken",
26
+ "refresh_token",
27
+ "idtoken",
28
+ "id_token",
29
+ "sessiontoken",
30
+ "credential",
31
+ "credentials",
32
+ "webhookurl",
33
+ "snmpcommunity",
34
+ "community_string",
35
+ "community",
36
+ "authPassword",
37
+ "privPassword",
38
+ "passphrase"
39
+ ],
40
+ "patterns": [
41
+ {
42
+ "source": "-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY-----",
43
+ "flags": "g"
44
+ },
45
+ {
46
+ "source": "\\bAKIA[0-9A-Z]{16}\\b",
47
+ "flags": "g"
48
+ },
49
+ {
50
+ "source": "\\beyJ[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}",
51
+ "flags": "g"
52
+ },
53
+ {
54
+ "source": "\\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})",
55
+ "flags": "g"
56
+ },
57
+ {
58
+ "source": "\\bxox[abpors]-[A-Za-z0-9-]{10,}",
59
+ "flags": "gi"
60
+ },
61
+ {
62
+ "source": "\\b(?:[sr]k_(?:live|test)_[A-Za-z0-9]{10,}|whsec_[A-Za-z0-9]{10,})",
63
+ "flags": "g"
64
+ },
65
+ {
66
+ "source": "(?<pre>\\bBearer\\s+)[A-Za-z0-9\\-._~+/]{8,}={0,2}",
67
+ "flags": "gi"
68
+ },
69
+ {
70
+ "source": "(?<pre>\\bBasic\\s+)(?=[A-Za-z0-9+/]*[A-Z0-9])[A-Za-z0-9+/]{16,}={0,2}",
71
+ "flags": "g"
72
+ },
73
+ {
74
+ "source": "(?<pre>https?:\\/\\/(?:[a-z]+\\.)?discord(?:app)?\\.com\\/api\\/(?:v\\d+\\/)?webhooks\\/\\d+\\/)[A-Za-z0-9_-]+",
75
+ "flags": "gi"
76
+ },
77
+ {
78
+ "source": "(?<pre>[\"']?\\b(?:snmp[_-]?community|community(?:[_-]?string)?|auth[_-]?password|priv[_-]?password|passphrase)[\"']?\\s*[:=]\\s*)(?:\"(?:\\\\.|[^\"\\\\])*(?:\"|$)|'(?:\\\\.|[^'\\\\])*(?:'|$)|[^\\s,;}]+)",
79
+ "flags": "gi"
80
+ },
81
+ {
82
+ "source": "(?<pre>\\b(?:api[_-]?key|apikey|access[_-]?key|access[_-]?token|refresh[_-]?token|secret[_-]?key|client[_-]?secret|password|passwd|pwd|auth[_-]?token|private[_-]?key|token|secret)\\s*[:=]\\s*[\"']?)[^\\s'\",;]{4,}",
83
+ "flags": "gi"
84
+ },
85
+ {
86
+ "source": "(?<pre>\\b(?:password|passwd|secret|token|api[_-]?key)\\s*[=:]\\s*)(?![\"'[])\\S+",
87
+ "flags": "gi"
88
+ },
89
+ {
90
+ "source": "(?<pre>\\b[a-z][a-z0-9+.-]*:\\/\\/[^\\s:/?#@]*:)[^\\s/?#@]+(?<post>@)",
91
+ "flags": "gi"
92
+ }
93
+ ],
94
+ "pythonLevels": {
95
+ "5": 10,
96
+ "10": 20,
97
+ "20": 30,
98
+ "30": 40,
99
+ "40": 50,
100
+ "50": 60
101
+ },
102
+ "contextAliases": {
103
+ "agent_uuid": "agentUuid",
104
+ "tenant_id": "tenantId",
105
+ "command_id": "commandId",
106
+ "collection_cycle_id": "collectionCycleId",
107
+ "correlation_id": "correlationId",
108
+ "run_id": "runId",
109
+ "work_id": "workId",
110
+ "stage_id": "stageId",
111
+ "device_id": "deviceId",
112
+ "trace_id": "traceId",
113
+ "span_id": "spanId"
114
+ }
115
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ function receiverName(node) {
4
+ if (node.type === 'Identifier') return node.name;
5
+ if (node.type === 'ThisExpression') return 'this';
6
+ if (node.type === 'MemberExpression' && !node.computed) {
7
+ const prefix = receiverName(node.object);
8
+ return prefix && `${prefix}.${node.property.name}`;
9
+ }
10
+ return null;
11
+ }
12
+ module.exports = {
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: { description: 'Keep interpolated Pino log fields searchable with an object-first call.' },
16
+ fixable: 'code',
17
+ schema: [{ type: 'object', properties: {
18
+ loggerNames: { type: 'array', items: { type: 'string' }, uniqueItems: true },
19
+ }, additionalProperties: false }],
20
+ messages: { structured: 'Pass searchable fields as the first argument to this logger.' },
21
+ },
22
+ create(context) {
23
+ const names = new Set(context.options[0]?.loggerNames || ['log', 'logger', 'req.log', 'request.log', 'app.log', 'fastify.log']);
24
+ const methods = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
25
+ const source = context.sourceCode;
26
+ function constantPrimitive(identifier) {
27
+ let scope = source.getScope(identifier);
28
+ let variable;
29
+ while (scope && !variable) { variable = scope.set.get(identifier.name); scope = scope.upper; }
30
+ const def = variable?.defs[0];
31
+ return variable?.defs.length === 1 && def?.parent?.kind === 'const'
32
+ && def.node.id.type === 'Identifier' && def.node.init?.type === 'Literal'
33
+ && !def.node.init.regex && typeof def.node.init.value !== 'object';
34
+ }
35
+ return {
36
+ CallExpression(node) {
37
+ const callee = node.callee;
38
+ if (callee.type !== 'MemberExpression' || callee.computed
39
+ || !methods.has(callee.property.name) || !names.has(receiverName(callee.object))) return;
40
+ const template = node.arguments[0];
41
+ if (template?.type !== 'TemplateLiteral' || !template.expressions.length) return;
42
+ const canFix = node.arguments.length === 1 && template.expressions.every(expr =>
43
+ expr.type === 'Identifier' && expr.name !== '__proto__' && constantPrimitive(expr));
44
+ context.report({ node: template, messageId: 'structured',
45
+ // Keep the original message and its formatting. Only duplicate reads
46
+ // of known primitive consts; calls/getters/mutable bindings need review.
47
+ fix: canFix ? fixer => fixer.insertTextBefore(template,
48
+ `{ ${[...new Set(template.expressions.map(expr => expr.name))].join(', ')} }, `) : undefined,
49
+ });
50
+ },
51
+ };
52
+ },
53
+ };
package/eslint.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ESLint } from 'eslint';
2
+ declare const plugin: ESLint.Plugin;
3
+ export = plugin;
package/eslint.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ meta: { name: 'ntlogger' },
4
+ rules: { 'prefer-object-first': require('./eslint-rules/prefer-object-first') },
5
+ };