@bymax-one/nest-logger 1.2.5 → 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 +118 -1
- package/README.md +42 -1
- package/dist/server/index.cjs +127 -16
- package/dist/server/index.d.cts +124 -10
- package/dist/server/index.d.ts +124 -10
- package/dist/server/index.mjs +127 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,122 @@ 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
|
+
|
|
14
130
|
## [1.2.5] - 2026-08-14
|
|
15
131
|
|
|
16
132
|
### Fixed
|
|
@@ -1014,7 +1130,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1014
1130
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1015
1131
|
`release.yml`, Dependabot, and issue templates
|
|
1016
1132
|
|
|
1017
|
-
[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
|
|
1018
1135
|
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
|
1019
1136
|
[1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
|
|
1020
1137
|
[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
|
|
@@ -753,6 +753,39 @@ The consequence worth knowing: a sink you supply may be the **only** one the app
|
|
|
753
753
|
|
|
754
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.
|
|
755
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
|
+
|
|
756
789
|
### Postgres destination (Prisma)
|
|
757
790
|
|
|
758
791
|
```typescript
|
|
@@ -975,6 +1008,14 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
|
|
|
975
1008
|
|
|
976
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.
|
|
977
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
|
+
|
|
978
1019
|
### Bounded field size
|
|
979
1020
|
|
|
980
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.
|
package/dist/server/index.cjs
CHANGED
|
@@ -586,6 +586,30 @@ function collectContextLoggerProviders() {
|
|
|
586
586
|
function collectContextLoggerTokens() {
|
|
587
587
|
return Array.from(contextTokens.values());
|
|
588
588
|
}
|
|
589
|
+
|
|
590
|
+
// src/server/utils/safe-stdio.util.ts
|
|
591
|
+
var swallowStreamError = () => void 0;
|
|
592
|
+
function ensureGuarded(stream) {
|
|
593
|
+
if (stream.listeners("error").includes(swallowStreamError)) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
stream.on("error", swallowStreamError);
|
|
597
|
+
}
|
|
598
|
+
function writeSafely(stream, payload) {
|
|
599
|
+
try {
|
|
600
|
+
ensureGuarded(stream);
|
|
601
|
+
stream.write(payload);
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function writeStdoutSafely(payload) {
|
|
606
|
+
writeSafely(process.stdout, payload);
|
|
607
|
+
}
|
|
608
|
+
function writeStderrSafely(line) {
|
|
609
|
+
writeSafely(process.stderr, line);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/server/destinations/default-stdout.destination.ts
|
|
589
613
|
exports.DefaultStdoutDestination = class DefaultStdoutDestination {
|
|
590
614
|
/**
|
|
591
615
|
* @param opts.minLevel — Optional minimum level filter. Entries below this
|
|
@@ -600,9 +624,17 @@ exports.DefaultStdoutDestination = class DefaultStdoutDestination {
|
|
|
600
624
|
}
|
|
601
625
|
/**
|
|
602
626
|
* Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
|
|
627
|
+
*
|
|
628
|
+
* Routed through {@link writeStdoutSafely} rather than calling
|
|
629
|
+
* `process.stdout.write` directly. This is the sink a consumer gets when they
|
|
630
|
+
* configure nothing, so it is the path that decides whether a closed pipe
|
|
631
|
+
* (`node app | head`) kills the application — and unguarded, it did: EPIPE
|
|
632
|
+
* arrives asynchronously through the stream's `'error'` event, Node attaches no
|
|
633
|
+
* default handler, and the emit becomes an uncaught exception. Measured on the
|
|
634
|
+
* built artifact, exit code 42.
|
|
603
635
|
*/
|
|
604
636
|
write(payload) {
|
|
605
|
-
|
|
637
|
+
writeStdoutSafely(payload);
|
|
606
638
|
}
|
|
607
639
|
};
|
|
608
640
|
exports.DefaultStdoutDestination = __decorateClass([
|
|
@@ -1034,7 +1066,7 @@ function reportDestinationFailure(logKey, name, cause, msg) {
|
|
|
1034
1066
|
type: toSingleLineMessage(String(cause.name)),
|
|
1035
1067
|
message: toSingleLineMessage(String(cause.message))
|
|
1036
1068
|
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1037
|
-
|
|
1069
|
+
writeStderrSafely(
|
|
1038
1070
|
JSON.stringify({
|
|
1039
1071
|
level: "error",
|
|
1040
1072
|
logKey,
|
|
@@ -1057,10 +1089,7 @@ function reportWriteFailure(name, cause) {
|
|
|
1057
1089
|
);
|
|
1058
1090
|
}
|
|
1059
1091
|
function rescueToStdout(payload) {
|
|
1060
|
-
|
|
1061
|
-
process.stdout.write(payload);
|
|
1062
|
-
} catch {
|
|
1063
|
-
}
|
|
1092
|
+
writeStdoutSafely(payload);
|
|
1064
1093
|
}
|
|
1065
1094
|
function destinationToStream(destination, health) {
|
|
1066
1095
|
return new stream.Writable({
|
|
@@ -1757,7 +1786,7 @@ var DestinationRegistry = class {
|
|
|
1757
1786
|
await destination.onShutdown?.();
|
|
1758
1787
|
} catch (cause) {
|
|
1759
1788
|
const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
1760
|
-
|
|
1789
|
+
writeStderrSafely(
|
|
1761
1790
|
`[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
|
|
1762
1791
|
`
|
|
1763
1792
|
);
|
|
@@ -2037,23 +2066,60 @@ exports.BymaxLoggerModule = __decorateClass([
|
|
|
2037
2066
|
], exports.BymaxLoggerModule);
|
|
2038
2067
|
|
|
2039
2068
|
// src/server/destinations/pretty-dev.destination.ts
|
|
2069
|
+
var DEFAULT_VIEW = {
|
|
2070
|
+
colorize: true,
|
|
2071
|
+
translateTime: "SYS:HH:MM:ss.l",
|
|
2072
|
+
ignore: "pid,hostname,service",
|
|
2073
|
+
singleLine: false
|
|
2074
|
+
};
|
|
2075
|
+
var MAX_BUFFERED_ENTRIES = 1e3;
|
|
2076
|
+
var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
2077
|
+
function writeRawToStdout(payload) {
|
|
2078
|
+
writeStdoutSafely(payload);
|
|
2079
|
+
}
|
|
2040
2080
|
var PrettyDevDestination = class {
|
|
2041
2081
|
/**
|
|
2042
2082
|
* @param opts.minLevel — Optional minimum level filter applied by the
|
|
2043
2083
|
* destination registry's multi-stream wiring. Omit to accept every level.
|
|
2084
|
+
* @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
|
|
2085
|
+
* Omitted fields keep their default, so `new PrettyDevDestination()` renders
|
|
2086
|
+
* exactly as it did before this option existed.
|
|
2044
2087
|
*/
|
|
2045
2088
|
constructor(opts = {}) {
|
|
2046
2089
|
this.name = "pretty-dev";
|
|
2047
2090
|
/**
|
|
2048
2091
|
* Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
|
|
2049
2092
|
* permanently-failed destination — whose writes are DROPPED, since the registry
|
|
2050
|
-
* dropped it from the active set — from the transient pre-init window
|
|
2051
|
-
* {@link write} falls back to raw stdout so a bootstrap log is not lost.
|
|
2093
|
+
* dropped it from the active set — from the transient pre-init window.
|
|
2052
2094
|
*/
|
|
2053
2095
|
this.initFailed = false;
|
|
2096
|
+
/**
|
|
2097
|
+
* Entries written before {@link onInit} built the transform, held so they can be
|
|
2098
|
+
* RENDERED rather than printed raw.
|
|
2099
|
+
*
|
|
2100
|
+
* The transform cannot exist earlier: loading the optional peer is async, and
|
|
2101
|
+
* `onInit` is the first moment the lifecycle offers. Before this buffer those
|
|
2102
|
+
* entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
|
|
2103
|
+
* put dozens of JSON lines on screen before the first rendered one, and a
|
|
2104
|
+
* developer who had just turned pretty on reasonably concluded it had not
|
|
2105
|
+
* worked. Reported from exactly that reading.
|
|
2106
|
+
*/
|
|
2107
|
+
this.buffer = [];
|
|
2108
|
+
/**
|
|
2109
|
+
* Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
|
|
2110
|
+
* straight to raw stdout — and the buffer is drained raw at the same moment, so
|
|
2111
|
+
* the output stays in ORDER rather than replaying old entries after newer ones.
|
|
2112
|
+
*/
|
|
2113
|
+
this.bufferOverflowed = false;
|
|
2114
|
+
/**
|
|
2115
|
+
* UTF-8 size of everything currently held, tracked incrementally so the bound
|
|
2116
|
+
* costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
|
|
2117
|
+
*/
|
|
2118
|
+
this.bufferedBytes = 0;
|
|
2054
2119
|
if (opts.minLevel !== void 0) {
|
|
2055
2120
|
this.minLevel = opts.minLevel;
|
|
2056
2121
|
}
|
|
2122
|
+
this.view = opts.view ?? {};
|
|
2057
2123
|
}
|
|
2058
2124
|
/**
|
|
2059
2125
|
* Lazily resolve `pino-pretty` and build the colorized transform that renders
|
|
@@ -2066,18 +2132,31 @@ var PrettyDevDestination = class {
|
|
|
2066
2132
|
try {
|
|
2067
2133
|
const { build } = await import('pino-pretty');
|
|
2068
2134
|
this.stream = build({
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2135
|
+
...DEFAULT_VIEW,
|
|
2136
|
+
...this.view,
|
|
2137
|
+
// AFTER the consumer's view, deliberately. Omitting `destination` from
|
|
2138
|
+
// `PrettyViewOptions` states that the library owns the sink; applying it
|
|
2139
|
+
// last is what ENFORCES it, including against an untyped caller reaching
|
|
2140
|
+
// in from JavaScript. A redirected stream would route around the fan-out
|
|
2141
|
+
// and the last-resort rescue.
|
|
2073
2142
|
destination: process.stdout
|
|
2074
2143
|
});
|
|
2075
2144
|
} catch {
|
|
2076
2145
|
this.initFailed = true;
|
|
2146
|
+
this.flushBuffer((payload) => {
|
|
2147
|
+
writeRawToStdout(payload);
|
|
2148
|
+
});
|
|
2077
2149
|
throw new Error(
|
|
2078
2150
|
"[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
|
|
2079
2151
|
);
|
|
2080
2152
|
}
|
|
2153
|
+
this.flushBuffer((payload) => {
|
|
2154
|
+
try {
|
|
2155
|
+
this.stream?.write(payload);
|
|
2156
|
+
} catch {
|
|
2157
|
+
writeRawToStdout(payload);
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2081
2160
|
}
|
|
2082
2161
|
/**
|
|
2083
2162
|
* Write a serialized NDJSON entry to the pretty transform.
|
|
@@ -2088,8 +2167,10 @@ var PrettyDevDestination = class {
|
|
|
2088
2167
|
* fan-out already skips a failed destination before reaching this method
|
|
2089
2168
|
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2090
2169
|
* belt to that, for any caller holding the destination directly;
|
|
2091
|
-
* - pre-init (transient window before {@link onInit}, e.g.
|
|
2092
|
-
*
|
|
2170
|
+
* - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
|
|
2171
|
+
* emits while instantiating providers) → BUFFERED, then rendered through
|
|
2172
|
+
* the transform once it exists. Nothing is lost on any path: if init fails
|
|
2173
|
+
* the buffer is drained raw, and if it overflows it is drained raw in order.
|
|
2093
2174
|
*
|
|
2094
2175
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
2095
2176
|
*/
|
|
@@ -2101,7 +2182,36 @@ var PrettyDevDestination = class {
|
|
|
2101
2182
|
if (this.initFailed) {
|
|
2102
2183
|
return;
|
|
2103
2184
|
}
|
|
2104
|
-
|
|
2185
|
+
if (this.bufferOverflowed) {
|
|
2186
|
+
writeRawToStdout(payload);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
2190
|
+
if (this.buffer.length >= MAX_BUFFERED_ENTRIES || this.bufferedBytes + payloadBytes > MAX_BUFFERED_BYTES) {
|
|
2191
|
+
this.bufferOverflowed = true;
|
|
2192
|
+
this.flushBuffer(writeRawToStdout);
|
|
2193
|
+
writeRawToStdout(payload);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
this.buffer.push(payload);
|
|
2197
|
+
this.bufferedBytes += payloadBytes;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
2201
|
+
*
|
|
2202
|
+
* Emptying unconditionally is what makes this safe to call from both the
|
|
2203
|
+
* success and failure paths: whichever runs first owns the entries, and the
|
|
2204
|
+
* other finds nothing to emit twice.
|
|
2205
|
+
*
|
|
2206
|
+
* @param emit - Receives each buffered payload, oldest first.
|
|
2207
|
+
*/
|
|
2208
|
+
flushBuffer(emit) {
|
|
2209
|
+
const held = this.buffer;
|
|
2210
|
+
this.buffer = [];
|
|
2211
|
+
this.bufferedBytes = 0;
|
|
2212
|
+
for (const payload of held) {
|
|
2213
|
+
emit(payload);
|
|
2214
|
+
}
|
|
2105
2215
|
}
|
|
2106
2216
|
/**
|
|
2107
2217
|
* Flush and close the pretty transform, resolving once the stream has ended.
|
|
@@ -2110,6 +2220,7 @@ var PrettyDevDestination = class {
|
|
|
2110
2220
|
async onShutdown() {
|
|
2111
2221
|
const stream = this.stream;
|
|
2112
2222
|
if (stream === void 0) {
|
|
2223
|
+
this.flushBuffer(writeRawToStdout);
|
|
2113
2224
|
return;
|
|
2114
2225
|
}
|
|
2115
2226
|
await new Promise((resolve, reject) => {
|
package/dist/server/index.d.cts
CHANGED
|
@@ -864,28 +864,104 @@ declare class DefaultStdoutDestination implements ILogDestination {
|
|
|
864
864
|
});
|
|
865
865
|
/**
|
|
866
866
|
* Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
|
|
867
|
+
*
|
|
868
|
+
* Routed through {@link writeStdoutSafely} rather than calling
|
|
869
|
+
* `process.stdout.write` directly. This is the sink a consumer gets when they
|
|
870
|
+
* configure nothing, so it is the path that decides whether a closed pipe
|
|
871
|
+
* (`node app | head`) kills the application — and unguarded, it did: EPIPE
|
|
872
|
+
* arrives asynchronously through the stream's `'error'` event, Node attaches no
|
|
873
|
+
* default handler, and the emit becomes an uncaught exception. Measured on the
|
|
874
|
+
* built artifact, exit code 42.
|
|
867
875
|
*/
|
|
868
876
|
write(payload: string): void;
|
|
869
877
|
}
|
|
870
878
|
|
|
879
|
+
/**
|
|
880
|
+
* How the development terminal renders an entry — a VIEW over the record, never
|
|
881
|
+
* a change to it. Every field maps to the `pino-pretty` option of the same name.
|
|
882
|
+
*
|
|
883
|
+
* Declared here rather than reusing `pino-pretty`'s own `PrettyOptions`, and the
|
|
884
|
+
* reason is load-bearing: a type import from the peer emits a hard
|
|
885
|
+
* `import { PrettyOptions } from 'pino-pretty'` at the top of the published
|
|
886
|
+
* `.d.ts`, so every consumer who type-checks WITHOUT the optional peer installed
|
|
887
|
+
* would hit an unresolved module. That would make an optional peer effectively
|
|
888
|
+
* required — a worse regression than the missing options, and the same
|
|
889
|
+
* contradiction this library already refused when asked to hard-fail on a missing
|
|
890
|
+
* peer. Measured on the built artifact, not assumed.
|
|
891
|
+
*
|
|
892
|
+
* `destination` is deliberately absent. The library owns where entries go; a
|
|
893
|
+
* consumer redirecting the stream would route around the multistream fan-out and
|
|
894
|
+
* the last-resort rescue. Omitting it from this type states the intent — applying
|
|
895
|
+
* the library's own value AFTER the spread in {@link PrettyDevDestination.onInit}
|
|
896
|
+
* is what enforces it, including against an untyped JavaScript caller.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* // One line per entry, with the fields a project repeats on every line hidden.
|
|
900
|
+
* const view: PrettyViewOptions = {
|
|
901
|
+
* singleLine: true,
|
|
902
|
+
* ignore: 'pid,hostname,service,deployment,event.name'
|
|
903
|
+
* }
|
|
904
|
+
*/
|
|
905
|
+
interface PrettyViewOptions {
|
|
906
|
+
/**
|
|
907
|
+
* Collapse the whole entry onto one line. The single biggest change to how a
|
|
908
|
+
* terminal reads — `false` is what expands one entry into a block.
|
|
909
|
+
* Default: `false`.
|
|
910
|
+
*/
|
|
911
|
+
singleLine?: boolean;
|
|
912
|
+
/**
|
|
913
|
+
* Comma-separated field names to hide from the rendered line. Display-only:
|
|
914
|
+
* what a real sink receives is untouched. Extend rather than replace when the
|
|
915
|
+
* goal is quieter output — the default already hides the noisiest fields.
|
|
916
|
+
* Default: `'pid,hostname,service'`.
|
|
917
|
+
*/
|
|
918
|
+
ignore?: string;
|
|
919
|
+
/**
|
|
920
|
+
* Render only the message line, hiding the record entirely. In pretty mode
|
|
921
|
+
* there is no JSON copy behind it (`destinations` REPLACES stdout), so a field
|
|
922
|
+
* hidden here is not visible anywhere — including `logKey`. That is the point
|
|
923
|
+
* of the option rather than a caveat; use `messageFormat` to pull a specific
|
|
924
|
+
* field back into the line. Default: `false`.
|
|
925
|
+
*/
|
|
926
|
+
hideObject?: boolean;
|
|
927
|
+
/**
|
|
928
|
+
* Template for the message line, e.g. `'[{context}] {msg}'`. The way to keep a
|
|
929
|
+
* chosen field visible when `hideObject` is on.
|
|
930
|
+
*/
|
|
931
|
+
messageFormat?: string;
|
|
932
|
+
/** Timestamp format, or `false` to leave it raw. Default: `'SYS:HH:MM:ss.l'`. */
|
|
933
|
+
translateTime?: string | boolean;
|
|
934
|
+
/** ANSI colour in the rendered line. Default: `true`. */
|
|
935
|
+
colorize?: boolean;
|
|
936
|
+
}
|
|
871
937
|
/**
|
|
872
938
|
* Pretty-print destination backed by `pino-pretty`.
|
|
873
939
|
*
|
|
874
940
|
* Lifecycle contract (driven by `DestinationRegistry`):
|
|
875
|
-
* 1. `onInit()` resolves `pino-pretty
|
|
876
|
-
* `process.stdout`).
|
|
941
|
+
* 1. `onInit()` resolves `pino-pretty`, builds the transform (rendering to
|
|
942
|
+
* `process.stdout`), and flushes anything written before it existed.
|
|
877
943
|
* 2. `write()` forwards each serialized line to the transform.
|
|
878
944
|
* 3. `onShutdown()` flushes and closes the transform.
|
|
879
945
|
*
|
|
880
946
|
* @example
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
947
|
+
* // Default view — unchanged by the options below.
|
|
948
|
+
* new PrettyDevDestination()
|
|
949
|
+
*
|
|
950
|
+
* // One line per entry, with this project's repeated fields hidden.
|
|
951
|
+
* new PrettyDevDestination({
|
|
952
|
+
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
|
|
953
|
+
* })
|
|
954
|
+
*
|
|
955
|
+
* // Message-only, with the context pulled back into the line.
|
|
956
|
+
* new PrettyDevDestination({
|
|
957
|
+
* view: { hideObject: true, messageFormat: '[{context}] {msg}' }
|
|
884
958
|
* })
|
|
885
959
|
*/
|
|
886
960
|
declare class PrettyDevDestination implements ILogDestination {
|
|
887
961
|
readonly name = "pretty-dev";
|
|
888
962
|
readonly minLevel?: LogLevel;
|
|
963
|
+
/** Consumer-chosen view, merged over {@link DEFAULT_VIEW} at init. */
|
|
964
|
+
private readonly view;
|
|
889
965
|
/**
|
|
890
966
|
* The `pino-pretty` transform stream. Undefined until {@link onInit} succeeds.
|
|
891
967
|
*/
|
|
@@ -893,16 +969,42 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
893
969
|
/**
|
|
894
970
|
* Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
|
|
895
971
|
* permanently-failed destination — whose writes are DROPPED, since the registry
|
|
896
|
-
* dropped it from the active set — from the transient pre-init window
|
|
897
|
-
* {@link write} falls back to raw stdout so a bootstrap log is not lost.
|
|
972
|
+
* dropped it from the active set — from the transient pre-init window.
|
|
898
973
|
*/
|
|
899
974
|
private initFailed;
|
|
975
|
+
/**
|
|
976
|
+
* Entries written before {@link onInit} built the transform, held so they can be
|
|
977
|
+
* RENDERED rather than printed raw.
|
|
978
|
+
*
|
|
979
|
+
* The transform cannot exist earlier: loading the optional peer is async, and
|
|
980
|
+
* `onInit` is the first moment the lifecycle offers. Before this buffer those
|
|
981
|
+
* entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
|
|
982
|
+
* put dozens of JSON lines on screen before the first rendered one, and a
|
|
983
|
+
* developer who had just turned pretty on reasonably concluded it had not
|
|
984
|
+
* worked. Reported from exactly that reading.
|
|
985
|
+
*/
|
|
986
|
+
private buffer;
|
|
987
|
+
/**
|
|
988
|
+
* Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
|
|
989
|
+
* straight to raw stdout — and the buffer is drained raw at the same moment, so
|
|
990
|
+
* the output stays in ORDER rather than replaying old entries after newer ones.
|
|
991
|
+
*/
|
|
992
|
+
private bufferOverflowed;
|
|
993
|
+
/**
|
|
994
|
+
* UTF-8 size of everything currently held, tracked incrementally so the bound
|
|
995
|
+
* costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
|
|
996
|
+
*/
|
|
997
|
+
private bufferedBytes;
|
|
900
998
|
/**
|
|
901
999
|
* @param opts.minLevel — Optional minimum level filter applied by the
|
|
902
1000
|
* destination registry's multi-stream wiring. Omit to accept every level.
|
|
1001
|
+
* @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
|
|
1002
|
+
* Omitted fields keep their default, so `new PrettyDevDestination()` renders
|
|
1003
|
+
* exactly as it did before this option existed.
|
|
903
1004
|
*/
|
|
904
1005
|
constructor(opts?: {
|
|
905
1006
|
minLevel?: LogLevel;
|
|
1007
|
+
view?: PrettyViewOptions;
|
|
906
1008
|
});
|
|
907
1009
|
/**
|
|
908
1010
|
* Lazily resolve `pino-pretty` and build the colorized transform that renders
|
|
@@ -921,12 +1023,24 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
921
1023
|
* fan-out already skips a failed destination before reaching this method
|
|
922
1024
|
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
1025
|
* belt to that, for any caller holding the destination directly;
|
|
924
|
-
* - pre-init (transient window before {@link onInit}, e.g.
|
|
925
|
-
*
|
|
1026
|
+
* - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
|
|
1027
|
+
* emits while instantiating providers) → BUFFERED, then rendered through
|
|
1028
|
+
* the transform once it exists. Nothing is lost on any path: if init fails
|
|
1029
|
+
* the buffer is drained raw, and if it overflows it is drained raw in order.
|
|
926
1030
|
*
|
|
927
1031
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
928
1032
|
*/
|
|
929
1033
|
write(payload: string): void;
|
|
1034
|
+
/**
|
|
1035
|
+
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
1036
|
+
*
|
|
1037
|
+
* Emptying unconditionally is what makes this safe to call from both the
|
|
1038
|
+
* success and failure paths: whichever runs first owns the entries, and the
|
|
1039
|
+
* other finds nothing to emit twice.
|
|
1040
|
+
*
|
|
1041
|
+
* @param emit - Receives each buffered payload, oldest first.
|
|
1042
|
+
*/
|
|
1043
|
+
private flushBuffer;
|
|
930
1044
|
/**
|
|
931
1045
|
* Flush and close the pretty transform, resolving once the stream has ended.
|
|
932
1046
|
* A no-op when {@link onInit} never ran.
|
|
@@ -1382,4 +1496,4 @@ interface LogEntry {
|
|
|
1382
1496
|
*/
|
|
1383
1497
|
declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
|
|
1384
1498
|
|
|
1385
|
-
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
|
1499
|
+
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, type PrettyViewOptions, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -864,28 +864,104 @@ declare class DefaultStdoutDestination implements ILogDestination {
|
|
|
864
864
|
});
|
|
865
865
|
/**
|
|
866
866
|
* Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
|
|
867
|
+
*
|
|
868
|
+
* Routed through {@link writeStdoutSafely} rather than calling
|
|
869
|
+
* `process.stdout.write` directly. This is the sink a consumer gets when they
|
|
870
|
+
* configure nothing, so it is the path that decides whether a closed pipe
|
|
871
|
+
* (`node app | head`) kills the application — and unguarded, it did: EPIPE
|
|
872
|
+
* arrives asynchronously through the stream's `'error'` event, Node attaches no
|
|
873
|
+
* default handler, and the emit becomes an uncaught exception. Measured on the
|
|
874
|
+
* built artifact, exit code 42.
|
|
867
875
|
*/
|
|
868
876
|
write(payload: string): void;
|
|
869
877
|
}
|
|
870
878
|
|
|
879
|
+
/**
|
|
880
|
+
* How the development terminal renders an entry — a VIEW over the record, never
|
|
881
|
+
* a change to it. Every field maps to the `pino-pretty` option of the same name.
|
|
882
|
+
*
|
|
883
|
+
* Declared here rather than reusing `pino-pretty`'s own `PrettyOptions`, and the
|
|
884
|
+
* reason is load-bearing: a type import from the peer emits a hard
|
|
885
|
+
* `import { PrettyOptions } from 'pino-pretty'` at the top of the published
|
|
886
|
+
* `.d.ts`, so every consumer who type-checks WITHOUT the optional peer installed
|
|
887
|
+
* would hit an unresolved module. That would make an optional peer effectively
|
|
888
|
+
* required — a worse regression than the missing options, and the same
|
|
889
|
+
* contradiction this library already refused when asked to hard-fail on a missing
|
|
890
|
+
* peer. Measured on the built artifact, not assumed.
|
|
891
|
+
*
|
|
892
|
+
* `destination` is deliberately absent. The library owns where entries go; a
|
|
893
|
+
* consumer redirecting the stream would route around the multistream fan-out and
|
|
894
|
+
* the last-resort rescue. Omitting it from this type states the intent — applying
|
|
895
|
+
* the library's own value AFTER the spread in {@link PrettyDevDestination.onInit}
|
|
896
|
+
* is what enforces it, including against an untyped JavaScript caller.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* // One line per entry, with the fields a project repeats on every line hidden.
|
|
900
|
+
* const view: PrettyViewOptions = {
|
|
901
|
+
* singleLine: true,
|
|
902
|
+
* ignore: 'pid,hostname,service,deployment,event.name'
|
|
903
|
+
* }
|
|
904
|
+
*/
|
|
905
|
+
interface PrettyViewOptions {
|
|
906
|
+
/**
|
|
907
|
+
* Collapse the whole entry onto one line. The single biggest change to how a
|
|
908
|
+
* terminal reads — `false` is what expands one entry into a block.
|
|
909
|
+
* Default: `false`.
|
|
910
|
+
*/
|
|
911
|
+
singleLine?: boolean;
|
|
912
|
+
/**
|
|
913
|
+
* Comma-separated field names to hide from the rendered line. Display-only:
|
|
914
|
+
* what a real sink receives is untouched. Extend rather than replace when the
|
|
915
|
+
* goal is quieter output — the default already hides the noisiest fields.
|
|
916
|
+
* Default: `'pid,hostname,service'`.
|
|
917
|
+
*/
|
|
918
|
+
ignore?: string;
|
|
919
|
+
/**
|
|
920
|
+
* Render only the message line, hiding the record entirely. In pretty mode
|
|
921
|
+
* there is no JSON copy behind it (`destinations` REPLACES stdout), so a field
|
|
922
|
+
* hidden here is not visible anywhere — including `logKey`. That is the point
|
|
923
|
+
* of the option rather than a caveat; use `messageFormat` to pull a specific
|
|
924
|
+
* field back into the line. Default: `false`.
|
|
925
|
+
*/
|
|
926
|
+
hideObject?: boolean;
|
|
927
|
+
/**
|
|
928
|
+
* Template for the message line, e.g. `'[{context}] {msg}'`. The way to keep a
|
|
929
|
+
* chosen field visible when `hideObject` is on.
|
|
930
|
+
*/
|
|
931
|
+
messageFormat?: string;
|
|
932
|
+
/** Timestamp format, or `false` to leave it raw. Default: `'SYS:HH:MM:ss.l'`. */
|
|
933
|
+
translateTime?: string | boolean;
|
|
934
|
+
/** ANSI colour in the rendered line. Default: `true`. */
|
|
935
|
+
colorize?: boolean;
|
|
936
|
+
}
|
|
871
937
|
/**
|
|
872
938
|
* Pretty-print destination backed by `pino-pretty`.
|
|
873
939
|
*
|
|
874
940
|
* Lifecycle contract (driven by `DestinationRegistry`):
|
|
875
|
-
* 1. `onInit()` resolves `pino-pretty
|
|
876
|
-
* `process.stdout`).
|
|
941
|
+
* 1. `onInit()` resolves `pino-pretty`, builds the transform (rendering to
|
|
942
|
+
* `process.stdout`), and flushes anything written before it existed.
|
|
877
943
|
* 2. `write()` forwards each serialized line to the transform.
|
|
878
944
|
* 3. `onShutdown()` flushes and closes the transform.
|
|
879
945
|
*
|
|
880
946
|
* @example
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
947
|
+
* // Default view — unchanged by the options below.
|
|
948
|
+
* new PrettyDevDestination()
|
|
949
|
+
*
|
|
950
|
+
* // One line per entry, with this project's repeated fields hidden.
|
|
951
|
+
* new PrettyDevDestination({
|
|
952
|
+
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
|
|
953
|
+
* })
|
|
954
|
+
*
|
|
955
|
+
* // Message-only, with the context pulled back into the line.
|
|
956
|
+
* new PrettyDevDestination({
|
|
957
|
+
* view: { hideObject: true, messageFormat: '[{context}] {msg}' }
|
|
884
958
|
* })
|
|
885
959
|
*/
|
|
886
960
|
declare class PrettyDevDestination implements ILogDestination {
|
|
887
961
|
readonly name = "pretty-dev";
|
|
888
962
|
readonly minLevel?: LogLevel;
|
|
963
|
+
/** Consumer-chosen view, merged over {@link DEFAULT_VIEW} at init. */
|
|
964
|
+
private readonly view;
|
|
889
965
|
/**
|
|
890
966
|
* The `pino-pretty` transform stream. Undefined until {@link onInit} succeeds.
|
|
891
967
|
*/
|
|
@@ -893,16 +969,42 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
893
969
|
/**
|
|
894
970
|
* Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
|
|
895
971
|
* permanently-failed destination — whose writes are DROPPED, since the registry
|
|
896
|
-
* dropped it from the active set — from the transient pre-init window
|
|
897
|
-
* {@link write} falls back to raw stdout so a bootstrap log is not lost.
|
|
972
|
+
* dropped it from the active set — from the transient pre-init window.
|
|
898
973
|
*/
|
|
899
974
|
private initFailed;
|
|
975
|
+
/**
|
|
976
|
+
* Entries written before {@link onInit} built the transform, held so they can be
|
|
977
|
+
* RENDERED rather than printed raw.
|
|
978
|
+
*
|
|
979
|
+
* The transform cannot exist earlier: loading the optional peer is async, and
|
|
980
|
+
* `onInit` is the first moment the lifecycle offers. Before this buffer those
|
|
981
|
+
* entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
|
|
982
|
+
* put dozens of JSON lines on screen before the first rendered one, and a
|
|
983
|
+
* developer who had just turned pretty on reasonably concluded it had not
|
|
984
|
+
* worked. Reported from exactly that reading.
|
|
985
|
+
*/
|
|
986
|
+
private buffer;
|
|
987
|
+
/**
|
|
988
|
+
* Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
|
|
989
|
+
* straight to raw stdout — and the buffer is drained raw at the same moment, so
|
|
990
|
+
* the output stays in ORDER rather than replaying old entries after newer ones.
|
|
991
|
+
*/
|
|
992
|
+
private bufferOverflowed;
|
|
993
|
+
/**
|
|
994
|
+
* UTF-8 size of everything currently held, tracked incrementally so the bound
|
|
995
|
+
* costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
|
|
996
|
+
*/
|
|
997
|
+
private bufferedBytes;
|
|
900
998
|
/**
|
|
901
999
|
* @param opts.minLevel — Optional minimum level filter applied by the
|
|
902
1000
|
* destination registry's multi-stream wiring. Omit to accept every level.
|
|
1001
|
+
* @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
|
|
1002
|
+
* Omitted fields keep their default, so `new PrettyDevDestination()` renders
|
|
1003
|
+
* exactly as it did before this option existed.
|
|
903
1004
|
*/
|
|
904
1005
|
constructor(opts?: {
|
|
905
1006
|
minLevel?: LogLevel;
|
|
1007
|
+
view?: PrettyViewOptions;
|
|
906
1008
|
});
|
|
907
1009
|
/**
|
|
908
1010
|
* Lazily resolve `pino-pretty` and build the colorized transform that renders
|
|
@@ -921,12 +1023,24 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
921
1023
|
* fan-out already skips a failed destination before reaching this method
|
|
922
1024
|
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
1025
|
* belt to that, for any caller holding the destination directly;
|
|
924
|
-
* - pre-init (transient window before {@link onInit}, e.g.
|
|
925
|
-
*
|
|
1026
|
+
* - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
|
|
1027
|
+
* emits while instantiating providers) → BUFFERED, then rendered through
|
|
1028
|
+
* the transform once it exists. Nothing is lost on any path: if init fails
|
|
1029
|
+
* the buffer is drained raw, and if it overflows it is drained raw in order.
|
|
926
1030
|
*
|
|
927
1031
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
928
1032
|
*/
|
|
929
1033
|
write(payload: string): void;
|
|
1034
|
+
/**
|
|
1035
|
+
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
1036
|
+
*
|
|
1037
|
+
* Emptying unconditionally is what makes this safe to call from both the
|
|
1038
|
+
* success and failure paths: whichever runs first owns the entries, and the
|
|
1039
|
+
* other finds nothing to emit twice.
|
|
1040
|
+
*
|
|
1041
|
+
* @param emit - Receives each buffered payload, oldest first.
|
|
1042
|
+
*/
|
|
1043
|
+
private flushBuffer;
|
|
930
1044
|
/**
|
|
931
1045
|
* Flush and close the pretty transform, resolving once the stream has ended.
|
|
932
1046
|
* A no-op when {@link onInit} never ran.
|
|
@@ -1382,4 +1496,4 @@ interface LogEntry {
|
|
|
1382
1496
|
*/
|
|
1383
1497
|
declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
|
|
1384
1498
|
|
|
1385
|
-
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
|
1499
|
+
export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, type PrettyViewOptions, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
|
package/dist/server/index.mjs
CHANGED
|
@@ -583,6 +583,30 @@ function collectContextLoggerProviders() {
|
|
|
583
583
|
function collectContextLoggerTokens() {
|
|
584
584
|
return Array.from(contextTokens.values());
|
|
585
585
|
}
|
|
586
|
+
|
|
587
|
+
// src/server/utils/safe-stdio.util.ts
|
|
588
|
+
var swallowStreamError = () => void 0;
|
|
589
|
+
function ensureGuarded(stream) {
|
|
590
|
+
if (stream.listeners("error").includes(swallowStreamError)) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
stream.on("error", swallowStreamError);
|
|
594
|
+
}
|
|
595
|
+
function writeSafely(stream, payload) {
|
|
596
|
+
try {
|
|
597
|
+
ensureGuarded(stream);
|
|
598
|
+
stream.write(payload);
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function writeStdoutSafely(payload) {
|
|
603
|
+
writeSafely(process.stdout, payload);
|
|
604
|
+
}
|
|
605
|
+
function writeStderrSafely(line) {
|
|
606
|
+
writeSafely(process.stderr, line);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/server/destinations/default-stdout.destination.ts
|
|
586
610
|
var DefaultStdoutDestination = class {
|
|
587
611
|
/**
|
|
588
612
|
* @param opts.minLevel — Optional minimum level filter. Entries below this
|
|
@@ -597,9 +621,17 @@ var DefaultStdoutDestination = class {
|
|
|
597
621
|
}
|
|
598
622
|
/**
|
|
599
623
|
* Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
|
|
624
|
+
*
|
|
625
|
+
* Routed through {@link writeStdoutSafely} rather than calling
|
|
626
|
+
* `process.stdout.write` directly. This is the sink a consumer gets when they
|
|
627
|
+
* configure nothing, so it is the path that decides whether a closed pipe
|
|
628
|
+
* (`node app | head`) kills the application — and unguarded, it did: EPIPE
|
|
629
|
+
* arrives asynchronously through the stream's `'error'` event, Node attaches no
|
|
630
|
+
* default handler, and the emit becomes an uncaught exception. Measured on the
|
|
631
|
+
* built artifact, exit code 42.
|
|
600
632
|
*/
|
|
601
633
|
write(payload) {
|
|
602
|
-
|
|
634
|
+
writeStdoutSafely(payload);
|
|
603
635
|
}
|
|
604
636
|
};
|
|
605
637
|
DefaultStdoutDestination = __decorateClass([
|
|
@@ -1031,7 +1063,7 @@ function reportDestinationFailure(logKey, name, cause, msg) {
|
|
|
1031
1063
|
type: toSingleLineMessage(String(cause.name)),
|
|
1032
1064
|
message: toSingleLineMessage(String(cause.message))
|
|
1033
1065
|
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1034
|
-
|
|
1066
|
+
writeStderrSafely(
|
|
1035
1067
|
JSON.stringify({
|
|
1036
1068
|
level: "error",
|
|
1037
1069
|
logKey,
|
|
@@ -1054,10 +1086,7 @@ function reportWriteFailure(name, cause) {
|
|
|
1054
1086
|
);
|
|
1055
1087
|
}
|
|
1056
1088
|
function rescueToStdout(payload) {
|
|
1057
|
-
|
|
1058
|
-
process.stdout.write(payload);
|
|
1059
|
-
} catch {
|
|
1060
|
-
}
|
|
1089
|
+
writeStdoutSafely(payload);
|
|
1061
1090
|
}
|
|
1062
1091
|
function destinationToStream(destination, health) {
|
|
1063
1092
|
return new Writable({
|
|
@@ -1754,7 +1783,7 @@ var DestinationRegistry = class {
|
|
|
1754
1783
|
await destination.onShutdown?.();
|
|
1755
1784
|
} catch (cause) {
|
|
1756
1785
|
const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
1757
|
-
|
|
1786
|
+
writeStderrSafely(
|
|
1758
1787
|
`[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
|
|
1759
1788
|
`
|
|
1760
1789
|
);
|
|
@@ -2034,23 +2063,60 @@ BymaxLoggerModule = __decorateClass([
|
|
|
2034
2063
|
], BymaxLoggerModule);
|
|
2035
2064
|
|
|
2036
2065
|
// src/server/destinations/pretty-dev.destination.ts
|
|
2066
|
+
var DEFAULT_VIEW = {
|
|
2067
|
+
colorize: true,
|
|
2068
|
+
translateTime: "SYS:HH:MM:ss.l",
|
|
2069
|
+
ignore: "pid,hostname,service",
|
|
2070
|
+
singleLine: false
|
|
2071
|
+
};
|
|
2072
|
+
var MAX_BUFFERED_ENTRIES = 1e3;
|
|
2073
|
+
var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
2074
|
+
function writeRawToStdout(payload) {
|
|
2075
|
+
writeStdoutSafely(payload);
|
|
2076
|
+
}
|
|
2037
2077
|
var PrettyDevDestination = class {
|
|
2038
2078
|
/**
|
|
2039
2079
|
* @param opts.minLevel — Optional minimum level filter applied by the
|
|
2040
2080
|
* destination registry's multi-stream wiring. Omit to accept every level.
|
|
2081
|
+
* @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
|
|
2082
|
+
* Omitted fields keep their default, so `new PrettyDevDestination()` renders
|
|
2083
|
+
* exactly as it did before this option existed.
|
|
2041
2084
|
*/
|
|
2042
2085
|
constructor(opts = {}) {
|
|
2043
2086
|
this.name = "pretty-dev";
|
|
2044
2087
|
/**
|
|
2045
2088
|
* Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
|
|
2046
2089
|
* permanently-failed destination — whose writes are DROPPED, since the registry
|
|
2047
|
-
* dropped it from the active set — from the transient pre-init window
|
|
2048
|
-
* {@link write} falls back to raw stdout so a bootstrap log is not lost.
|
|
2090
|
+
* dropped it from the active set — from the transient pre-init window.
|
|
2049
2091
|
*/
|
|
2050
2092
|
this.initFailed = false;
|
|
2093
|
+
/**
|
|
2094
|
+
* Entries written before {@link onInit} built the transform, held so they can be
|
|
2095
|
+
* RENDERED rather than printed raw.
|
|
2096
|
+
*
|
|
2097
|
+
* The transform cannot exist earlier: loading the optional peer is async, and
|
|
2098
|
+
* `onInit` is the first moment the lifecycle offers. Before this buffer those
|
|
2099
|
+
* entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
|
|
2100
|
+
* put dozens of JSON lines on screen before the first rendered one, and a
|
|
2101
|
+
* developer who had just turned pretty on reasonably concluded it had not
|
|
2102
|
+
* worked. Reported from exactly that reading.
|
|
2103
|
+
*/
|
|
2104
|
+
this.buffer = [];
|
|
2105
|
+
/**
|
|
2106
|
+
* Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
|
|
2107
|
+
* straight to raw stdout — and the buffer is drained raw at the same moment, so
|
|
2108
|
+
* the output stays in ORDER rather than replaying old entries after newer ones.
|
|
2109
|
+
*/
|
|
2110
|
+
this.bufferOverflowed = false;
|
|
2111
|
+
/**
|
|
2112
|
+
* UTF-8 size of everything currently held, tracked incrementally so the bound
|
|
2113
|
+
* costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
|
|
2114
|
+
*/
|
|
2115
|
+
this.bufferedBytes = 0;
|
|
2051
2116
|
if (opts.minLevel !== void 0) {
|
|
2052
2117
|
this.minLevel = opts.minLevel;
|
|
2053
2118
|
}
|
|
2119
|
+
this.view = opts.view ?? {};
|
|
2054
2120
|
}
|
|
2055
2121
|
/**
|
|
2056
2122
|
* Lazily resolve `pino-pretty` and build the colorized transform that renders
|
|
@@ -2063,18 +2129,31 @@ var PrettyDevDestination = class {
|
|
|
2063
2129
|
try {
|
|
2064
2130
|
const { build } = await import('pino-pretty');
|
|
2065
2131
|
this.stream = build({
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2132
|
+
...DEFAULT_VIEW,
|
|
2133
|
+
...this.view,
|
|
2134
|
+
// AFTER the consumer's view, deliberately. Omitting `destination` from
|
|
2135
|
+
// `PrettyViewOptions` states that the library owns the sink; applying it
|
|
2136
|
+
// last is what ENFORCES it, including against an untyped caller reaching
|
|
2137
|
+
// in from JavaScript. A redirected stream would route around the fan-out
|
|
2138
|
+
// and the last-resort rescue.
|
|
2070
2139
|
destination: process.stdout
|
|
2071
2140
|
});
|
|
2072
2141
|
} catch {
|
|
2073
2142
|
this.initFailed = true;
|
|
2143
|
+
this.flushBuffer((payload) => {
|
|
2144
|
+
writeRawToStdout(payload);
|
|
2145
|
+
});
|
|
2074
2146
|
throw new Error(
|
|
2075
2147
|
"[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
|
|
2076
2148
|
);
|
|
2077
2149
|
}
|
|
2150
|
+
this.flushBuffer((payload) => {
|
|
2151
|
+
try {
|
|
2152
|
+
this.stream?.write(payload);
|
|
2153
|
+
} catch {
|
|
2154
|
+
writeRawToStdout(payload);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2078
2157
|
}
|
|
2079
2158
|
/**
|
|
2080
2159
|
* Write a serialized NDJSON entry to the pretty transform.
|
|
@@ -2085,8 +2164,10 @@ var PrettyDevDestination = class {
|
|
|
2085
2164
|
* fan-out already skips a failed destination before reaching this method
|
|
2086
2165
|
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2087
2166
|
* belt to that, for any caller holding the destination directly;
|
|
2088
|
-
* - pre-init (transient window before {@link onInit}, e.g.
|
|
2089
|
-
*
|
|
2167
|
+
* - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
|
|
2168
|
+
* emits while instantiating providers) → BUFFERED, then rendered through
|
|
2169
|
+
* the transform once it exists. Nothing is lost on any path: if init fails
|
|
2170
|
+
* the buffer is drained raw, and if it overflows it is drained raw in order.
|
|
2090
2171
|
*
|
|
2091
2172
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
2092
2173
|
*/
|
|
@@ -2098,7 +2179,36 @@ var PrettyDevDestination = class {
|
|
|
2098
2179
|
if (this.initFailed) {
|
|
2099
2180
|
return;
|
|
2100
2181
|
}
|
|
2101
|
-
|
|
2182
|
+
if (this.bufferOverflowed) {
|
|
2183
|
+
writeRawToStdout(payload);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
2187
|
+
if (this.buffer.length >= MAX_BUFFERED_ENTRIES || this.bufferedBytes + payloadBytes > MAX_BUFFERED_BYTES) {
|
|
2188
|
+
this.bufferOverflowed = true;
|
|
2189
|
+
this.flushBuffer(writeRawToStdout);
|
|
2190
|
+
writeRawToStdout(payload);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
this.buffer.push(payload);
|
|
2194
|
+
this.bufferedBytes += payloadBytes;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
2198
|
+
*
|
|
2199
|
+
* Emptying unconditionally is what makes this safe to call from both the
|
|
2200
|
+
* success and failure paths: whichever runs first owns the entries, and the
|
|
2201
|
+
* other finds nothing to emit twice.
|
|
2202
|
+
*
|
|
2203
|
+
* @param emit - Receives each buffered payload, oldest first.
|
|
2204
|
+
*/
|
|
2205
|
+
flushBuffer(emit) {
|
|
2206
|
+
const held = this.buffer;
|
|
2207
|
+
this.buffer = [];
|
|
2208
|
+
this.bufferedBytes = 0;
|
|
2209
|
+
for (const payload of held) {
|
|
2210
|
+
emit(payload);
|
|
2211
|
+
}
|
|
2102
2212
|
}
|
|
2103
2213
|
/**
|
|
2104
2214
|
* Flush and close the pretty transform, resolving once the stream has ended.
|
|
@@ -2107,6 +2217,7 @@ var PrettyDevDestination = class {
|
|
|
2107
2217
|
async onShutdown() {
|
|
2108
2218
|
const stream = this.stream;
|
|
2109
2219
|
if (stream === void 0) {
|
|
2220
|
+
this.flushBuffer(writeRawToStdout);
|
|
2110
2221
|
return;
|
|
2111
2222
|
}
|
|
2112
2223
|
await new Promise((resolve, reject) => {
|
package/package.json
CHANGED