@bymax-one/nest-logger 1.4.6 → 1.4.8

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 CHANGED
@@ -11,6 +11,68 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.4.8] - 2026-11-01
15
+
16
+ ### Fixed
17
+
18
+ - **A logging call no longer throws because of what it was handed.** `error`, `warn`, `log`, `fatal`
19
+ and the other NestJS variadic methods threw out of the call when an `Error` had a `name` or `message`
20
+ getter that throws, and when the message had no text form (a null-prototype object, a throwing
21
+ `toString`). In `catch (e) { logger.error(e) }` that second throw replaced the original error and
22
+ nothing was logged. The call now returns and the entry is written: an unreadable `Error` arrives as
23
+ the same `err` node `errorStructured` produces, with `err.type: 'SanitizeFailed'`, and a message with
24
+ no text form is logged as `Unreadable log message`.
25
+
26
+ #### Apply to a derived backend
27
+
28
+ Nothing to change in code. What can differ at run time:
29
+
30
+ - Entries that were never written now appear. A host that handled the throw (a `try` around the logging
31
+ call, or an error handler that reacted to it) no longer sees it; one that relied on the throw to detect
32
+ a hostile input should look for `err.type: 'SanitizeFailed'` (an `Error`) or the message
33
+ `Unreadable log message` (a message) instead.
34
+
35
+ ### Tests
36
+
37
+ - Each variadic position with an unreadable `name` or `message`, and each message with no text form on
38
+ `error`, `warn` and `log`, has a failing-first spec through the real Pino pipeline, with a control that
39
+ a readable error is logged as before.
40
+
41
+ ## [1.4.7] - 2026-11-01
42
+
43
+ ### Fixed
44
+
45
+ - **`errorStructured` keeps its `logKey` when the `Error` cannot be read.** A hostile `Error` (a Proxy whose
46
+ `ownKeys` throws, an own getter that throws, a `stack` or `message` getter that throws) made the whole
47
+ record fail the name redaction, so the entry arrived as `LOGGER_REDACTION_FAILED` and lost the caller's
48
+ `logKey`, `userId` and metadata. The `Error` node is now isolated: when it cannot be walked it is replaced
49
+ by its sanitized form and walked again, so the name redaction still applies to whatever can be read. An
50
+ unreadable `stack` or `cause` no longer degrades the node to `SanitizeFailed`; it keeps `name` and
51
+ `message` and carries `_errorFieldsIncomplete: true`. A hostile value that is not an `Error` still fails
52
+ the whole record, as before.
53
+ - **A stale comment in `HttpExceptionFilter` is corrected.** It said the cause chain was dropped at
54
+ `errorStructured`; the entry keeps the cause chain and `AggregateError.errors`, and the response body
55
+ that rides along as an own field is name-redacted. A new spec pins both.
56
+
57
+ #### Apply to a derived backend
58
+
59
+ Nothing to change in code. What can differ at run time:
60
+
61
+ - An alert on `LOGGER_REDACTION_FAILED` that fired for an unreadable `Error` stops firing for it. The event
62
+ now arrives under its own `logKey`, with the `err` node carrying `_errorFieldsIncomplete: true`. To find
63
+ these entries going forward, filter on `err._errorFieldsIncomplete: true` or `err.type: 'SanitizeFailed'`. `LOGGER_REDACTION_FAILED` is
64
+ left for a record that really cannot be read, such as hostile metadata.
65
+ - An error whose `stack` or `cause` getter throws used to reach the log as `{ type: 'SanitizeFailed' }`; it
66
+ now keeps its `type` and `message` and carries the marker. Code that matched `SanitizeFailed` for that
67
+ case should match the marker instead.
68
+
69
+ ### Tests
70
+
71
+ - An `Error` that cannot be read has a failing-first spec at the redactor and through the real Pino
72
+ pipeline, including a readable `password` next to a throwing getter and a hostile `message` getter, with
73
+ no clear secret in the line. Two existing cases that pinned the old degradation (`SanitizeFailed` for a
74
+ throwing `stack` getter, the record-level envelope for a hostile `Error`) now pin the new behavior.
75
+
14
76
  ## [1.4.6] - 2026-11-01
