@bymax-one/nest-logger 1.2.5 → 1.2.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 +220 -1
- package/README.md +77 -1
- package/dist/server/index.cjs +158 -39
- package/dist/server/index.d.cts +139 -10
- package/dist/server/index.d.ts +139 -10
- package/dist/server/index.mjs +158 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,223 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.7] - 2026-08-15
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **Serializer output is redacted by name under `redactStrategy: 'paths'` too.** Carrying an
|
|
19
|
+
error's own properties at every depth of the `cause` chain (below) opened a hole in that legacy
|
|
20
|
+
mode: the name walk is a pass-through there, so redaction fell entirely to `fast-redact`, and
|
|
21
|
+
`DEFAULT_REDACT_PATHS` reaches `*.*.*.*.password` — four wildcard levels. A serialized cause chain
|
|
22
|
+
goes deeper. Measured on the built artifact with a password on the deepest link of
|
|
23
|
+
`err.errors[0].cause.cause`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "cause": { "name": "Error", "message": "deep", "password": "LEAK-deep" } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Confirmed absent from `1.2.6`, where the field was not carried at all — so this was a regression
|
|
30
|
+
introduced inside this release rather than an inherited one, and it never reached a published
|
|
31
|
+
artifact.
|
|
32
|
+
|
|
33
|
+
`redactStrategy` is the consumer's choice about **their own payload**; what a serializer
|
|
34
|
+
synthesizes is this library's output, and the name walk — which has no depth ceiling — now covers
|
|
35
|
+
it either way. The caller's record is untouched by this: `'paths'` still means exactly what it
|
|
36
|
+
documented, four-level ceiling included, and the e2e case that pins that ceiling is what caught
|
|
37
|
+
the first attempt at this fix collapsing both hooks onto one redactor.
|
|
38
|
+
|
|
39
|
+
Reported by Copilot on the pull request that introduced the deeper copy.
|
|
40
|
+
|
|
41
|
+
- **An own `__proto__` key on an error is now stored as data instead of being applied as a
|
|
42
|
+
prototype.** The own-property copy assigned each key, and `__proto__` is an accessor inherited
|
|
43
|
+
from `Object.prototype` — so the assignment walked the chain, invoked that setter, and replaced
|
|
44
|
+
the sanitized node's prototype. An error-like object arriving from JSON (a worker boundary, a
|
|
45
|
+
queue, an HTTP body) carries `__proto__` as an ordinary own enumerable key, and `Object.entries`
|
|
46
|
+
hands it over like any other.
|
|
47
|
+
|
|
48
|
+
Measured rather than assumed. `Object.prototype` itself is never touched, so this is prototype
|
|
49
|
+
injection into a single log node rather than global pollution, and with a JSON-sourced payload the
|
|
50
|
+
emitted line was unchanged — `JSON.stringify` ignores inherited fields. The live hazard is the
|
|
51
|
+
node: given `{"__proto__": null}` it loses `hasOwnProperty` and `toString`, which then **throw**
|
|
52
|
+
for anything downstream that touches them, inside the one path whose contract is that logging
|
|
53
|
+
never crashes the application.
|
|
54
|
+
|
|
55
|
+
The copy now defines an own data property, which makes the key inert and keeps its value — an
|
|
56
|
+
error that genuinely carries a `__proto__` field gets it logged as the data it is. Reported by
|
|
57
|
+
Copilot on the pull request that introduced the deeper copy.
|
|
58
|
+
|
|
59
|
+
### Documentation
|
|
60
|
+
|
|
61
|
+
- **`ignore` needs an escaped dot for `event.name`, and every example here taught the opposite.**
|
|
62
|
+
`pino-pretty` reads a dot as a nested path, and this library emits `event.name` as a literal
|
|
63
|
+
top-level key — so `ignore: '…,event.name'` looks for an `event` object with a `name` inside,
|
|
64
|
+
finds neither, and hides nothing without a word of complaint. Measured on one entry with only the
|
|
65
|
+
option changing:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
69
|
+
ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Found by a consumer who copied the `1.2.6` example verbatim, configured it, and saw the field
|
|
73
|
+
still on screen. The escaped form is now in the `PrettyViewOptions` JSDoc — where an editor's
|
|
74
|
+
autocomplete shows it, which is where someone actually is when writing the option — in the README,
|
|
75
|
+
and in the `1.2.6` example itself, which is corrected rather than left to keep teaching it.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **An error's own properties now survive at every depth of the `cause` chain, not just at the top.**
|
|
80
|
+
`code`, `statusCode` and any domain field an application attaches were copied onto the error handed
|
|
81
|
+
to the log call and **dropped the moment that same error was wrapped as someone else's `cause`** —
|
|
82
|
+
which is what a `catch` in `main.ts` does routinely. Measured on the published `1.2.6`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"type": "Error",
|
|
87
|
+
"message": "bootstrap failed",
|
|
88
|
+
"code": "EBOOT",
|
|
89
|
+
"cause": { "name": "Error", "message": "config invalid" }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The inner error carried `code: "BYMAX_CONFIG_VALIDATION"` and an `issues` array holding one entry
|
|
94
|
+
per invalid variable. Both vanished. The reporting consumer framed the severity exactly right:
|
|
95
|
+
because `message` survives and their message is the full aggregated report, **the human stayed
|
|
96
|
+
served while the machine went blind** — the text an operator reads was intact, and the fields an
|
|
97
|
+
alert or a dashboard keys on were gone.
|
|
98
|
+
|
|
99
|
+
Nothing justified the asymmetry. The reason own properties are "part of the contract" at the top —
|
|
100
|
+
`pino.stdSerializers.err` copied them, so dropping them is a silent compatibility loss — is the
|
|
101
|
+
same reason one level down, and a wrapped error is the common case rather than the exotic one.
|
|
102
|
+
They still never shadow a field the serializer derives, and they still pass through the same
|
|
103
|
+
name-based redaction and size bound as any other serializer output.
|
|
104
|
+
|
|
105
|
+
Found by following up an asymmetry noticed while answering a consumer's question about how to pass
|
|
106
|
+
an error through the NestJS bridge — not by a bug report.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- **The own-property copy lives in one place instead of two.** The `err` serializer read the raw
|
|
111
|
+
thrown value and walked its properties itself, duplicating what the sanitizer now does at every
|
|
112
|
+
node; it copies from the sanitized node instead. Same fields, one walk — and no second copy to
|
|
113
|
+
drift away from the first.
|
|
114
|
+
|
|
115
|
+
## [1.2.6] - 2026-08-15
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
|
|
119
|
+
- **`PrettyDevDestination` takes a `view`, so the terminal rendering is the consumer's choice.**
|
|
120
|
+
Requested with the measurement that makes the case — one real entry rendered three ways through
|
|
121
|
+
`pino-pretty`, where only the options differ:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
A) the built-in view 7 lines for one entry
|
|
125
|
+
B) singleLine + an extended ignore list 1 line, fields kept
|
|
126
|
+
C) hideObject + messageFormat '[{context}] {msg}' 1 line, message only
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
All of it was reachable in `pino-pretty` and none of it was reachable through this library, which
|
|
130
|
+
hard-coded five options and read only `minLevel` from its constructor.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
new PrettyDevDestination({
|
|
134
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`PrettyViewOptions` covers `singleLine`, `ignore`, `hideObject`, `messageFormat`, `translateTime`
|
|
139
|
+
and `colorize`. **Additive** — every field defaults to what it rendered before, so
|
|
140
|
+
`new PrettyDevDestination()` is unchanged.
|
|
141
|
+
|
|
142
|
+
**`destination` is deliberately not exposed.** The library owns where entries go; a redirected
|
|
143
|
+
stream would route around the multistream fan-out and the last-resort rescue. It is absent from
|
|
144
|
+
the type AND applied after the consumer's options are spread, so an untyped JavaScript caller
|
|
145
|
+
cannot smuggle one in either — omitting it from a type is a suggestion, ordering the spread is the
|
|
146
|
+
guarantee.
|
|
147
|
+
|
|
148
|
+
The options bag is this library's **own** interface rather than `pino-pretty`'s `PrettyOptions`,
|
|
149
|
+
and that is load-bearing rather than stylistic: a type import from the peer emits a hard
|
|
150
|
+
`import { PrettyOptions } from 'pino-pretty'` on line 4 of the published `.d.ts`, so every consumer
|
|
151
|
+
who type-checks without the optional peer installed would hit an unresolved module. That would make
|
|
152
|
+
an optional peer effectively required. Measured on the built artifact.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
|
|
156
|
+
- **Entries emitted before the pretty transform exists are now rendered, not dumped raw.** A real
|
|
157
|
+
boot put **43 raw NDJSON lines on screen before the first rendered one** — every entry NestJS emits
|
|
158
|
+
while instantiating providers, because the transform cannot be built until `onInit` (loading the
|
|
159
|
+
optional peer is async). Nothing was ever lost, but a developer who had just enabled pretty output
|
|
160
|
+
saw a screen of JSON and concluded it had not worked. That is what was reported.
|
|
161
|
+
|
|
162
|
+
Those entries are now held and flushed **through** the transform once it exists, in arrival order.
|
|
163
|
+
Nothing is lost on any path, which is the property the raw passthrough had and the buffer had to
|
|
164
|
+
keep:
|
|
165
|
+
|
|
166
|
+
- **init fails** → the held entries are drained raw, because the renderer they were waiting for is
|
|
167
|
+
never coming;
|
|
168
|
+
- **a bound is reached** (1000 entries or 4 MiB, whichever trips first — nothing guarantees
|
|
169
|
+
`onInit` ever runs) → the buffer drains raw _before_ the entry that tripped it, so output stays
|
|
170
|
+
in order rather than replaying old entries after newer ones;
|
|
171
|
+
- **shutdown before init** → drained raw rather than dying with the process.
|
|
172
|
+
|
|
173
|
+
The byte ceiling is there because the entry count alone does not bound memory: a payload has no
|
|
174
|
+
whole-record size limit (`maxEntrySizeBytes` bounds what a serializer emits for one field, not the
|
|
175
|
+
entry), so 1000 held entries carrying large metadata could retain far more than the count suggests
|
|
176
|
+
— where the raw passthrough this buffer replaced retained nothing at all.
|
|
177
|
+
|
|
178
|
+
- **A closed pipe can no longer crash the host, which the `try/catch` around
|
|
179
|
+
`process.stdout.write` never actually prevented.** Every fallback path in this library — the
|
|
180
|
+
last-resort NDJSON rescue, the pre-init drain, the destination failure reports — wrapped its raw
|
|
181
|
+
write in a `try/catch` and documented that as EPIPE protection. It is not, and this was measured
|
|
182
|
+
rather than argued:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
stdout error listeners at start: 0
|
|
186
|
+
sync result: no-throw ← the try/catch caught nothing
|
|
187
|
+
UNCAUGHT:EPIPE ← arrived later, as an uncaught exception
|
|
188
|
+
child exit code: 42 ← the process died
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A closed pipe (`node app | head`) reports EPIPE **asynchronously**, through the stream's `'error'`
|
|
192
|
+
event, after `write()` has already returned. Node attaches no default handler, so the emit becomes
|
|
193
|
+
an uncaught exception. The guarantee was asserted in prose and absent from the code, on exactly the
|
|
194
|
+
paths whose purpose is to survive a broken sink.
|
|
195
|
+
|
|
196
|
+
Every library-owned write to a process stream now goes through one `safe-stdio` helper that
|
|
197
|
+
installs a swallow-EPIPE handler on first use, covering the asynchronous half; the `try/catch`
|
|
198
|
+
remains for the synchronous half, since either alone leaves a way to die. The handler is found by
|
|
199
|
+
identity rather than remembered, so anything that strips listeners from a process stream cannot
|
|
200
|
+
silently disable the protection permanently.
|
|
201
|
+
|
|
202
|
+
**This includes `DefaultStdoutDestination`, and that is the half that actually mattered.** The
|
|
203
|
+
first version of the fix guarded only the fallback paths — the rescue, the drain, the failure
|
|
204
|
+
reports — none of which a healthy application ever takes. The default sink, which is what a
|
|
205
|
+
consumer gets when they configure nothing, still called `process.stdout.write` directly, so the
|
|
206
|
+
ordinary install was exactly as crashable as before. Caught in review and then reproduced against
|
|
207
|
+
the built artifact: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42. Re-measured
|
|
208
|
+
after the fix: survived, exit 0.
|
|
209
|
+
|
|
210
|
+
`PrettyDevDestination` is deliberately **not** guarded by this library. `pino-pretty` receives
|
|
211
|
+
`process.stdout` and attaches two `'error'` listeners of its own; a child piped to a closed reader
|
|
212
|
+
survived and exited 0 with nothing from us on the stream, so a guard there would be code whose
|
|
213
|
+
need is disproven. A test pins that, and goes red if `pino-pretty` ever stops attaching them.
|
|
214
|
+
|
|
215
|
+
One consequence worth stating rather than burying: after the first log line, `process.stdout` has a
|
|
216
|
+
swallowing `'error'` listener that belongs to this library, so an EPIPE raised by **your** writes
|
|
217
|
+
to stdout also stops being an uncaught exception. That is the trade — the alternative is the crash
|
|
218
|
+
above — and your own `'error'` listener is never removed or replaced.
|
|
219
|
+
|
|
220
|
+
Reported by Copilot against the new pre-init buffer, then a second time against the incomplete
|
|
221
|
+
fix; the same ineffective pattern was already shipped in `1.2.3` and `1.2.5`, so this corrects
|
|
222
|
+
those paths too.
|
|
223
|
+
|
|
224
|
+
- **A write after a failed init is verifiably dropped, not merely silent.** Adding the buffer made
|
|
225
|
+
"dropped" and "held" observationally identical — the existing case asserted only that nothing
|
|
226
|
+
reached stdout, and buffering produces the same silence. A **cold** mutation run caught it
|
|
227
|
+
(the incremental runs had been reporting 100% while the cold run read 99.62%, with all three
|
|
228
|
+
survivors in this file). The case now shuts down after the failed init, which drains anything
|
|
229
|
+
still held: a merely-buffered entry surfaces there, a dropped one does not.
|
|
230
|
+
|
|
14
231
|
## [1.2.5] - 2026-08-14
|
|
15
232
|
|
|
16
233
|
### Fixed
|
|
@@ -1014,7 +1231,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1014
1231
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1015
1232
|
`release.yml`, Dependabot, and issue templates
|
|
1016
1233
|
|
|
1017
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
1234
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...HEAD
|
|
1235
|
+
[1.2.7]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...v1.2.7
|
|
1236
|
+
[1.2.6]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...v1.2.6
|
|
1018
1237
|
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
|
1019
1238
|
[1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
|
|
1020
1239
|
[1.2.3]: https://github.com/bymaxone/nest-logger/compare/v1.2.2...v1.2.3
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ pnpm add @bymax-one/nest-logger
|
|
|
60
60
|
- ✅ **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
|
|
61
61
|
- ✅ **`MODULE_ACTION_RESULT` Log Keys** — a naming convention enforced by an exported regex for CI validation
|
|
62
62
|
- ✅ **NestJS `LoggerService` Bridge** — drop-in replacement; all NestJS internal logs flow through Pino
|
|
63
|
-
- ✅ **Pretty-Print in Dev** — opt-in `PrettyDevDestination` for readable local output (requires optional `pino-pretty`)
|
|
63
|
+
- ✅ **Pretty-Print in Dev** — opt-in `PrettyDevDestination` with a configurable `view` (single-line, hidden fields, message-only) for readable local output (requires optional `pino-pretty`)
|
|
64
64
|
- ✅ **Field Size Guard** — a serialized field over the ceiling (default 64 KB) is replaced by a compact truncation envelope instead of flooding the sink
|
|
65
65
|
|
|
66
66
|
### 🛡️ Security & Privacy
|
|
@@ -534,6 +534,32 @@ 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 a `catch` in `main.ts` does routinely. They never shadow a
|
|
560
|
+
field the serializer derives, and they pass through the same name-based redaction and size bound as
|
|
561
|
+
any other serializer output.
|
|
562
|
+
|
|
537
563
|
An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
|
|
538
564
|
Pino's standard serializer did.
|
|
539
565
|
|
|
@@ -753,6 +779,48 @@ The consequence worth knowing: a sink you supply may be the **only** one the app
|
|
|
753
779
|
|
|
754
780
|
So the common accident — adding `new PrettyDevDestination()` without installing the optional `pino-pretty` — costs you colours and a line on stderr telling you why, never your logs.
|
|
755
781
|
|
|
782
|
+
### Choosing how the dev terminal renders
|
|
783
|
+
|
|
784
|
+
`PrettyDevDestination` takes a `view`. Every field defaults to what it rendered before, so `new PrettyDevDestination()` is unchanged — the options exist because the default view is deliberately verbose, and one entry can be seven lines.
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
// One line per entry, with the fields your project repeats on every line hidden.
|
|
788
|
+
new PrettyDevDestination({
|
|
789
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
// Message only, with the context pulled back into the line.
|
|
793
|
+
new PrettyDevDestination({
|
|
794
|
+
view: { hideObject: true, messageFormat: '[{context}] {msg}' }
|
|
795
|
+
})
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
| Field | Default | Notes |
|
|
799
|
+
| --------------- | ------------------------ | ---------------------------------------------------------------------------- |
|
|
800
|
+
| `singleLine` | `false` | The single biggest change to how a terminal reads |
|
|
801
|
+
| `ignore` | `'pid,hostname,service'` | Display-only — what a real sink receives is untouched |
|
|
802
|
+
| `hideObject` | `false` | Hides the record entirely; see the caveat below |
|
|
803
|
+
| `messageFormat` | — | e.g. `'[{context}] {msg}'`; how to keep one field visible under `hideObject` |
|
|
804
|
+
| `translateTime` | `'SYS:HH:MM:ss.l'` | Or `false` for the raw timestamp |
|
|
805
|
+
| `colorize` | `true` | ANSI colour |
|
|
806
|
+
|
|
807
|
+
> **`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.
|
|
808
|
+
|
|
809
|
+
> **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
|
+
>
|
|
811
|
+
> ```
|
|
812
|
+
> ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
813
|
+
> ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
814
|
+
> ```
|
|
815
|
+
>
|
|
816
|
+
> 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.
|
|
817
|
+
|
|
818
|
+
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
|
+
|
|
820
|
+
> **`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
|
+
|
|
822
|
+
**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.
|
|
823
|
+
|
|
756
824
|
### Postgres destination (Prisma)
|
|
757
825
|
|
|
758
826
|
```typescript
|
|
@@ -975,6 +1043,14 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
|
|
|
975
1043
|
|
|
976
1044
|
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.
|
|
977
1045
|
|
|
1046
|
+
### A closed pipe does not kill the process
|
|
1047
|
+
|
|
1048
|
+
`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.
|
|
1049
|
+
|
|
1050
|
+
Every write this library makes to a process stream — the default stdout sink, the last-resort rescue, the pre-init drain, the `stderr` failure reports — installs a swallowing `'error'` listener on first use. **The side effect is worth knowing:** that listener is on the shared `process.stdout`, so after the first log line an EPIPE raised by _your_ writes stops being an uncaught exception too. The alternative is the crash above. Your own `'error'` listener is never removed or replaced — this only ever adds one.
|
|
1051
|
+
|
|
1052
|
+
If you write to `process.stdout` from a custom `ILogDestination`, do the same in your own sink; the [destinations guide](./docs/guidelines/DESTINATIONS-IMPLEMENTATION-GUIDELINES.md) has the exact pattern.
|
|
1053
|
+
|
|
978
1054
|
### Bounded field size
|
|
979
1055
|
|
|
980
1056
|
Every serializer — the default `err` one and any you supply — is wrapped so a field whose serialized JSON exceeds `maxEntrySizeBytes` (64 KB by default) is replaced by a compact envelope: `_truncated`, `_logKey: 'LOGGER_ENTRY_TRUNCATED'`, `_originalSize`, and a 200-character `_preview`. An accidentally logged webhook payload costs a bounded number of bytes and leaves a record that truncation happened, instead of flooding the sink.
|