@bymax-one/nest-logger 1.2.6 → 1.2.9

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
@@ -534,6 +534,33 @@ any other field. `AggregateError` members reach the entry as `err.errors`. There
534
534
  OpenTelemetry attribute for a cause chain, so these stay namespaced under `err` rather than
535
535
  inventing an `exception.cause` the spec does not define.
536
536
 
537
+ **An error's own enumerable properties travel with it at every depth** — `code` on a Node system
538
+ error, `statusCode` from an HTTP layer, whatever domain fields you attached — including on a nested
539
+ `cause` and on each `AggregateError` member:
540
+
541
+ ```json
542
+ {
543
+ "err": {
544
+ "type": "Error",
545
+ "message": "bootstrap failed",
546
+ "code": "EBOOT",
547
+ "cause": {
548
+ "name": "Error",
549
+ "message": "config invalid",
550
+ "code": "BYMAX_CONFIG_VALIDATION",
551
+ "issues": [{ "variable": "DATABASE_URL", "code": "invalid_url" }]
552
+ }
553
+ }
554
+ }
555
+ ```
556
+
557
+ This matters because `code` is usually the field you alert on. Until 1.2.7 those properties survived
558
+ only on the error handed to the log call and were dropped the moment the same error was wrapped as
559
+ someone else's `cause` — which is what any handler that wraps what it caught does before
560
+ logging it. They never shadow a
561
+ field the serializer derives, and they pass through the same name-based redaction and size bound as
562
+ any other serializer output.
563
+
537
564
  An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
538
565
  Pino's standard serializer did.
539
566
 