15
77
 
16
78
  ### Fixed
@@ -2276,7 +2338,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
2276
2338
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
2277
2339
  `release.yml`, Dependabot, and issue templates
2278
2340
 
2279
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.6...HEAD
2341
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.8...HEAD
2342
+ [1.4.8]: https://github.com/bymaxone/nest-logger/compare/v1.4.7...v1.4.8
2343
+ [1.4.7]: https://github.com/bymaxone/nest-logger/compare/v1.4.6...v1.4.7
2280
2344
  [1.4.6]: https://github.com/bymaxone/nest-logger/compare/v1.4.5...v1.4.6
2281
2345
  [1.4.5]: https://github.com/bymaxone/nest-logger/compare/v1.4.4...v1.4.5
2282
2346
  [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) | The record is dropped whole, never partly emitted; the message and ambient context stay. Find the value that broke the walk |
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
 
@@ -386,25 +386,41 @@ function sanitizeError(err, options) {
386
386
  return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
387
387
  }
388
388
  function isErrorLike(value) {
389
+ try {
390
+ return readsAsError(value);
391
+ } catch {
392
+ return isErrorInstance(value);
393
+ }
394
+ }
395
+ function readsAsError(value) {
389
396
  if (typeof value !== "object" || value === null) {
390
397
  return false;
391
398
  }
392
399
  const candidate = value;
393
400
  return typeof candidate.name === "string" && typeof candidate.message === "string";
394
401
  }
