@bymax-one/nest-logger 1.2.4 → 1.2.6
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 +211 -1
- package/README.md +66 -10
- package/dist/server/index.cjs +204 -87
- package/dist/server/index.d.cts +124 -10
- package/dist/server/index.d.ts +124 -10
- package/dist/server/index.mjs +204 -87
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,214 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.6] - 2026-08-15
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`PrettyDevDestination` takes a `view`, so the terminal rendering is the consumer's choice.**
|
|
19
|
+
Requested with the measurement that makes the case — one real entry rendered three ways through
|
|
20
|
+
`pino-pretty`, where only the options differ:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
A) the built-in view 7 lines for one entry
|
|
24
|
+
B) singleLine + an extended ignore list 1 line, fields kept
|
|
25
|
+
C) hideObject + messageFormat '[{context}] {msg}' 1 line, message only
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
All of it was reachable in `pino-pretty` and none of it was reachable through this library, which
|
|
29
|
+
hard-coded five options and read only `minLevel` from its constructor.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
new PrettyDevDestination({
|
|
33
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`PrettyViewOptions` covers `singleLine`, `ignore`, `hideObject`, `messageFormat`, `translateTime`
|
|
38
|
+
and `colorize`. **Additive** — every field defaults to what it rendered before, so
|
|
39
|
+
`new PrettyDevDestination()` is unchanged.
|
|
40
|
+
|
|
41
|
+
**`destination` is deliberately not exposed.** The library owns where entries go; a redirected
|
|
42
|
+
stream would route around the multistream fan-out and the last-resort rescue. It is absent from
|
|
43
|
+
the type AND applied after the consumer's options are spread, so an untyped JavaScript caller
|
|
44
|
+
cannot smuggle one in either — omitting it from a type is a suggestion, ordering the spread is the
|
|
45
|
+
guarantee.
|
|
46
|
+
|
|
47
|
+
The options bag is this library's **own** interface rather than `pino-pretty`'s `PrettyOptions`,
|
|
48
|
+
and that is load-bearing rather than stylistic: a type import from the peer emits a hard
|
|
49
|
+
`import { PrettyOptions } from 'pino-pretty'` on line 4 of the published `.d.ts`, so every consumer
|
|
50
|
+
who type-checks without the optional peer installed would hit an unresolved module. That would make
|
|
51
|
+
an optional peer effectively required. Measured on the built artifact.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Entries emitted before the pretty transform exists are now rendered, not dumped raw.** A real
|
|
56
|
+
boot put **43 raw NDJSON lines on screen before the first rendered one** — every entry NestJS emits
|
|
57
|
+
while instantiating providers, because the transform cannot be built until `onInit` (loading the
|
|
58
|
+
optional peer is async). Nothing was ever lost, but a developer who had just enabled pretty output
|
|
59
|
+
saw a screen of JSON and concluded it had not worked. That is what was reported.
|
|
60
|
+
|
|
61
|
+
Those entries are now held and flushed **through** the transform once it exists, in arrival order.
|
|
62
|
+
Nothing is lost on any path, which is the property the raw passthrough had and the buffer had to
|
|
63
|
+
keep:
|
|
64
|
+
|
|
65
|
+
- **init fails** → the held entries are drained raw, because the renderer they were waiting for is
|
|
66
|
+
never coming;
|
|
67
|
+
- **a bound is reached** (1000 entries or 4 MiB, whichever trips first — nothing guarantees
|
|
68
|
+
`onInit` ever runs) → the buffer drains raw _before_ the entry that tripped it, so output stays
|
|
69
|
+
in order rather than replaying old entries after newer ones;
|
|
70
|
+
- **shutdown before init** → drained raw rather than dying with the process.
|
|
71
|
+
|
|
72
|
+
The byte ceiling is there because the entry count alone does not bound memory: a payload has no
|
|
73
|
+
whole-record size limit (`maxEntrySizeBytes` bounds what a serializer emits for one field, not the
|
|
74
|
+
entry), so 1000 held entries carrying large metadata could retain far more than the count suggests
|
|
75
|
+
— where the raw passthrough this buffer replaced retained nothing at all.
|
|
76
|
+
|
|
77
|
+
- **A closed pipe can no longer crash the host, which the `try/catch` around
|
|
78
|
+
`process.stdout.write` never actually prevented.** Every fallback path in this library — the
|
|
79
|
+
last-resort NDJSON rescue, the pre-init drain, the destination failure reports — wrapped its raw
|
|
80
|
+
write in a `try/catch` and documented that as EPIPE protection. It is not, and this was measured
|
|
81
|
+
rather than argued:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
stdout error listeners at start: 0
|
|
85
|
+
sync result: no-throw ← the try/catch caught nothing
|
|
86
|
+
UNCAUGHT:EPIPE ← arrived later, as an uncaught exception
|
|
87
|
+
child exit code: 42 ← the process died
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A closed pipe (`node app | head`) reports EPIPE **asynchronously**, through the stream's `'error'`
|
|
91
|
+
event, after `write()` has already returned. Node attaches no default handler, so the emit becomes
|
|
92
|
+
an uncaught exception. The guarantee was asserted in prose and absent from the code, on exactly the
|
|
93
|
+
paths whose purpose is to survive a broken sink.
|
|
94
|
+
|
|
95
|
+
Every library-owned write to a process stream now goes through one `safe-stdio` helper that
|
|
96
|
+
installs a swallow-EPIPE handler on first use, covering the asynchronous half; the `try/catch`
|
|
97
|
+
remains for the synchronous half, since either alone leaves a way to die. The handler is found by
|
|
98
|
+
identity rather than remembered, so anything that strips listeners from a process stream cannot
|
|
99
|
+
silently disable the protection permanently.
|
|
100
|
+
|
|
101
|
+
**This includes `DefaultStdoutDestination`, and that is the half that actually mattered.** The
|
|
102
|
+
first version of the fix guarded only the fallback paths — the rescue, the drain, the failure
|
|
103
|
+
reports — none of which a healthy application ever takes. The default sink, which is what a
|
|
104
|
+
consumer gets when they configure nothing, still called `process.stdout.write` directly, so the
|
|
105
|
+
ordinary install was exactly as crashable as before. Caught in review and then reproduced against
|
|
106
|
+
the built artifact: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42. Re-measured
|
|
107
|
+
after the fix: survived, exit 0.
|
|
108
|
+
|
|
109
|
+
`PrettyDevDestination` is deliberately **not** guarded by this library. `pino-pretty` receives
|
|
110
|
+
`process.stdout` and attaches two `'error'` listeners of its own; a child piped to a closed reader
|
|
111
|
+
survived and exited 0 with nothing from us on the stream, so a guard there would be code whose
|
|
112
|
+
need is disproven. A test pins that, and goes red if `pino-pretty` ever stops attaching them.
|
|
113
|
+
|
|
114
|
+
One consequence worth stating rather than burying: after the first log line, `process.stdout` has a
|
|
115
|
+
swallowing `'error'` listener that belongs to this library, so an EPIPE raised by **your** writes
|
|
116
|
+
to stdout also stops being an uncaught exception. That is the trade — the alternative is the crash
|
|
117
|
+
above — and your own `'error'` listener is never removed or replaced.
|
|
118
|
+
|
|
119
|
+
Reported by Copilot against the new pre-init buffer, then a second time against the incomplete
|
|
120
|
+
fix; the same ineffective pattern was already shipped in `1.2.3` and `1.2.5`, so this corrects
|
|
121
|
+
those paths too.
|
|
122
|
+
|
|
123
|
+
- **A write after a failed init is verifiably dropped, not merely silent.** Adding the buffer made
|
|
124
|
+
"dropped" and "held" observationally identical — the existing case asserted only that nothing
|
|
125
|
+
reached stdout, and buffering produces the same silence. A **cold** mutation run caught it
|
|
126
|
+
(the incremental runs had been reporting 100% while the cold run read 99.62%, with all three
|
|
127
|
+
survivors in this file). The case now shuts down after the failed init, which drains anything
|
|
128
|
+
still held: a merely-buffered entry surfaces there, a dropped one does not.
|
|
129
|
+
|
|
130
|
+
## [1.2.5] - 2026-08-14
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
|
|
134
|
+
- **An `Error` handed to the NestJS logger methods is no longer silently dropped.** Reported from a
|
|
135
|
+
real run rather than a unit test. `@bymax-one/nest-auth` logs the cause the way every caller does,
|
|
136
|
+
an SMTP `STARTTLS 502` failed, and this is what came out:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// the call site, nest-auth
|
|
140
|
+
this.logger.error(`delivery failed for "${subject}"`, error)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"level": "error",
|
|
146
|
+
"context": "DefaultAuthEmailProvider",
|
|
147
|
+
"msg": "delivery failed for \"Verify your email address\""
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
No `err`, no `stack`, no warning. The cause existed the whole time and was discarded. The dispatch
|
|
152
|
+
is fire-and-forget, so the HTTP response was `204` while nothing had been delivered: **the log was
|
|
153
|
+
the only surface where that failure could appear, and it appeared without its reason.** In CI you
|
|
154
|
+
can raise the level and re-run; in production nobody runs at `debug`, so the reason was never
|
|
155
|
+
written at all.
|
|
156
|
+
|
|
157
|
+
Neither side was obviously wrong, which is what made it survive. NestJS types the slot as
|
|
158
|
+
`error(message, stack?: string, context?: string)`, so this library read a string and discarded
|
|
159
|
+
anything else — implementing the contract faithfully. Passing the `Error` itself is what callers
|
|
160
|
+
actually do, because every other logger in the ecosystem accepts it.
|
|
161
|
+
|
|
162
|
+
**Measured before the fix: eleven of the twelve level/position combinations lost the `Error`.**
|
|
163
|
+
Only `error(err)` survived — every other level (`log`, `warn`, `debug`, `verbose`, `fatal`) routes
|
|
164
|
+
through a shared helper that handled no `Error` in either position. `fatal` was the worst of them,
|
|
165
|
+
being the level a caller reaches for when the process is about to die.
|
|
166
|
+
|
|
167
|
+
An `Error` anywhere in the variadic tail is now attached as `err`, the same field the leading-`Error`
|
|
168
|
+
branch already wrote, at every level. A trailing _string_ is still the `stack`, and `stack` is never
|
|
169
|
+
populated from an `Error` — the `err` serializer derives it, so writing both would emit the same
|
|
170
|
+
trace under two names.
|
|
171
|
+
|
|
172
|
+
**Detection is structural, not `instanceof`.** The first version of this fix used
|
|
173
|
+
`instanceof Error`, which is realm-local — so it would still have dropped a genuine `Error` built in
|
|
174
|
+
a `vm` context, and one already normalized into a plain `{ name, message, stack }`, which is what
|
|
175
|
+
`HttpExceptionFilter` produces and what any error crossing a process or worker boundary becomes.
|
|
176
|
+
That is the population most likely to be carrying the reason something failed, and this repository
|
|
177
|
+
had already recorded the same lesson in `sanitizeError` ("too narrow, and the gap was a live
|
|
178
|
+
defect"). The bridge now shares `isErrorLike` with `sanitizeError`, so the two cannot disagree about
|
|
179
|
+
what an error is. The bar stays deliberately narrow — string `name` AND string `message` — so an
|
|
180
|
+
ordinary metadata object is not mistaken for a cause.
|
|
181
|
+
|
|
182
|
+
**Two output changes to expect, since "additive" would overstate it.** Nothing is removed or
|
|
183
|
+
renamed, and a caller passing a string stack is untouched. But a caller who was passing an `Error`
|
|
184
|
+
necessarily sees a difference, because that is the defect:
|
|
185
|
+
|
|
186
|
+
- entries that silently dropped a cause now carry an `err` object — a **new field** on lines that
|
|
187
|
+
previously had none, which a strict log-pipeline schema will see;
|
|
188
|
+
- `warn(new Error('x'))` and the other non-`error` levels now emit `msg: "x"` instead of
|
|
189
|
+
`msg: "Error: x"`, matching what `error(new Error('x'))` already produced. A query grepping for
|
|
190
|
+
the `"Error: "` prefix on those levels will stop matching.
|
|
191
|
+
|
|
192
|
+
Both are the point of the change rather than side effects, but neither is invisible.
|
|
193
|
+
|
|
194
|
+
Scope, as measured by the reporting consumer across the ten `@bymax-one/*` packages that backend
|
|
195
|
+
installs at its pinned versions: **37 affected call sites, all of them in `nest-auth`**, none
|
|
196
|
+
elsewhere. Two are security-relevant — a breach check that fails open and logs that it admitted a
|
|
197
|
+
password it could not verify, and the `onRefreshTokenReuseDetected` hook, the tripwire for a stolen
|
|
198
|
+
token. Both said something went wrong and threw away what.
|
|
199
|
+
|
|
200
|
+
### Documentation
|
|
201
|
+
|
|
202
|
+
- **The `pnpm prune --prod` note named the wrong mechanism.** It described the peer surviving as a
|
|
203
|
+
_residue_ of an incomplete prune. A second consumer whose Dockerfile never prunes — a clean
|
|
204
|
+
`pnpm install --prod --frozen-lockfile` in a fresh runtime stage — measured the peer present anyway.
|
|
205
|
+
In both images the peer was recorded in the lockfile and pnpm placed it in the store for a
|
|
206
|
+
production-only install. The distinction matters because "we do a clean prod install, not a prune"
|
|
207
|
+
reads like an exemption and is not one; it is what that consumer expected before measuring their
|
|
208
|
+
own image.
|
|
209
|
+
|
|
210
|
+
The note is now phrased as **two measurements rather than a law of pnpm** — whether a prod install
|
|
211
|
+
always carries an optional peer, however the lockfile was produced, has not been tested here. The
|
|
212
|
+
first version of this note over-generalized from the evidence, which is the same class of error as
|
|
213
|
+
the two corrections it documents.
|
|
214
|
+
|
|
215
|
+
The note now also says what is different **elsewhere**: under npm's or yarn's flat `node_modules` a
|
|
216
|
+
pruned devDependency really is gone, so the warning must not be read as universal. And it records
|
|
217
|
+
the probe that nearly produced the opposite conclusion — `require.resolve` from the library's
|
|
218
|
+
app-visible path returns `MODULE_NOT_FOUND` while the lazy import still succeeds, because the
|
|
219
|
+
library resolves from its real `.pnpm/` path. Verify by behaviour, not from a path the library
|
|
220
|
+
never uses.
|
|
221
|
+
|
|
14
222
|
## [1.2.4] - 2026-08-14
|
|
15
223
|
|
|
16
224
|
### Documentation
|
|
@@ -922,7 +1130,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
922
1130
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
923
1131
|
`release.yml`, Dependabot, and issue templates
|
|
924
1132
|
|
|
925
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
1133
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...HEAD
|
|
1134
|
+
[1.2.6]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...v1.2.6
|
|
1135
|
+
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
|
926
1136
|
[1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
|
|
927
1137
|
[1.2.3]: https://github.com/bymaxone/nest-logger/compare/v1.2.2...v1.2.3
|
|
928
1138
|
[1.2.2]: https://github.com/bymaxone/nest-logger/compare/v1.2.1...v1.2.2
|
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
|
|
@@ -149,16 +149,31 @@ pnpm add @opentelemetry/api @opentelemetry/sdk-node
|
|
|
149
149
|
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
> ⚠️
|
|
153
|
-
>
|
|
154
|
-
>
|
|
155
|
-
>
|
|
156
|
-
>
|
|
152
|
+
> ⚠️ **Under pnpm, `-D` may not make it absent in production — measure, do not assume.** Two
|
|
153
|
+
> consumers who had assumed otherwise measured the peer **present** in their real production images,
|
|
154
|
+
> by two different build routes: `pnpm prune --prod` keeps the store entry, and a clean
|
|
155
|
+
> `pnpm install --prod --frozen-lockfile` in a fresh stage installs it too. In both, the peer was
|
|
156
|
+
> recorded in the lockfile. `PrettyDevDestination` imports it lazily, relative to the library's own
|
|
157
|
+
> directory under `.pnpm/`, where it is a sibling — so it resolves.
|
|
157
158
|
>
|
|
158
|
-
>
|
|
159
|
-
>
|
|
160
|
-
>
|
|
161
|
-
>
|
|
159
|
+
> That is two measurements, not a law of pnpm: whether a production install always carries an optional
|
|
160
|
+
> peer, however the lockfile was produced, has not been tested here. The advice below does not depend
|
|
161
|
+
> on it.
|
|
162
|
+
>
|
|
163
|
+
> The clean-install case is worth stating separately, because "we do a fresh prod install, not a
|
|
164
|
+
> prune" reads like an exemption and is not one. It is what one of those consumers concluded before
|
|
165
|
+
> measuring their own image.
|
|
166
|
+
>
|
|
167
|
+
> **On other package managers this is different**, and the difference cuts the other way: under npm's
|
|
168
|
+
> or yarn's flat `node_modules`, a pruned devDependency really is gone. If you install with those,
|
|
169
|
+
> do not read this warning as universal.
|
|
170
|
+
>
|
|
171
|
+
> Either way, the rule that survives both layouts: **"it is a devDependency, therefore this path
|
|
172
|
+
> cannot run in production" is not a safe premise, because the premise is about someone else's
|
|
173
|
+
> install command.** A `PrettyDevDestination` left registered in production will not crash and will
|
|
174
|
+
> not warn — it renders ANSI colour into a log pipeline that has silently failed to parse every line
|
|
175
|
+
> since the deploy, which is indistinguishable from a service that went quiet. Gate it on an explicit
|
|
176
|
+
> configuration value you can see, not on the packaging.
|
|
162
177
|
|
|
163
178
|
> [!NOTE]
|
|
164
179
|
> `@opentelemetry/api` is resolved lazily when the Pino instance is built. When it is absent the trace mixin silently steps aside — the logger never fails to start, and never warns, over an optional peer.
|
|
@@ -738,6 +753,39 @@ The consequence worth knowing: a sink you supply may be the **only** one the app
|
|
|
738
753
|
|
|
739
754
|
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.
|
|
740
755
|
|
|
756
|
+
### Choosing how the dev terminal renders
|
|
757
|
+
|
|
758
|
+
`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.
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
// One line per entry, with the fields your project repeats on every line hidden.
|
|
762
|
+
new PrettyDevDestination({
|
|
763
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
// Message only, with the context pulled back into the line.
|
|
767
|
+
new PrettyDevDestination({
|
|
768
|
+
view: { hideObject: true, messageFormat: '[{context}] {msg}' }
|
|
769
|
+
})
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
| Field | Default | Notes |
|
|
773
|
+
| --------------- | ------------------------ | ---------------------------------------------------------------------------- |
|
|
774
|
+
| `singleLine` | `false` | The single biggest change to how a terminal reads |
|
|
775
|
+
| `ignore` | `'pid,hostname,service'` | Display-only — what a real sink receives is untouched |
|
|
776
|
+
| `hideObject` | `false` | Hides the record entirely; see the caveat below |
|
|
777
|
+
| `messageFormat` | — | e.g. `'[{context}] {msg}'`; how to keep one field visible under `hideObject` |
|
|
778
|
+
| `translateTime` | `'SYS:HH:MM:ss.l'` | Or `false` for the raw timestamp |
|
|
779
|
+
| `colorize` | `true` | ANSI colour |
|
|
780
|
+
|
|
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.
|
|
782
|
+
|
|
783
|
+
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
|
+
|
|
785
|
+
> **`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
|
+
|
|
787
|
+
**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.
|
|
788
|
+
|
|
741
789
|
### Postgres destination (Prisma)
|
|
742
790
|
|
|
743
791
|
```typescript
|
|
@@ -960,6 +1008,14 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
|
|
|
960
1008
|
|
|
961
1009
|
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.
|
|
962
1010
|
|
|
1011
|
+
### A closed pipe does not kill the process
|
|
1012
|
+
|
|
1013
|
+
`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.
|
|
1014
|
+
|
|
1015
|
+
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.
|
|
1016
|
+
|
|
1017
|
+
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.
|
|
1018
|
+
|
|
963
1019
|
### Bounded field size
|
|
964
1020
|
|
|
965
1021
|
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.
|