@bymax-one/nest-logger 1.2.7 → 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/CHANGELOG.md +514 -2
- package/README.md +36 -5
- package/dist/server/index.cjs +335 -32
- package/dist/server/index.d.cts +132 -10
- package/dist/server/index.d.ts +132 -10
- package/dist/server/index.mjs +335 -32
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,513 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.9] - 2026-08-16
|
|
15
|
+
|
|
16
|
+
> **`1.2.8` was merged but never published.** The defect below was found in it after the merge and
|
|
17
|
+
> before any tag existed, so the version was left untagged rather than released and superseded. npm
|
|
18
|
+
> therefore goes `1.2.7` → `1.2.9`; the `1.2.8` section stays as the record of what landed, because
|
|
19
|
+
> it is what `main` contains.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **A destination whose `minLevel` was not one of the six levels received NOTHING, silently.**
|
|
24
|
+
`readonly minLevel?: LogLevel` is a compile-time claim, and a JavaScript consumer — or a
|
|
25
|
+
miscast one — can return any string; `'verbose'` is a real NestJS level name and a natural
|
|
26
|
+
thing to write. `pino.multistream` does not reject an unrecognised level: it builds without
|
|
27
|
+
complaint and the entry then matches nothing. Measured on a sink configured that way: **0 of 3
|
|
28
|
+
emitted entries delivered**, while the registry recorded it as covering the module level,
|
|
29
|
+
because `LOG_LEVEL_PRIORITY.indexOf` returned `-1` and lost the comparison. A destination
|
|
30
|
+
believed healthy and receiving, receiving nothing, is the worst failure this library has.
|
|
31
|
+
`safeMinLevel` now validates the value against the canonical list and treats an unrecognised
|
|
32
|
+
one as absent, so the destination falls back to the module level and keeps receiving.
|
|
33
|
+
`validateOptions` already held `options.level` to that same list; this is the check reaching
|
|
34
|
+
the one place it had not.
|
|
35
|
+
|
|
36
|
+
- **A write returning a thenable that is not `instanceof Promise` no longer takes the synchronous
|
|
37
|
+
path, where its rejection escaped and its entry could be discarded.** `instanceof` is realm-local:
|
|
38
|
+
it answers `false` for a promise built in another realm — a worker, a `vm` context — and for any
|
|
39
|
+
structurally valid thenable. Measured, both:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
thenable instanceof Promise? false
|
|
43
|
+
promise from another realm instanceof Promise? false
|
|
44
|
+
Promise.resolve() assimilates both? true
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Such a write took the branch meant for synchronous sinks: the stream callback fired immediately, a
|
|
48
|
+
later rejection surfaced as an **unhandled rejection** instead of a reported
|
|
49
|
+
`LOGGER_DESTINATION_WRITE_FAILED`, and the write was never counted as in flight — so `1.2.8`'s
|
|
50
|
+
readiness check could tell a buffering destination that delivery was proven and let it discard its
|
|
51
|
+
only copy of an entry that was about to fail. Losing an entry is the one outcome this path does not
|
|
52
|
+
accept.
|
|
53
|
+
|
|
54
|
+
The branch is now on `undefined`, which is what the declared `void | PromiseLike<void>` actually
|
|
55
|
+
distinguishes, and the result goes through `Promise.resolve` — which assimilates both shapes.
|
|
56
|
+
Synchronous writes keep the synchronous path, so back-pressure is unchanged.
|
|
57
|
+
|
|
58
|
+
This library learned the realm-local lesson once already, from `instanceof Error`, which is why
|
|
59
|
+
`isErrorLike` exists. The same mistake sat one file away.
|
|
60
|
+
|
|
61
|
+
### Breaking
|
|
62
|
+
|
|
63
|
+
- **`ILogDestination` declares `void | PromiseLike<void>` instead of `void | Promise<void>`,** on
|
|
64
|
+
`write`, `onInit`, `onRegistryReady` and `onShutdown`. The fix above handles a thenable at runtime;
|
|
65
|
+
the public type still forbade one, so a TypeScript consumer could not return the very shape the
|
|
66
|
+
release claims to support — the tests needed casts precisely because of that gap, and the casts are
|
|
67
|
+
gone.
|
|
68
|
+
|
|
69
|
+
**This is source-breaking for CALLERS, and an earlier draft of this note wrongly said it was not.**
|
|
70
|
+
Widening what an implementation may return necessarily narrows what a caller receives: `PromiseLike`
|
|
71
|
+
exposes only `then`, so code that called `.catch()` or `.finally()` on the result no longer type-checks.
|
|
72
|
+
|
|
73
|
+
The call has to be narrowed first, in both versions: `write` has always been able to return
|
|
74
|
+
`void`, so `.catch()` was never available on the bare call.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const result = destination.write(payload)
|
|
78
|
+
|
|
79
|
+
// before — `result` narrowed to `Promise<void>`, so `.catch` was there
|
|
80
|
+
if (result !== undefined) result.catch(handle)
|
|
81
|
+
|
|
82
|
+
// after — it narrows to `PromiseLike<void>`; assimilate to get `.catch` back
|
|
83
|
+
if (result !== undefined) Promise.resolve(result).catch(handle)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
There is no type that gives implementations the freedom and callers a full `Promise`; this release
|
|
87
|
+
chooses the implementations, because a destination that cannot be written in the first place is the
|
|
88
|
+
worse failure.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **A throwing `minLevel` getter could abort application bootstrap.** `effectiveLevelOf` reads the
|
|
93
|
+
consumer-defined `minLevel`, and `readonly minLevel?: LogLevel` does not stop it being a getter.
|
|
94
|
+
It is read on BOTH branches of the init loop — including inside the catch that exists so a failing
|
|
95
|
+
destination cannot stop the application from starting — so a throw there took the app down at
|
|
96
|
+
start-up and stranded every destination after it. The read is guarded inside `effectiveLevelOf`,
|
|
97
|
+
which covers every call site at once, and falls back to the module level: what a destination
|
|
98
|
+
without a `minLevel` gets anyway. Same defect class as the `name` getter, in the sibling property.
|
|
99
|
+
|
|
100
|
+
The registry was not even the FIRST reader. `pino-factory` builds the multistream entry from
|
|
101
|
+
`destination.minLevel` at provider-construction time, outside any guard and before any lifecycle
|
|
102
|
+
hook — so a hostile getter failed the factory and the application never started, earlier than any
|
|
103
|
+
fail-soft path could contain it. Found by sweeping every consumer-defined property read in
|
|
104
|
+
`src/server` rather than by fixing the site that was reported: eight of the nine were already
|
|
105
|
+
inside a `try`, and that ninth one made the registry fix unreachable in practice. Both sites share
|
|
106
|
+
one `safeMinLevel` now, and it PINS the first answer per destination.
|
|
107
|
+
|
|
108
|
+
Guarding the read stops the throw but not the DISAGREEMENT. Nothing makes the getter pure, and two
|
|
109
|
+
independent consumers read it: the factory, which fixes the multistream entry's level, and the
|
|
110
|
+
registry, which records the same destination's health level. A stateful getter answering `info` to
|
|
111
|
+
one and `error` to the other would let an `error` sink be credited with covering held `info`
|
|
112
|
+
entries, and a buffering destination would discard its only copy of them — a loss path, not an
|
|
113
|
+
inconsistency. Pinning makes the two agree by construction; `readonly minLevel?: LogLevel` says it
|
|
114
|
+
should not change anyway.
|
|
115
|
+
|
|
116
|
+
- **The shutdown reporter could be made to forge a second stderr record.** Its guard used
|
|
117
|
+
`escapeControlCharacters`, which preserves newlines ON PURPOSE because a stack trace is
|
|
118
|
+
legitimately multi-line — but it was applied to `cause.message` when no stack existed, and to any
|
|
119
|
+
non-Error thrown value. A destination rejecting with `'failed\n[forged entry]'` therefore wrote a
|
|
120
|
+
second raw line that an operator reads as a genuine record. The two escapers are not
|
|
121
|
+
interchangeable: a stack goes through `escapeControlCharacters`, a message or a non-Error value
|
|
122
|
+
through `toSingleLineMessage`. The regression asserts the emitted text is ONE line, not merely that
|
|
123
|
+
an escape appears in it.
|
|
124
|
+
|
|
125
|
+
- **A failing `onShutdown` could abort the teardown of every destination behind it.** The registry
|
|
126
|
+
built its stderr report by coercing the thrown value ABOVE the guard: reading `stack`/`message` on
|
|
127
|
+
an `Error` with hostile getters, or `String(cause)` on a value with a throwing `Symbol.toPrimitive`,
|
|
128
|
+
throws from inside the very `catch` that exists to keep one bad sink from mattering. The throw then
|
|
129
|
+
propagated out of `onApplicationShutdown`, and every destination still queued lost its flush —
|
|
130
|
+
turning one destination's bad error object into lost entries everywhere.
|
|
131
|
+
|
|
132
|
+
The coercion is now inside its own guard, falls back to `UnknownError` when the value cannot be
|
|
133
|
+
read at all, and the text is escaped like every other terminal-bound path. The regression test
|
|
134
|
+
asserts that the SECOND destination still shuts down, not merely that nothing threw: continuing the
|
|
135
|
+
teardown is the property that matters. Reverting the guard makes it fail — verified, not assumed.
|
|
136
|
+
|
|
137
|
+
**The same gap existed at every other reporting call site, and the first fix only closed one of
|
|
138
|
+
them.** `reportWriteFailure(destination.name, …)` in the write adapter and `reportInitFailure`
|
|
139
|
+
in the registry both read the name at the call site — inside the catch — so a hostile getter turned
|
|
140
|
+
a contained write failure into an unhandled rejection, and a failed `onInit` into an aborted
|
|
141
|
+
bootstrap, contradicting the comment one line above it that the library never throws to abort boot.
|
|
142
|
+
A shared `safeDestinationName` now performs that read under a guard, and every reporter uses it.
|
|
143
|
+
|
|
144
|
+
The destination's own `name` is read inside a guard as well, and a separate one:
|
|
145
|
+
`readonly name: string` does not stop a consumer implementing it as a getter, and reading it at the
|
|
146
|
+
call site would have left it inside the `catch` with the same cost. That second gap was found by
|
|
147
|
+
reviewing the fix for the first — the correction had reproduced the defect one layer up.
|
|
148
|
+
|
|
149
|
+
The control-character escaping on this path is now asserted rather than assumed. Every shutdown
|
|
150
|
+
test used plain ASCII, so replacing the escaper with an identity function would have left them all
|
|
151
|
+
green — a 100% mutation score does not cover a transformation no test observes. The new case emits
|
|
152
|
+
a stack containing an ANSI escape and a C1 character, and asserts that those are neutralized while
|
|
153
|
+
the stack's own newlines survive.
|
|
154
|
+
|
|
155
|
+
Found by a review comment on the DOCUMENTATION examples. The examples mirrored the implementation
|
|
156
|
+
faithfully, which is exactly why the defect was worth chasing back into `src/`: a guarded writer
|
|
157
|
+
never guarded the arguments handed to it, and that had been true in three places.
|
|
158
|
+
|
|
159
|
+
### Documentation
|
|
160
|
+
|
|
161
|
+
- **Snippets that referenced symbols which were not there are now caught by a parser, not by
|
|
162
|
+
eye.** Five review rounds each found another documented example using `safeMinLevel`,
|
|
163
|
+
`LOGGER_OPTIONS_TOKEN`, `PREVIEW_LENGTH`, `this.reportShutdownFailure` or an outright invented
|
|
164
|
+
`RESERVED_LOG_KEYS.LOGGER_DESTINATION_SHUTDOWN_FAILED` with nothing to resolve them. `pnpm
|
|
165
|
+
check:docs` parses every TypeScript block — both `ts` and `typescript` fences — and fails on
|
|
166
|
+
three things: a symbol this package declares in `src/` used without being imported or declared,
|
|
167
|
+
a `this.member` the class shown does not define, and a `CONSTANT.KEY` the real constant does not
|
|
168
|
+
have. It reads the syntax tree rather than the text, so a name in a comment is a mention and not
|
|
169
|
+
a use. It runs in CI and in `prepublishOnly`, because a gate nothing executes is not a gate.
|
|
170
|
+
|
|
171
|
+
The 43 pre-existing cases in the planning documents sit in a baseline that shrinks and does not
|
|
172
|
+
grow: regenerating writes the intersection with what still reproduces, so a defect introduced in
|
|
173
|
+
the same edit is never adopted, and an entry that stops reproducing fails too rather than
|
|
174
|
+
lingering. Widening what the check looks at is the one case where the list legitimately grows,
|
|
175
|
+
and it takes a separate `--adopt-new` flag so that decision is stated rather than taken
|
|
176
|
+
silently.
|
|
177
|
+
|
|
178
|
+
- **The destinations guide credited the wrong component with building the fan-out.** It said
|
|
179
|
+
`DestinationRegistry` wraps each destination into the `pino.multistream` array; that happens in
|
|
180
|
+
`buildPinoInstance` (`src/server/pino-factory.ts:574`), and the registry owns the lifecycle hooks
|
|
181
|
+
and the health record instead. The mistake was not only prose — the snippet's relative imports
|
|
182
|
+
were written as `../utils/...`, which resolve only from `services/`, so following the guide gave
|
|
183
|
+
paths that do not exist from where the code actually lives.
|
|
184
|
+
|
|
185
|
+
- **The specification gave the wrong reason for reporting init failures on stderr.** It said Pino
|
|
186
|
+
was not yet wired at that point. It is — the instance is built during provider construction,
|
|
187
|
+
which is why the same method emits `LOGGER_BOOTSTRAP_OK` through the logger a few lines later.
|
|
188
|
+
The actual reason is the fan-out: the logger writes to the very set containing the sink that
|
|
189
|
+
just failed. An implementer reading the old text would have inferred the wrong lifecycle order.
|
|
190
|
+
|
|
191
|
+
- **The shipped `README.md` no longer teaches the old contract.** Its `ILogDestination` reference
|
|
192
|
+
still declared `Promise<void>` on `write`, `onInit` and `onShutdown`, and omitted `onRegistryReady`
|
|
193
|
+
entirely — so the one document most consumers read described types incompatible with the interface
|
|
194
|
+
and hid a hook from anyone who buffers. It ships inside the package, which makes it the copy that
|
|
195
|
+
matters most.
|
|
196
|
+
|
|
197
|
+
- **Overflow was only reported when a later flush failed.** A request that stalls long enough to
|
|
198
|
+
overflow the cap and then SUCCEEDS runs no catch at all, so the discarded entries were lost with
|
|
199
|
+
nothing said — the opposite of the policy stated one paragraph above. Both examples report the
|
|
200
|
+
dropped count on every settlement, not only from the failure path.
|
|
201
|
+
|
|
202
|
+
- **A guarded WRITER does not guard the arguments handed to it.** The documented adapters called
|
|
203
|
+
`writeStderrSafely` with an interpolated `${String(err)}`, and that coercion runs before the call:
|
|
204
|
+
an unknown rejection value with a hostile `Symbol.toPrimitive`, or an `Error` whose `message` getter
|
|
205
|
+
throws, takes the handler down before `callback()` — an unhandled rejection produced from inside the
|
|
206
|
+
containment. The previous release note credited the safe writer with a protection it never had.
|
|
207
|
+
Every documented adapter now uses `reportDestinationFailure`, which does the coercion inside its own
|
|
208
|
+
guard and escapes control characters.
|
|
209
|
+
|
|
210
|
+
- **A raced timeout is not a timeout.** The database example briefly carried a `Promise.race`
|
|
211
|
+
deadline, which settles the AWAIT without cancelling the query: the batch would be requeued while
|
|
212
|
+
the original `createMany` was still running, the next flush would start a second one, and a slow
|
|
213
|
+
database would accumulate concurrent inserts of the same batch — duplicates, not just delay. The
|
|
214
|
+
bound belongs at the driver, where it aborts the statement server-side (`statement_timeout` on the
|
|
215
|
+
connection string), and the example says so instead of demonstrating a race. The HTTP example keeps
|
|
216
|
+
`AbortSignal.timeout`, which does cancel.
|
|
217
|
+
|
|
218
|
+
- **Retaining a failed batch forever is a memory leak.** Requeueing on failure while `write` keeps
|
|
219
|
+
appending is an unbounded queue: a sustained outage ends in an OOM, which loses every held entry
|
|
220
|
+
AND the application. Both worked examples now cap the buffer and drop the OLDEST beyond it — the
|
|
221
|
+
newest entries describe the incident — and report the dropped count rather than discarding
|
|
222
|
+
silently. A destination that cannot lose anything is told to use a durable spool, not a buffer.
|
|
223
|
+
|
|
224
|
+
- **Four defects in the specification's own examples**, all from wiring `DestinationHealth` through
|
|
225
|
+
them: the factory call site still passed the old three arguments, so a `LogLevel` landed where the
|
|
226
|
+
health tracker was expected; the adapter never checked `isFailed`/`shouldRescue`, though the
|
|
227
|
+
fan-out builds a stream for every registered destination and that branch is what keeps a
|
|
228
|
+
failed-init sink out of it; the new provider dependency omitted the explicit `@Inject` this
|
|
229
|
+
repository requires because tsup strips decorator metadata; and the shutdown loop contained
|
|
230
|
+
failures with `Promise.allSettled` while reporting none of them, contradicting the guarantee the
|
|
231
|
+
destination examples state.
|
|
232
|
+
|
|
233
|
+
- **The consumer-facing stderr guard reintroduced a defect the library had already fixed.** It
|
|
234
|
+
remembered `guarded = true` in a boolean, which goes stale the moment anything else removes the
|
|
235
|
+
listener — a test doing cleanup is enough — and every later EPIPE is uncaught again. It also
|
|
236
|
+
treated ANY existing `'error'` listener as proof the stream was safe, though a consumer's handler
|
|
237
|
+
may rethrow. `safe-stdio.util.ts` arrived at the right shape after exactly this: check whether OUR
|
|
238
|
+
listener is in `stream.listeners('error')`, and only ever add. The documented helper now does the
|
|
239
|
+
same.
|
|
240
|
+
|
|
241
|
+
- **The Loki example discarded its batch on every HTTP error.** `fetch` rejects only on a
|
|
242
|
+
network-level failure; 401, 429 and 500 resolve normally, so the retention path added for it never
|
|
243
|
+
ran on the failures a log sink actually sees. It checks `response.ok` and throws now. Its timer
|
|
244
|
+
also still detached the flush directly, which the same commit had fixed only in the Postgres
|
|
245
|
+
example.
|
|
246
|
+
|
|
247
|
+
- **A background flush was invisible to shutdown.** Both examples now chain background flushes into
|
|
248
|
+
one tracked promise and `onShutdown` awaits it before the final drain: a flush that fails requeues
|
|
249
|
+
its batch, and without the await those entries sit in a buffer nobody drains again.
|
|
250
|
+
|
|
251
|
+
- **The specification formatted failure reports inline instead of using `reportDestinationFailure`.**
|
|
252
|
+
An `Error` with a throwing `message` getter — or a non-Error with a throwing coercion hook — makes
|
|
253
|
+
an inline `String(cause)` throw from inside the very handler that exists to contain failures, and
|
|
254
|
+
the inline form also skips the control-character escaping applied to remote-derived text. All four
|
|
255
|
+
reporting sites in the document now go through the helper that does the coercion inside its own
|
|
256
|
+
guard.
|
|
257
|
+
|
|
258
|
+
- **The batch-retention fix introduced a crash path of its own.** Making `flush()` rethrow is right
|
|
259
|
+
for the shutdown caller, which awaits it — and wrong for the two background callers, which detach
|
|
260
|
+
the promise. A detached rejection is an unhandled rejection, which terminates the process on
|
|
261
|
+
Node 24: a loss path traded for a crash path, in the same commit that closed the loss. The two
|
|
262
|
+
detached call sites now go through a `flushInBackground()` that swallows what `flush` already
|
|
263
|
+
retained and reported, while `onShutdown` still awaits `flush` directly, where the rejection has
|
|
264
|
+
somewhere to go.
|
|
265
|
+
|
|
266
|
+
- **The Loki example was still discarding its batch** — the same defect as the Postgres one, one
|
|
267
|
+
section earlier, with `// Fail soft` written over it. Fail-soft means not crashing the host; it
|
|
268
|
+
never meant dropping the entries. It retains and reports now, and the guarded stderr helper both
|
|
269
|
+
examples use is defined once, ahead of its first use, instead of being named and left undefined.
|
|
270
|
+
|
|
271
|
+
- **The documented adapters recorded a failure only after the promise rejected**, leaving the
|
|
272
|
+
in-flight window the implementation closes with `markWritePending`/`markWriteSettled`. Readiness
|
|
273
|
+
can be computed while a write is still pending; without the pending count it reads as silent
|
|
274
|
+
success, and a buffering sink discards its copy moments before that write rejects.
|
|
275
|
+
|
|
276
|
+
- **The specification's registry example referenced members it did not declare** — `this.active`,
|
|
277
|
+
`this.health`, `this.effectiveLevelOf` — so the authoritative document showed an implementation
|
|
278
|
+
that would not compile. It now declares them, and its shutdown loop iterates the ACTIVE subset
|
|
279
|
+
rather than every registered destination, matching the implementation: a sink whose `onInit`
|
|
280
|
+
failed may never have acquired what `onShutdown` would close.
|
|
281
|
+
|
|
282
|
+
- **The documented adapters reported a failed write but never RECORDED it.** The prose added with
|
|
283
|
+
them says the adapter "contains it, records it and reports it"; the examples did the first and the
|
|
284
|
+
third. Recording is the half that prevents the loss: without `markWriteFailed`, readiness still
|
|
285
|
+
counts the sink as having taken the entry and another destination may discard the copy it was
|
|
286
|
+
holding. The examples now take the health tracker and mark the sink before reporting.
|
|
287
|
+
|
|
288
|
+
- **The worked Postgres destination still lost a batch, in the example that demonstrates not losing
|
|
289
|
+
one.** Removing the `try/catch` from its `write` was not enough: the flush it triggers runs after
|
|
290
|
+
that `write` returned, and `flush()` ended in `catch { /* fail-soft */ }`, discarding every entry
|
|
291
|
+
in the batch while the adapter had already recorded them as taken. A background flush is the one
|
|
292
|
+
failure a destination must handle itself — so it now returns the batch to the buffer, reports it,
|
|
293
|
+
and rethrows so the caller sees it. The stderr helper it uses is written out in full, because the
|
|
294
|
+
library's guarded one is internal and a consumer cannot import it.
|
|
295
|
+
|
|
296
|
+
- **The specification contained two incompatible lifecycle contracts.** The `onRegistryReady`
|
|
297
|
+
guarantee added above says the hook runs for every registered destination; the `DestinationRegistry`
|
|
298
|
+
example further down truncated the registered list to the survivors of `onInit` and never called
|
|
299
|
+
the hook at all. The example now keeps the registered set, writes only to the active subset, and
|
|
300
|
+
awaits the hook for every destination with per-destination isolation.
|
|
301
|
+
|
|
302
|
+
- **The guide told destinations to swallow their own write failures — a loss path, in the document
|
|
303
|
+
that defines the fail-soft contract.** "Catch every error in `write` and log it via
|
|
304
|
+
`process.stderr.write`" reads as prudence and is the opposite: a swallowed failure looks like a
|
|
305
|
+
successful write from outside, so `markWriteFailed` never runs for that sink, readiness can then
|
|
306
|
+
credit it with entries it dropped, and ANOTHER destination discards the held copies it was keeping.
|
|
307
|
+
The entry stops existing anywhere — exactly the outcome this release exists to prevent. The advice
|
|
308
|
+
is now to let the failure reach the adapter, which contains it, RECORDS it and reports it. Two
|
|
309
|
+
anti-pattern entries carried the same instruction, and the worked Postgres destination practised
|
|
310
|
+
it; all three are corrected.
|
|
311
|
+
|
|
312
|
+
- **Every reporter in the documented examples wrote to `process.stderr` directly.** The guide
|
|
313
|
+
explains, three sections earlier, that a closed pipe reports EPIPE asynchronously and kills the
|
|
314
|
+
process — which is why `safe-stdio.util.ts` exists. The containment helpers added one commit ago
|
|
315
|
+
ignored that and would have traded a contained destination failure for an uncaught exception. The
|
|
316
|
+
library's own paths now route through `writeStderrSafely` in every example. The consumer-facing
|
|
317
|
+
examples do not, because that helper is internal and unexported; they point at the EPIPE note
|
|
318
|
+
instead, which is the honest instruction for code outside this package.
|
|
319
|
+
|
|
320
|
+
- **The technical specification did not state that `onRegistryReady` runs for a destination whose own
|
|
321
|
+
`onInit` rejected.** It runs for every registered destination, verified against
|
|
322
|
+
`notifyRegistryReady`, and a sink that failed to initialize is exactly the one most likely to be
|
|
323
|
+
holding entries it now has to resolve. Left unstated, an implementer would reasonably assume the
|
|
324
|
+
opposite and strand them.
|
|
325
|
+
|
|
326
|
+
- **The drain contract described a return value `_write` does not have.** It said the adapter signals
|
|
327
|
+
backpressure by `_write` returning `false`; `_write` returns `void`. Deferring its callback is the
|
|
328
|
+
actual mechanism — the chunk stays in flight, the internal buffer grows, and it is the PUBLIC
|
|
329
|
+
`writable.write()` that returns `false` at `highWaterMark`. The conclusion the section drew was
|
|
330
|
+
right and the model under it was not, which is the kind of text a destination author reasons from.
|
|
331
|
+
|
|
332
|
+
- **The documented adapter examples contradicted the fail-soft contract they were illustrating.**
|
|
333
|
+
They routed a failed write into `callback(err)`, and the guide listed that as an advantage — "errors
|
|
334
|
+
are propagated as a callback err (not as an exception that crashes the app)". A `Writable` given an
|
|
335
|
+
error in its callback emits `'error'`, which with no listener terminates the host. The production
|
|
336
|
+
adapter has always done the opposite, and says so in a comment one line above the code: contain the
|
|
337
|
+
failure, report it on stderr, complete the write as successful. Every example, and the two prose
|
|
338
|
+
lines that recommended the pattern, now match it.
|
|
339
|
+
|
|
340
|
+
- **Every documented `destinationToStream` example branched on `result instanceof Promise`** — the
|
|
341
|
+
exact defect fixed above, in the snippets that teach people how to write a destination. A reader
|
|
342
|
+
implementing against the guide, the specification or the plan documents would have reproduced the
|
|
343
|
+
escaped rejection and the lost entry. All of them now branch on `undefined` and assimilate with
|
|
344
|
+
`Promise.resolve`.
|
|
345
|
+
|
|
346
|
+
These two were found only because a review kept pulling the same thread. Correcting the interface
|
|
347
|
+
and the four excerpts that restate it still left the README and every worked example behind: a
|
|
348
|
+
contract lives in more places than the ones that quote its signature.
|
|
349
|
+
|
|
350
|
+
So the rule is now a gate rather than a habit. `check:published` compiled only README blocks that
|
|
351
|
+
**import** the package, on the reasoning that naming the API is not a claim about it — which left a
|
|
352
|
+
block that _re-declares_ an exported type unchecked, and that is exactly where the README drifted.
|
|
353
|
+
It now compiles those too, with three assertions because each covers what the previous one cannot:
|
|
354
|
+
mutual assignability catches a changed return type; key parity catches an omitted member, which is
|
|
355
|
+
assignable in both directions and is how the absent `onRegistryReady` survived; per-member type
|
|
356
|
+
identity catches a changed PARAMETER, which stays bivariant even under `strictFunctionTypes`.
|
|
357
|
+
|
|
358
|
+
Both of the later assertions were added because a measured version of the gate let a real drift
|
|
359
|
+
through: the first passed a README declaring `write(payload: unknown)`, and the second — comparing
|
|
360
|
+
members as `T[K]` — passed one that dropped `readonly`, because indexed access discards property
|
|
361
|
+
modifiers. Each hole was reproduced against the version that had it before being closed, and each
|
|
362
|
+
assertion is verified the same way: introduce the drift, watch the gate name it, restore.
|
|
363
|
+
|
|
364
|
+
- **`heldEntriesDeliveredElsewhere` is documented as best-effort rather than as proof.** The JSDoc
|
|
365
|
+
said `true` requires no write still in flight, which reads as complete accounting — while the
|
|
366
|
+
`1.2.8` note already admitted that writes queued inside the `Writable` adapter are invisible until
|
|
367
|
+
`destination.write()` is called. Both statements shipped in the same release, and they contradict
|
|
368
|
+
each other.
|
|
369
|
+
|
|
370
|
+
Calling it best-effort was not enough on its own: the surrounding prose still framed the decision
|
|
371
|
+
as "discard only on proof", which is the same contradiction one paragraph up. The contract is now
|
|
372
|
+
stated as what it is — a **deduplication signal**, where `true` means the smaller risk is dropping
|
|
373
|
+
the held copy, not that the entry is safe. The library's own destinations dedupe on it because a
|
|
374
|
+
duplicated boot line is a smaller harm than a lost one; a destination that cannot tolerate any loss
|
|
375
|
+
is told to ignore the flag and always emit. The guide, the specification and the JSDoc now say the
|
|
376
|
+
same thing, so a reader meets one story instead of three.
|
|
377
|
+
|
|
378
|
+
Reframing the parameter was still not the end of it: `PrettyDevDestination` went on calling the
|
|
379
|
+
same value "a proven fact" in one comment and instructing the hook to "discard only what is proven
|
|
380
|
+
delivered" in another, and this changelog kept the word too. One class saying both things is worse
|
|
381
|
+
than either alone — "proven" invites a destination author to discard with full confidence, which is
|
|
382
|
+
precisely the loss this release refuses. The vocabulary is now one word everywhere: a signal, with
|
|
383
|
+
the queued-write blind spot named where the decision is taken.
|
|
384
|
+
|
|
385
|
+
## 1.2.8 - 2026-08-15 (merged, never published)
|
|
386
|
+
|
|
387
|
+
### Fixed
|
|
388
|
+
|
|
389
|
+
- **A pretty destination registered alongside another sink no longer prints every boot entry twice.**
|
|
390
|
+
The fan-out hands each entry to every registered destination, so entries buffered before
|
|
391
|
+
`PrettyDevDestination.onInit` had ALSO been delivered to the co-destination. When the pretty sink
|
|
392
|
+
then failed to initialize, it drained its buffer raw to stdout, producing a second copy of every
|
|
393
|
+
boot line. Measured on the supported
|
|
394
|
+
`[DefaultStdoutDestination(), PrettyDevDestination()]` pair: two occurrences of one entry.
|
|
395
|
+
|
|
396
|
+
**You were affected only if you registered pretty ALONGSIDE another destination** — the shape a
|
|
397
|
+
derived backend reaches by adding an HTTP or file sink beside pretty for local development. A
|
|
398
|
+
one-element `destinations` list is structurally immune, because the option REPLACES the default
|
|
399
|
+
sink rather than adding to it, so pretty-alone and stdout-alone can never produce the pair.
|
|
400
|
+
Worth stating explicitly: "duplicated boot lines" is not a symptom from which a reader would guess
|
|
401
|
+
the triggering configuration.
|
|
402
|
+
|
|
403
|
+
The destination could not decide this alone: at `onInit` failure it has no way to know what became
|
|
404
|
+
of the entries. The decision moves to a new optional `ILogDestination.onRegistryReady`, which the
|
|
405
|
+
registry calls once every `onInit` has settled.
|
|
406
|
+
|
|
407
|
+
**The policy is: prefer a duplicated line over a lost one.** The hook carries ONE signal — whether
|
|
408
|
+
another live sink appears to have accepted everything this destination accepted — and the
|
|
409
|
+
destination dedupes only on that. `true` requires all of: another destination, initialized, at or
|
|
410
|
+
below this level, with no write failure and nothing still in flight. Anything less certain reads as
|
|
411
|
+
`false`, and the entries are emitted.
|
|
412
|
+
|
|
413
|
+
An earlier draft handed over three hints and let the destination judge. Every judgement had a
|
|
414
|
+
losing branch, and review found them one at a time: trusting "a sink survived" lost entries to a
|
|
415
|
+
sink at a higher level; trusting the level lost them to a sink that was itself the asker; then to
|
|
416
|
+
one whose writes were throwing; then to one whose write had not settled yet. Collapsing to a single
|
|
417
|
+
signal is what removed the class, rather than the four instances.
|
|
418
|
+
|
|
419
|
+
In the configuration that motivated this — pretty beside `DefaultStdoutDestination` — nothing
|
|
420
|
+
duplicates: the stdout sink is live at the same level, the signal reads `true`, and the held copies
|
|
421
|
+
are dropped. Duplication is left wherever the signal cannot be given, which is exactly where
|
|
422
|
+
discarding would risk silence.
|
|
423
|
+
|
|
424
|
+
**One narrow gap is known and left open rather than papered over, and it is the reason the value is
|
|
425
|
+
a signal and not a proof.** The accounting covers writes this library has SEEN — resolved, rejected,
|
|
426
|
+
or in flight. A write still queued inside the `Writable` adapter, behind a slow async destination
|
|
427
|
+
that has not been called yet, is invisible to it: the signal can read `true` while that entry has
|
|
428
|
+
not reached the sink. It will normally arrive (it is
|
|
429
|
+
queued, not lost); the residual risk is a queued write that later fails, and the entry existed only
|
|
430
|
+
in a buffer that was discarded on the strength of a different sink's record. Closing it means
|
|
431
|
+
readiness waiting for every pre-ready stream to drain, which is a larger change than the boot-time
|
|
432
|
+
cosmetic defect this whole path exists to fix. Stated here so a consumer can weigh it, rather than
|
|
433
|
+
implied by a guarantee that does not quite hold.
|
|
434
|
+
|
|
435
|
+
**One case is NOT deduplicated, and the first draft of this note claimed it was.** A shutdown that
|
|
436
|
+
happens before the registry's `onModuleInit` ran — the application aborting mid-bootstrap — still
|
|
437
|
+
drains the buffer raw. The multistream is wired by a provider factory while NestJS assembles the
|
|
438
|
+
graph, so entries can already have reached a co-destination by then, but the readiness hook has not
|
|
439
|
+
run and the destination has no way to learn that. It drains, because between a duplicated boot line
|
|
440
|
+
and a lost one this library picks the duplicate. Narrowing the claim rather than widening the fix:
|
|
441
|
+
making that path registry-aware would require the registry, which by definition has not run.
|
|
442
|
+
|
|
443
|
+
Reported by Copilot on the `1.2.6` pull request, in a review body rather than as an inline
|
|
444
|
+
comment — see the note below.
|
|
445
|
+
|
|
446
|
+
### Security
|
|
447
|
+
|
|
448
|
+
- **`messageFormat` interpolates every placeholder except `{msg}` raw, and that reopens terminal
|
|
449
|
+
log forging.** This library normalizes line separators and control characters in `msg` and in the
|
|
450
|
+
stack, never in metadata — escaping data would mean rewriting what a consumer asked to be logged.
|
|
451
|
+
`pino-pretty` substitutes whatever the field holds, so a newline in an interpolated field splits
|
|
452
|
+
one entry into two, and the second reads like a genuine record. Measured with a newline in
|
|
453
|
+
`context`:
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
lines produced by ONE entry: 2
|
|
457
|
+
1: "[10:35:14.484] INFO: [Auth"
|
|
458
|
+
2: "[10:00:00.000] INFO: FORGED admin promoted] real entry …"
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The option stays — rejecting non-`msg` placeholders would remove the feature, and the interpolation
|
|
462
|
+
happens inside `pino-pretty` where this library cannot escape it. What changes is that the JSDoc
|
|
463
|
+
now says so, and the README no longer recommends the pattern without the warning: interpolate only
|
|
464
|
+
fields your own code sets, never one carrying user input.
|
|
465
|
+
|
|
466
|
+
### Documentation
|
|
467
|
+
|
|
468
|
+
- **`hideObject` is scoped to the pretty destination, not to the entry.** The doc said a field hidden
|
|
469
|
+
there "is not visible anywhere", which holds only when pretty is the sole destination. Registered
|
|
470
|
+
alongside another sink, that sink still receives the complete entry.
|
|
471
|
+
|
|
472
|
+
- **The `safe-stdio` specs no longer leave `'error'` listeners on the shared process streams.** They
|
|
473
|
+
did so deliberately, with a rationale that the same pull request had already made stale: cleanup
|
|
474
|
+
had once broken the guarantee under test because the module remembered which streams it guarded,
|
|
475
|
+
and `ensureGuarded` was then changed to inspect the actual listener list. Every
|
|
476
|
+
`isolateModulesAsync` load installs a distinct handler, so the file accumulated one per case on a
|
|
477
|
+
stream that outlives the suite — where a later spec asserting an unhandled stream error propagates
|
|
478
|
+
would be silently satisfied. Each block now restores its exact baseline, and asserts that it did.
|
|
479
|
+
|
|
480
|
+
- **A process note, recorded because it cost two releases.** Copilot reports some findings inside a
|
|
481
|
+
`Suppressed comments` block in the review BODY, where they never become review threads. Checking
|
|
482
|
+
`reviewThreads` for `isResolved == false` — the documented way to avoid trusting the page — reports
|
|
483
|
+
zero open and is telling the truth about threads while missing these entirely. Ten such findings
|
|
484
|
+
sat unread on the `1.2.6` pull request, including the two fixed above. Reviews are now read
|
|
485
|
+
alongside threads.
|
|
486
|
+
|
|
487
|
+
- **A hand-built `PinoLoggerService` is not this library's behaviour, and the constructor doc used to
|
|
488
|
+
invite one without saying so.** `new PinoLoggerService(pino({...}, sink))` does not throw — the
|
|
489
|
+
parameter default exists for exactly that — but the Pino instance you built has none of the wiring
|
|
490
|
+
`buildPinoInstance` installs, and the omissions are silent. No `err` serializer, so the `cause`
|
|
491
|
+
chain **disappears entirely**: no marker, no warning, and a shape that reads as "the field was
|
|
492
|
+
never set". Also no name-based redaction, no trace-context mixin, no size bound.
|
|
493
|
+
|
|
494
|
+
Found the honest way: a consumer hand-built one to check whether a `cause` carried its `code`,
|
|
495
|
+
measured absence, and went to read the built `dist` before reporting it — where they found their
|
|
496
|
+
harness was the cause. The doc now names what such an instance lacks and points at booting the
|
|
497
|
+
module for a faithful one.
|
|
498
|
+
|
|
499
|
+
- **What redaction covers, stated as a boundary rather than left to be inferred.** It matches field
|
|
500
|
+
NAMES, not values: a secret under a covered name is censored at any depth, and the same secret
|
|
501
|
+
embedded **inside a string** under an uncovered name travels intact. Scanning values for
|
|
502
|
+
secret-shaped text would mean rewriting what a consumer asked to be logged, wrong in both
|
|
503
|
+
directions and silently — so the boundary is real and belongs in the open. It matters more since
|
|
504
|
+
`1.2.7`: preserving an error's own fields at every depth makes whatever a `cause` carries more
|
|
505
|
+
visible, including what should not be there.
|
|
506
|
+
|
|
507
|
+
The README also now says which redactor governs what — `redactStrategy` covers the consumer's
|
|
508
|
+
payload, their child bindings and any serializer they supply; the built-in `err` serializer is the
|
|
509
|
+
library's own output and is always name-walked, because `fast-redact`'s four-wildcard ceiling
|
|
510
|
+
cannot reach a serialized `cause` chain.
|
|
511
|
+
|
|
512
|
+
- **The `1.2.7` notes used `a catch in main.ts` as the example, and it is the wrong one.** Not false
|
|
513
|
+
as a general statement — a handler that wraps and logs does exist — but it became the canonical
|
|
514
|
+
example of the motivating case, and for the consumer who reported that case it describes a path
|
|
515
|
+
that does not exist: their config validation rejects inside `NestFactory.create`, before the
|
|
516
|
+
logger is attached, and reports through `console.error` on stderr without touching the serializer.
|
|
517
|
+
A reader anchors on the example. Now "any handler that wraps what it caught before logging it".
|
|
518
|
+
Corrected in the README and in the `1.2.7` entry; the `v1.2.7` tag annotation is left as published,
|
|
519
|
+
since rewriting a published tag is worse than the imprecision it would fix.
|
|
520
|
+
|
|
14
521
|
## [1.2.7] - 2026-08-15
|
|
15
522
|
|
|
16
523
|
### Security
|
|
@@ -79,7 +586,8 @@ heading here.
|
|
|
79
586
|
- **An error's own properties now survive at every depth of the `cause` chain, not just at the top.**
|
|
80
587
|
`code`, `statusCode` and any domain field an application attaches were copied onto the error handed
|
|
81
588
|
to the log call and **dropped the moment that same error was wrapped as someone else's `cause`** —
|
|
82
|
-
which is what
|
|
589
|
+
which is what any handler that wraps what it caught does before logging it. Measured on the
|
|
590
|
+
published `1.2.6`:
|
|
83
591
|
|
|
84
592
|
```json
|
|
85
593
|
{
|
|
@@ -1231,7 +1739,11 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1231
1739
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1232
1740
|
`release.yml`, Dependabot, and issue templates
|
|
1233
1741
|
|
|
1234
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
1742
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...HEAD
|
|
1743
|
+
[1.2.9]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...v1.2.9
|
|
1744
|
+
|
|
1745
|
+
<!-- 1.2.8 has no link: it was merged but never tagged, so there is no v1.2.8 to compare against. See its section above. -->
|
|
1746
|
+
|
|
1235
1747
|
[1.2.7]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...v1.2.7
|
|
1236
1748
|
[1.2.6]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...v1.2.6
|
|
1237
1749
|
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
package/README.md
CHANGED
|
@@ -556,7 +556,8 @@ error, `statusCode` from an HTTP layer, whatever domain fields you attached —
|
|
|
556
556
|
|
|
557
557
|
This matters because `code` is usually the field you alert on. Until 1.2.7 those properties survived
|
|
558
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
|
|
559
|
+
someone else's `cause` — which is what any handler that wraps what it caught does before
|
|
560
|
+
logging it. They never shadow a
|
|
560
561
|
field the serializer derives, and they pass through the same name-based redaction and size bound as
|
|
561
562
|
any other serializer output.
|
|
562
563
|
|
|
@@ -790,6 +791,7 @@ new PrettyDevDestination({
|
|
|
790
791
|
})
|
|
791
792
|
|
|
792
793
|
// Message only, with the context pulled back into the line.
|
|
794
|
+
// See the messageFormat warning below before interpolating any other field.
|
|
793
795
|
new PrettyDevDestination({
|
|
794
796
|
view: { hideObject: true, messageFormat: '[{context}] {msg}' }
|
|
795
797
|
})
|
|
@@ -804,7 +806,7 @@ new PrettyDevDestination({
|
|
|
804
806
|
| `translateTime` | `'SYS:HH:MM:ss.l'` | Or `false` for the raw timestamp |
|
|
805
807
|
| `colorize` | `true` | ANSI colour |
|
|
806
808
|
|
|
807
|
-
> **`hideObject` hides it
|
|
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.
|
|
808
810
|
|
|
809
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:
|
|
810
812
|
>
|
|
@@ -817,6 +819,16 @@ new PrettyDevDestination({
|
|
|
817
819
|
|
|
818
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.
|
|
819
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
|
+
|
|
820
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.
|
|
821
833
|
|
|
822
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.
|
|
@@ -1043,6 +1055,21 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
|
|
|
1043
1055
|
|
|
1044
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.
|
|
1045
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
|
+
|
|
1046
1073
|
### A closed pipe does not kill the process
|
|
1047
1074
|
|
|
1048
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.
|
|
@@ -1297,9 +1324,13 @@ interface ILogDestination {
|
|
|
1297
1324
|
readonly name: string
|
|
1298
1325
|
readonly minLevel?: LogLevel
|
|
1299
1326
|
/** `payload` is a newline-terminated JSON entry, UTF-8 encoded — not an object. */
|
|
1300
|
-
write(payload: string): void |
|
|
1301
|
-
onInit?():
|
|
1302
|
-
|
|
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>
|
|
1303
1334
|
}
|
|
1304
1335
|
```
|
|
1305
1336
|
|