@bymax-one/nest-logger 1.4.6 → 1.4.7
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/CHANGELOG.md +37 -1
- package/README.md +3 -1
- package/dist/server/index.cjs +31 -5
- package/dist/server/index.d.cts +0 -60
- package/dist/server/index.d.ts +0 -60
- package/dist/server/index.mjs +31 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,41 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.4.7] - 2026-11-01
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`errorStructured` keeps its `logKey` when the `Error` cannot be read.** A hostile `Error` (a Proxy whose
|
|
19
|
+
`ownKeys` throws, an own getter that throws, a `stack` or `message` getter that throws) made the whole
|
|
20
|
+
record fail the name redaction, so the entry arrived as `LOGGER_REDACTION_FAILED` and lost the caller's
|
|
21
|
+
`logKey`, `userId` and metadata. The `Error` node is now isolated: when it cannot be walked it is replaced
|
|
22
|
+
by its sanitized form and walked again, so the name redaction still applies to whatever can be read. An
|
|
23
|
+
unreadable `stack` or `cause` no longer degrades the node to `SanitizeFailed`; it keeps `name` and
|
|
24
|
+
`message` and carries `_errorFieldsIncomplete: true`. A hostile value that is not an `Error` still fails
|
|
25
|
+
the whole record, as before.
|
|
26
|
+
- **A stale comment in `HttpExceptionFilter` is corrected.** It said the cause chain was dropped at
|
|
27
|
+
`errorStructured`; the entry keeps the cause chain and `AggregateError.errors`, and the response body
|
|
28
|
+
that rides along as an own field is name-redacted. A new spec pins both.
|
|
29
|
+
|
|
30
|
+
#### Apply to a derived backend
|
|
31
|
+
|
|
32
|
+
Nothing to change in code. What can differ at run time:
|
|
33
|
+
|
|
34
|
+
- An alert on `LOGGER_REDACTION_FAILED` that fired for an unreadable `Error` stops firing for it. The event
|
|
35
|
+
now arrives under its own `logKey`, with the `err` node carrying `_errorFieldsIncomplete: true`. To find
|
|
36
|
+
these entries going forward, filter on `err._errorFieldsIncomplete: true` or `err.type: 'SanitizeFailed'`. `LOGGER_REDACTION_FAILED` is
|
|
37
|
+
left for a record that really cannot be read, such as hostile metadata.
|
|
38
|
+
- An error whose `stack` or `cause` getter throws used to reach the log as `{ type: 'SanitizeFailed' }`; it
|
|
39
|
+
now keeps its `type` and `message` and carries the marker. Code that matched `SanitizeFailed` for that
|
|
40
|
+
case should match the marker instead.
|
|
41
|
+
|
|
42
|
+
### Tests
|
|
43
|
+
|
|
44
|
+
- An `Error` that cannot be read has a failing-first spec at the redactor and through the real Pino
|
|
45
|
+
pipeline, including a readable `password` next to a throwing getter and a hostile `message` getter, with
|
|
46
|
+
no clear secret in the line. Two existing cases that pinned the old degradation (`SanitizeFailed` for a
|
|
47
|
+
throwing `stack` getter, the record-level envelope for a hostile `Error`) now pin the new behavior.
|
|
48
|
+
|
|
14
49
|
## [1.4.6] - 2026-11-01
|
|
15
50
|
|
|
16
51
|
### Fixed
|
|
@@ -2276,7 +2311,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
2276
2311
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
2277
2312
|
`release.yml`, Dependabot, and issue templates
|
|
2278
2313
|
|
|
2279
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.
|
|
2314
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.7...HEAD
|
|
2315
|
+
[1.4.7]: https://github.com/bymaxone/nest-logger/compare/v1.4.6...v1.4.7
|
|
2280
2316
|
[1.4.6]: https://github.com/bymaxone/nest-logger/compare/v1.4.5...v1.4.6
|
|
2281
2317
|
[1.4.5]: https://github.com/bymaxone/nest-logger/compare/v1.4.4...v1.4.5
|
|
2282
2318
|
[1.4.4]: https://github.com/bymaxone/nest-logger/compare/v1.4.3...v1.4.4
|
package/README.md
CHANGED
|
@@ -729,6 +729,8 @@ Pino's standard serializer did.
|
|
|
729
729
|
|
|
730
730
|
When those properties cannot be read (a Proxy whose `ownKeys` throws, or a getter that throws), none of them is copied and the error node carries `_errorFieldsIncomplete: true`, so an entry without its `code` is never mistaken for a complete one.
|
|
731
731
|
|
|
732
|
+
An `Error` that cannot be read at all does not cost the record its `logKey`, `userId` or metadata: the node is replaced by its sanitized form, which keeps `name` and `message` and carries the same marker unless `name` or `message` itself is unreadable, in which case it is `{ type: 'SanitizeFailed' }`. Filter on `err._errorFieldsIncomplete: true` or `SanitizeFailed` to find these entries.
|
|
733
|
+
|
|
732
734
|
### 7. Context propagation with `LogContextService`
|
|
733
735
|
|
|
734
736
|
```typescript
|
|
@@ -1599,7 +1601,7 @@ The `PrettyDevDestination` errors are thrown inside `onInit`, which the registry
|
|
|
1599
1601
|
| `LOGGER_OTEL_API_BROKEN` | NDJSON line on `stderr`, once | `@opentelemetry/api` is installed but throws while loading, or exports no usable `trace` namespace | Reinstall it, or turn `otel.shouldAutoInjectTraceContext` off; until then `traceId` and `spanId` are absent |
|
|
1600
1602
|
| `LOGGER_BOOTSTRAP_WARNING` | log entry with `reason: 'OTEL_API_UNAVAILABLE'` | `otel.shouldAutoInjectTraceContext` is on and `@opentelemetry/api` cannot be resolved | Install `@opentelemetry/api`, or turn the option off; until then `traceId` and `spanId` are absent |
|
|
1601
1603
|
| `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | a serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
|
|
1602
|
-
| `LOGGER_REDACTION_FAILED` | `_logKey` in the envelope, with `_redactionFailed: true` | a record cannot be walked by the redactor (a throwing getter, a hostile proxy)
|
|
1604
|
+
| `LOGGER_REDACTION_FAILED` | `_logKey` in the envelope, with `_redactionFailed: true` | a record cannot be walked by the redactor (a throwing getter, a hostile proxy in the metadata) | The record is dropped whole, never partly emitted; the message and ambient context stay. Find the value that broke the walk |
|
|
1603
1605
|
|
|
1604
1606
|
---
|
|
1605
1607
|
|
package/dist/server/index.cjs
CHANGED
|
@@ -399,12 +399,14 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
|
399
399
|
}
|
|
400
400
|
seen.add(value);
|
|
401
401
|
const result = { name: value.name, message: value.message };
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
const stack = readOptional(() => value.stack, result);
|
|
403
|
+
if (stack !== void 0) {
|
|
404
|
+
result.stack = scrubStack(stack);
|
|
404
405
|
}
|
|
405
406
|
const isAtDepthLimit = depth2 >= maxDepth;
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
const cause = readOptional(() => value.cause, result);
|
|
408
|
+
if (cause !== void 0) {
|
|
409
|
+
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
|
|
408
410
|
}
|
|
409
411
|
const aggregated = readAggregatedErrors(value);
|
|
410
412
|
if (aggregated !== void 0) {
|
|
@@ -431,7 +433,18 @@ function assignOwnErrorFields(target, source) {
|
|
|
431
433
|
}
|
|
432
434
|
}
|
|
433
435
|
} catch {
|
|
434
|
-
|
|
436
|
+
markIncomplete(target);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function markIncomplete(target) {
|
|
440
|
+
Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
|
|
441
|
+
}
|
|
442
|
+
function readOptional(read, node) {
|
|
443
|
+
try {
|
|
444
|
+
return read();
|
|
445
|
+
} catch {
|
|
446
|
+
markIncomplete(node);
|
|
447
|
+
return void 0;
|
|
435
448
|
}
|
|
436
449
|
}
|
|
437
450
|
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
@@ -1485,6 +1498,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
|
|
|
1485
1498
|
if (ancestors.has(value)) {
|
|
1486
1499
|
return REDACT_CIRCULAR;
|
|
1487
1500
|
}
|
|
1501
|
+
if (value instanceof Error) {
|
|
1502
|
+
return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1503
|
+
}
|
|
1504
|
+
return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1505
|
+
}
|
|
1506
|
+
function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
|
|
1507
|
+
try {
|
|
1508
|
+
return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1509
|
+
} catch {
|
|
1510
|
+
return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
|
|
1488
1514
|
const resolvedToJson = resolveToJson(value);
|
|
1489
1515
|
if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
|
|
1490
1516
|
return value;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -618,66 +618,6 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
618
618
|
*/
|
|
619
619
|
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
620
620
|
|
|
621
|
-
/**
|
|
622
|
-
* Name-based recursive redaction.
|
|
623
|
-
*
|
|
624
|
-
* Layer: server/utils — the engine behind the library's DEFAULT PII protection.
|
|
625
|
-
* It walks a log record once and censors every value whose KEY NAME is in the
|
|
626
|
-
* sensitive set, to a bounded depth (see {@link REDACT_MAX_TRAVERSAL_DEPTH}).
|
|
627
|
-
*
|
|
628
|
-
* Why this replaced path matching. The previous default compiled 140
|
|
629
|
-
* `fast-redact` paths, 108 of them multi-level wildcards (`*.password`,
|
|
630
|
-
* `*.*.password`, …). `fast-redact` walks every key at every listed level on
|
|
631
|
-
* every log call, so the cost grew with the PATH COUNT, not with the payload:
|
|
632
|
-
* measured at ~107 µs per entry (≈9.3 k logs/s) against ~0.9 µs with redaction
|
|
633
|
-
* off. A single traversal is O(nodes) — the same order Pino's serializer already
|
|
634
|
-
* pays. The shipped configuration measures 462 k logs/s against 9.3 k, for
|
|
635
|
-
* byte-identical output on every payload the old paths covered.
|
|
636
|
-
*
|
|
637
|
-
* It is also STRICTLY SAFER than the paths it replaces:
|
|
638
|
-
* - depth 100 instead of four, and past it the value is DROPPED rather than
|
|
639
|
-
* passed through, so the ceiling cannot become a leak the way the old one
|
|
640
|
-
* was (anything below the wildcard list's fourth level was emitted in clear);
|
|
641
|
-
* - a sensitive key is caught wherever it appears, so a headers bag logged as
|
|
642
|
-
* `{ headers: { authorization } }` is covered, not only the exact
|
|
643
|
-
* `req.headers.authorization` shape the absolute paths pinned.
|
|
644
|
-
*
|
|
645
|
-
* Three value kinds need special handling, and getting any of them wrong is a
|
|
646
|
-
* leak rather than a cosmetic issue:
|
|
647
|
-
* - `Error` — copied through its prototype and descriptors, so it stays an
|
|
648
|
-
* `Error` (Pino's `err` serializer keys off the instance, and `message` /
|
|
649
|
-
* `stack` are own but NON-enumerable). Skipping errors entirely left an
|
|
650
|
-
* error under any key WITHOUT a serializer serializing its own enumerable
|
|
651
|
-
* properties in clear.
|
|
652
|
-
* - anything carrying `toJSON` (`Date`, `Decimal`, Luxon, Prisma types) — the
|
|
653
|
-
* method decides the serialized form, so the walk inspects its OUTPUT.
|
|
654
|
-
* Skipping these let a `toJSON` SYNTHESIZE a secret straight past both hooks.
|
|
655
|
-
* - `ArrayBuffer` views (`Buffer`, typed arrays) — indexed byte containers,
|
|
656
|
-
* returned untouched.
|
|
657
|
-
*
|
|
658
|
-
* What this file cannot reach: bindings passed to `child()`. Pino pre-serializes
|
|
659
|
-
* them into the instance's `chindings` fragment before any formatter runs, so
|
|
660
|
-
* `PinoLoggerService.child()` applies this redactor itself.
|
|
661
|
-
*
|
|
662
|
-
* Contract: SNAPSHOT and NEVER-THROW. Nothing the caller passed is mutated; the
|
|
663
|
-
* result is a fresh structure built from values read exactly ONCE. That is a
|
|
664
|
-
* deliberate trade against an earlier copy-on-write design which returned a
|
|
665
|
-
* clean subtree by reference: doing so left every accessor in it to be evaluated
|
|
666
|
-
* a SECOND time by `JSON.stringify`, and a stateful getter (or `toJSON`) can
|
|
667
|
-
* answer clean to the walk and `{ password }` to the serializer. Inspection
|
|
668
|
-
* cannot close that window — only reading once and pinning the result can. It
|
|
669
|
-
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
670
|
-
* faster than the path expansion it replaced.
|
|
671
|
-
*
|
|
672
|
-
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
673
|
-
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
674
|
-
* than crashing the request that produced it. Writing the censor is not among those
|
|
675
|
-
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
676
|
-
* error keeps its fields instead of costing the record.
|
|
677
|
-
*
|
|
678
|
-
* @see {@link createNameRedactor}
|
|
679
|
-
*/
|
|
680
|
-
|
|
681
621
|
/**
|
|
682
622
|
* A configured redactor.
|
|
683
623
|
*
|
package/dist/server/index.d.ts
CHANGED
|
@@ -618,66 +618,6 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
|
|
|
618
618
|
*/
|
|
619
619
|
type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
|
|
620
620
|
|
|
621
|
-
/**
|
|
622
|
-
* Name-based recursive redaction.
|
|
623
|
-
*
|
|
624
|
-
* Layer: server/utils — the engine behind the library's DEFAULT PII protection.
|
|
625
|
-
* It walks a log record once and censors every value whose KEY NAME is in the
|
|
626
|
-
* sensitive set, to a bounded depth (see {@link REDACT_MAX_TRAVERSAL_DEPTH}).
|
|
627
|
-
*
|
|
628
|
-
* Why this replaced path matching. The previous default compiled 140
|
|
629
|
-
* `fast-redact` paths, 108 of them multi-level wildcards (`*.password`,
|
|
630
|
-
* `*.*.password`, …). `fast-redact` walks every key at every listed level on
|
|
631
|
-
* every log call, so the cost grew with the PATH COUNT, not with the payload:
|
|
632
|
-
* measured at ~107 µs per entry (≈9.3 k logs/s) against ~0.9 µs with redaction
|
|
633
|
-
* off. A single traversal is O(nodes) — the same order Pino's serializer already
|
|
634
|
-
* pays. The shipped configuration measures 462 k logs/s against 9.3 k, for
|
|
635
|
-
* byte-identical output on every payload the old paths covered.
|
|
636
|
-
*
|
|
637
|
-
* It is also STRICTLY SAFER than the paths it replaces:
|
|
638
|
-
* - depth 100 instead of four, and past it the value is DROPPED rather than
|
|
639
|
-
* passed through, so the ceiling cannot become a leak the way the old one
|
|
640
|
-
* was (anything below the wildcard list's fourth level was emitted in clear);
|
|
641
|
-
* - a sensitive key is caught wherever it appears, so a headers bag logged as
|
|
642
|
-
* `{ headers: { authorization } }` is covered, not only the exact
|
|
643
|
-
* `req.headers.authorization` shape the absolute paths pinned.
|
|
644
|
-
*
|
|
645
|
-
* Three value kinds need special handling, and getting any of them wrong is a
|
|
646
|
-
* leak rather than a cosmetic issue:
|
|
647
|
-
* - `Error` — copied through its prototype and descriptors, so it stays an
|
|
648
|
-
* `Error` (Pino's `err` serializer keys off the instance, and `message` /
|
|
649
|
-
* `stack` are own but NON-enumerable). Skipping errors entirely left an
|
|
650
|
-
* error under any key WITHOUT a serializer serializing its own enumerable
|
|
651
|
-
* properties in clear.
|
|
652
|
-
* - anything carrying `toJSON` (`Date`, `Decimal`, Luxon, Prisma types) — the
|
|
653
|
-
* method decides the serialized form, so the walk inspects its OUTPUT.
|
|
654
|
-
* Skipping these let a `toJSON` SYNTHESIZE a secret straight past both hooks.
|
|
655
|
-
* - `ArrayBuffer` views (`Buffer`, typed arrays) — indexed byte containers,
|
|
656
|
-
* returned untouched.
|
|
657
|
-
*
|
|
658
|
-
* What this file cannot reach: bindings passed to `child()`. Pino pre-serializes
|
|
659
|
-
* them into the instance's `chindings` fragment before any formatter runs, so
|
|
660
|
-
* `PinoLoggerService.child()` applies this redactor itself.
|
|
661
|
-
*
|
|
662
|
-
* Contract: SNAPSHOT and NEVER-THROW. Nothing the caller passed is mutated; the
|
|
663
|
-
* result is a fresh structure built from values read exactly ONCE. That is a
|
|
664
|
-
* deliberate trade against an earlier copy-on-write design which returned a
|
|
665
|
-
* clean subtree by reference: doing so left every accessor in it to be evaluated
|
|
666
|
-
* a SECOND time by `JSON.stringify`, and a stateful getter (or `toJSON`) can
|
|
667
|
-
* answer clean to the walk and `{ password }` to the serializer. Inspection
|
|
668
|
-
* cannot close that window — only reading once and pinning the result can. It
|
|
669
|
-
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
670
|
-
* faster than the path expansion it replaced.
|
|
671
|
-
*
|
|
672
|
-
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
673
|
-
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
674
|
-
* than crashing the request that produced it. Writing the censor is not among those
|
|
675
|
-
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
676
|
-
* error keeps its fields instead of costing the record.
|
|
677
|
-
*
|
|
678
|
-
* @see {@link createNameRedactor}
|
|
679
|
-
*/
|
|
680
|
-
|
|
681
621
|
/**
|
|
682
622
|
* A configured redactor.
|
|
683
623
|
*
|
package/dist/server/index.mjs
CHANGED
|
@@ -396,12 +396,14 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
|
396
396
|
}
|
|
397
397
|
seen.add(value);
|
|
398
398
|
const result = { name: value.name, message: value.message };
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
const stack = readOptional(() => value.stack, result);
|
|
400
|
+
if (stack !== void 0) {
|
|
401
|
+
result.stack = scrubStack(stack);
|
|
401
402
|
}
|
|
402
403
|
const isAtDepthLimit = depth2 >= maxDepth;
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
const cause = readOptional(() => value.cause, result);
|
|
405
|
+
if (cause !== void 0) {
|
|
406
|
+
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
|
|
405
407
|
}
|
|
406
408
|
const aggregated = readAggregatedErrors(value);
|
|
407
409
|
if (aggregated !== void 0) {
|
|
@@ -428,7 +430,18 @@ function assignOwnErrorFields(target, source) {
|
|
|
428
430
|
}
|
|
429
431
|
}
|
|
430
432
|
} catch {
|
|
431
|
-
|
|
433
|
+
markIncomplete(target);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function markIncomplete(target) {
|
|
437
|
+
Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
|
|
438
|
+
}
|
|
439
|
+
function readOptional(read, node) {
|
|
440
|
+
try {
|
|
441
|
+
return read();
|
|
442
|
+
} catch {
|
|
443
|
+
markIncomplete(node);
|
|
444
|
+
return void 0;
|
|
432
445
|
}
|
|
433
446
|
}
|
|
434
447
|
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
@@ -1482,6 +1495,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
|
|
|
1482
1495
|
if (ancestors.has(value)) {
|
|
1483
1496
|
return REDACT_CIRCULAR;
|
|
1484
1497
|
}
|
|
1498
|
+
if (value instanceof Error) {
|
|
1499
|
+
return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1500
|
+
}
|
|
1501
|
+
return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1502
|
+
}
|
|
1503
|
+
function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
|
|
1504
|
+
try {
|
|
1505
|
+
return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
|
|
1506
|
+
} catch {
|
|
1507
|
+
return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
|
|
1485
1511
|
const resolvedToJson = resolveToJson(value);
|
|
1486
1512
|
if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
|
|
1487
1513
|
return value;
|
package/package.json
CHANGED