402
+ function isErrorInstance(value) {
403
+ try {
404
+ return value instanceof Error;
405
+ } catch {
406
+ return false;
407
+ }
408
+ }
395
409
  function toSanitizedError(value, seen, depth2, maxDepth) {
396
410
  try {
397
- if (!isErrorLike(value)) {
411
+ if (!readsAsError(value)) {
398
412
  return { name: "UnknownError", message: String(value) };
399
413
  }
400
414
  seen.add(value);
401
415
  const result = { name: value.name, message: value.message };
402
- if (value.stack !== void 0) {
403
- result.stack = scrubStack(value.stack);
416
+ const stack = readOptional(() => value.stack, result);
417
+ if (stack !== void 0) {
418
+ result.stack = scrubStack(stack);
404
419
  }
405
420
  const isAtDepthLimit = depth2 >= maxDepth;
406
- if (value.cause !== void 0) {
407
- result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
421
+ const cause = readOptional(() => value.cause, result);
422
+ if (cause !== void 0) {
423
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
408
424
  }
409
425
  const aggregated = readAggregatedErrors(value);
410
426
  if (aggregated !== void 0) {
@@ -431,7 +447,18 @@ function assignOwnErrorFields(target, source) {
431
447
  }
432
448
  }
433
449
  } catch {
434
- Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
450
+ markIncomplete(target);
451
+ }
452
+ }
453
+ function markIncomplete(target) {
454
+ Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
455
+ }
456
+ function readOptional(read, node) {
457
+ try {
458
+ return read();
459
+ } catch {
460
+ markIncomplete(node);
461
+ return void 0;
435
462
  }
436
463
  }
437
464
  function toSanitizedChild(value, seen, depth2, maxDepth) {
@@ -487,6 +514,13 @@ function safeErrorMessage(error) {
487
514
  return "Unreadable error message";
488
515
  }
489
516
  }
517
+ function safeLogMessage(message) {
518
+ try {
519
+ return toSingleLineMessage(typeof message === "string" ? message : String(message));
520
+ } catch {
521
+ return "Unreadable log message";
522
+ }
523
+ }
490
524
  function findErrorParam(optionalParams) {
491
525
  return optionalParams.find((param) => isErrorLike(param));
492
526
  }
@@ -558,10 +592,7 @@ exports.PinoLoggerService = class PinoLoggerService {
558
592
  assignIfDefined(payload, "context", context);
559
593
  assignIfDefined(payload, "stack", stack);
560
594
  assignIfDefined(payload, "err", findErrorParam(optionalParams));
561
- this.pino.error(
562
- payload,
563
- toSingleLineMessage(typeof message === "string" ? message : String(message))
564
- );
595
+ this.pino.error(payload, safeLogMessage(message));
565
596
  }
566
597
  /**
567
598
  * NestJS `warn` (warn level).
@@ -718,7 +749,7 @@ exports.PinoLoggerService = class PinoLoggerService {
718
749
  assignIfDefined(payload, "context", this.resolveContext(optionalParams));
719
750
  const leading = isErrorLike(message) ? message : void 0;
720
751
  assignIfDefined(payload, "err", leading ?? findErrorParam(optionalParams));
721
- const msg = leading === void 0 ? toSingleLineMessage(typeof message === "string" ? message : String(message)) : safeErrorMessage(leading);
752
+ const msg = leading === void 0 ? safeLogMessage(message) : safeErrorMessage(leading);
722
753
  switch (level) {
723
754
  case "info":
724
755
  this.pino.info(payload, msg);
@@ -1485,6 +1516,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
1485
1516
  if (ancestors.has(value)) {
1486
1517
  return REDACT_CIRCULAR;
1487
1518
  }
1519
+ if (value instanceof Error) {
1520
+ return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
1521
+ }
1522
+ return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
1523
+ }
1524
+ function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
1525
+ try {
1526
+ return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
1527
+ } catch {
1528
+ return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
1529
+ }
1530
+ }
1531
+ function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
1488
1532
  const resolvedToJson = resolveToJson(value);
1489
1533
  if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
1490
1534
  return value;
@@ -3192,11 +3236,7 @@ exports.PinoUnexpectedErrorSink = __decorateClass([
3192
3236
  __decorateParam(0, common.Inject(exports.PinoLoggerService))
3193
3237
  ], exports.PinoUnexpectedErrorSink);
3194
3238
  function toLoggableError(value) {
3195
- try {
3196
- return isErrorLike(value) ? value : sanitizeError(value);
3197
- } catch {
3198
- return sanitizeError(value);
3199
- }
3239
+ return isErrorLike(value) ? value : sanitizeError(value);
3200
3240
  }
3201
3241
  var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
3202
3242
  function InjectLogger(context) {
@@ -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
  *
@@ -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
  *
@@ -383,25 +383,41 @@ function sanitizeError(err, options) {
383
383
  return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
384
384
  }
385
385
  function isErrorLike(value) {
386
+ try {
387
+ return readsAsError(value);
388
+ } catch {
389
+ return isErrorInstance(value);
390
+ }
391
+ }
392
+ function readsAsError(value) {
386
393
  if (typeof value !== "object" || value === null) {
387
394
  return false;
388
395
  }
389
396
  const candidate = value;
390
397
  return typeof candidate.name === "string" && typeof candidate.message === "string";
391
398
  }
399
+ function isErrorInstance(value) {
400
+ try {
401
+ return value instanceof Error;
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
392
406
  function toSanitizedError(value, seen, depth2, maxDepth) {
393
407
  try {
394
- if (!isErrorLike(value)) {
408
+ if (!readsAsError(value)) {
395
409
  return { name: "UnknownError", message: String(value) };
396
410
  }
397
411
  seen.add(value);
398
412
  const result = { name: value.name, message: value.message };
399
- if (value.stack !== void 0) {
400
- result.stack = scrubStack(value.stack);
413
+ const stack = readOptional(() => value.stack, result);
414
+ if (stack !== void 0) {
415
+ result.stack = scrubStack(stack);
401
416
  }
402
417
  const isAtDepthLimit = depth2 >= maxDepth;
403
- if (value.cause !== void 0) {
404
- result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
418
+ const cause = readOptional(() => value.cause, result);
419
+ if (cause !== void 0) {
420
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
405
421
  }
406
422
  const aggregated = readAggregatedErrors(value);
407
423
  if (aggregated !== void 0) {
@@ -428,7 +444,18 @@ function assignOwnErrorFields(target, source) {
428
444
  }
429
445
  }
430
446
  } catch {
431
- Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
447
+ markIncomplete(target);
448
+ }
449
+ }
450
+ function markIncomplete(target) {
451
+ Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
452
+ }
453
+ function readOptional(read, node) {
454
+ try {
455
+ return read();
456
+ } catch {
457
+ markIncomplete(node);
458
+ return void 0;
432
459
  }
433
460
  }
434
461
  function toSanitizedChild(value, seen, depth2, maxDepth) {
@@ -484,6 +511,13 @@ function safeErrorMessage(error) {
484
511
  return "Unreadable error message";
485
512
  }
486
513
  }
514
+ function safeLogMessage(message) {
515
+ try {
516
+ return toSingleLineMessage(typeof message === "string" ? message : String(message));
517
+ } catch {
518
+ return "Unreadable log message";
519
+ }
520
+ }
487
521
  function findErrorParam(optionalParams) {
488
522
  return optionalParams.find((param) => isErrorLike(param));
489
523
  }
@@ -555,10 +589,7 @@ var PinoLoggerService = class {
555
589
  assignIfDefined(payload, "context", context);
556
590
  assignIfDefined(payload, "stack", stack);
557
591
  assignIfDefined(payload, "err", findErrorParam(optionalParams));
558
- this.pino.error(
559
- payload,
560
- toSingleLineMessage(typeof message === "string" ? message : String(message))
561
- );
592
+ this.pino.error(payload, safeLogMessage(message));
562
593
  }
563
594
  /**
564
595
  * NestJS `warn` (warn level).
@@ -715,7 +746,7 @@ var PinoLoggerService = class {
715
746
  assignIfDefined(payload, "context", this.resolveContext(optionalParams));
716
747
  const leading = isErrorLike(message) ? message : void 0;
717
748
  assignIfDefined(payload, "err", leading ?? findErrorParam(optionalParams));
718
- const msg = leading === void 0 ? toSingleLineMessage(typeof message === "string" ? message : String(message)) : safeErrorMessage(leading);
749
+ const msg = leading === void 0 ? safeLogMessage(message) : safeErrorMessage(leading);
719
750
  switch (level) {
720
751
  case "info":
721
752
  this.pino.info(payload, msg);
@@ -1482,6 +1513,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
1482
1513
  if (ancestors.has(value)) {
1483
1514
  return REDACT_CIRCULAR;
1484
1515
  }
1516
+ if (value instanceof Error) {
1517
+ return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
1518
+ }
1519
+ return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
1520
+ }
1521
+ function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
1522
+ try {
1523
+ return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
1524
+ } catch {
1525
+ return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
1526
+ }
1527
+ }
1528
+ function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
1485
1529
  const resolvedToJson = resolveToJson(value);
1486
1530
  if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
1487
1531
  return value;
@@ -3189,11 +3233,7 @@ PinoUnexpectedErrorSink = __decorateClass([
3189
3233
  __decorateParam(0, Inject(PinoLoggerService))
3190
3234
  ], PinoUnexpectedErrorSink);
3191
3235
  function toLoggableError(value) {
3192
- try {
3193
- return isErrorLike(value) ? value : sanitizeError(value);
3194
- } catch {
3195
- return sanitizeError(value);
3196
- }
3236
+ return isErrorLike(value) ? value : sanitizeError(value);
3197
3237
  }
3198
3238
  var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
3199
3239
  function InjectLogger(context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",