@@ -760,10 +787,11 @@ So the common accident — adding `new PrettyDevDestination()` without installin
760
787
  ```typescript
761
788
  // One line per entry, with the fields your project repeats on every line hidden.
762
789
  new PrettyDevDestination({
763
- view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
790
+ view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
764
791
  })
765
792
 
766
793
  // Message only, with the context pulled back into the line.
794
+ // See the messageFormat warning below before interpolating any other field.
767
795
  new PrettyDevDestination({
768
796
  view: { hideObject: true, messageFormat: '[{context}] {msg}' }
769
797
  })
@@ -778,10 +806,29 @@ new PrettyDevDestination({
778
806
  | `translateTime` | `'SYS:HH:MM:ss.l'` | Or `false` for the raw timestamp |
779
807
  | `colorize` | `true` | ANSI colour |
780
808
 
781
- > **`hideObject` hides it everywhere.** In pretty mode there is no JSON copy behind the rendering, because `destinations` **replaces** stdout — so a field hidden here, `logKey` included, is not visible anywhere. That is what the option is for; `messageFormat` is how you pull a specific field back.
809
+ > **`hideObject` hides it from THIS destination.** Any other destination you registered still receives the complete entry — the option is display-only and scoped to the pretty renderer. When pretty is your **only** destination that scope becomes total: `destinations` **replaces** stdout, so there is no JSON copy behind the rendering and a hidden field, `logKey` included, is not visible anywhere. That is what the option is for; `messageFormat` is how you pull a specific field back.
810
+
811
+ > **A dot in `ignore` means "nested path", so a field name that CONTAINS a dot must escape it.** This library emits `event.name` as a literal top-level key, so the obvious spelling hides nothing at all — `pino-pretty` looks for an `event` object with a `name` inside, finds neither, and says nothing. Measured on one entry with only the option changing:
812
+ >
813
+ > ```
814
+ > ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
815
+ > ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
816
+ > ```
817
+ >
818
+ > In a TypeScript string literal that is `'event\\.name'` — one real backslash reaching `pino-pretty`. `service` and `deployment` need no escape; they really are nested objects. Reported by a consumer who copied the example above when it still had the unescaped form.
782
819
 
783
820
  The shape is exported as `PrettyViewOptions` when you want to build the view separately — it is this library's own interface, not a re-export of `pino-pretty`'s `PrettyOptions`, so type-checking without the optional peer installed still resolves.
784
821
 
822
+ > **`messageFormat` interpolates every placeholder except `{msg}` raw — that can forge a terminal entry.** This library normalizes line separators and control characters in `msg` and in the stack, never in metadata, because escaping data would mean rewriting what you asked to be logged. `pino-pretty` substitutes whatever the field holds, so a newline in an interpolated field splits one entry into two and the second reads like a genuine record. Measured, with a newline in `context`:
823
+ >
824
+ > ```
825
+ > lines produced by ONE entry: 2
826
+ > 1: "[10:35:14.484] INFO: [Auth"
827
+ > 2: "[10:00:00.000] INFO: FORGED admin promoted] real entry …"
828
+ > ```
829
+ >
830
+ > Interpolate only fields your own code sets. Never one carrying user input — `{tenantId}`, `{userId}`, a header, a query value. If you need one of those visible, leave it in the record instead. That closes **this** path and no more: a metadata value is not safe terminal text either, since `JSON.stringify` escapes only C0 and emits DEL, the C1 range (U+0085 NEL included), U+2028 and U+2029 verbatim — see _One entry, one line_ below, where the same boundary is stated. Only `msg` and the stack carry that guarantee.
831
+
785
832
  > **`destination` is not exposed, by design.** The library owns where entries go — a redirected stream would route around the fan-out and the last-resort rescue above. It is absent from the type and applied after your options are merged, so it cannot be overridden from untyped JavaScript either.
786
833
 
787
834
  **The first entries of a boot are held, not lost.** The transform cannot exist until `onInit` — loading the optional peer is async — so everything NestJS emits while instantiating providers arrives before it. Those entries are buffered and then rendered through the transform in arrival order. If the peer is missing, the bound is reached, or the app shuts down before init, they are written as raw NDJSON instead: degraded, never dropped.
@@ -1008,6 +1055,21 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
1008
1055
 
1009
1056
  Every `write()` runs inside a try/catch. A destination that throws or rejects produces a `LOGGER_DESTINATION_WRITE_FAILED` line on `stderr` — never back through the logger, which would turn a broken sink into a write → log → write feedback loop — and the entry is dropped for that sink only. A destination whose `onInit()` rejects is reported as `LOGGER_DESTINATION_INIT_FAILED` and excluded from the shutdown sequence without blocking boot; it stays in the write fan-out, where the same fail-soft wrapper contains it. A logging backend going down degrades logging — it never takes the application with it.
1010
1057
 
1058
+ ### What redaction covers, and what it cannot
1059
+
1060
+ Redaction here matches **field names**, not values. Under the default `redactStrategy: 'names'` a secret stored under a covered name is censored at any depth — under the legacy `'paths'` strategy the four-level `fast-redact` ceiling applies instead, and a deeper field is not covered. Either way, the same secret **embedded inside a string** under an uncovered name travels intact. Measured:
1061
+
1062
+ ```json
1063
+ {
1064
+ "password": "[REDACTED]",
1065
+ "renderedBody": "Hi Ana, your reset code is 481920. Expires in 10 minutes."
1066
+ }
1067
+ ```
1068
+
1069
+ There is no name rule that reaches the second one, and scanning inside values for secret-shaped text would mean rewriting what you asked to be logged — wrong in both directions, silently. **The boundary is yours to hold:** do not put rendered bodies, raw provider responses or whole request payloads into a field, and be aware that attaching an upstream error as `cause` carries whatever that error carries. Since 1.2.7 preserves an error's own fields at every depth of the chain, a cause that quotes something it should not quote is now more visible, not less.
1070
+
1071
+ `redactStrategy` governs **your** payload — the record you log, your child bindings, and the output of any serializer you supply. The built-in `err` serializer is the library's own output and is always name-walked, including under `'paths'`, because `fast-redact`'s four-wildcard ceiling cannot reach the depth a `cause` chain serializes to.
1072
+
1011
1073
  ### A closed pipe does not kill the process
1012
1074
 
1013
1075
  `node app | head` closes the read end of stdout while the application is still writing. The stream reports that as `EPIPE` **asynchronously**, through its `'error'` event, after `write()` has already returned — so a `try/catch` around the write catches nothing, and because Node attaches no default handler to `process.stdout`, the emit becomes an uncaught exception and the process dies. Measured, not assumed: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42.
@@ -1262,9 +1324,13 @@ interface ILogDestination {
1262
1324
  readonly name: string
1263
1325
  readonly minLevel?: LogLevel
1264
1326
  /** `payload` is a newline-terminated JSON entry, UTF-8 encoded — not an object. */
1265
- write(payload: string): void | Promise<void>
1266
- onInit?(): Promise<void>
1267
- onShutdown?(): Promise<void>
1327
+ write(payload: string): void | PromiseLike<void>
1328
+ onInit?(): void | PromiseLike<void>
1329
+ /** Called once after EVERY destination's `onInit` settled. Only useful if you buffer. */
1330
+ onRegistryReady?(status: {
1331
+ readonly heldEntriesDeliveredElsewhere: boolean
1332
+ }): void | PromiseLike<void>
1333
+ onShutdown?(): void | PromiseLike<void>
1268
1334
  }
1269
1335
  ```
1270
1336