@bymax-one/nest-logger 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +514 -2
- package/README.md +36 -5
- package/dist/server/index.cjs +335 -32
- package/dist/server/index.d.cts +132 -10
- package/dist/server/index.d.ts +132 -10
- package/dist/server/index.mjs +335 -32
- package/package.json +3 -2
package/dist/server/index.d.ts
CHANGED
|
@@ -69,12 +69,55 @@ interface ILogDestination {
|
|
|
69
69
|
*
|
|
70
70
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
71
71
|
*/
|
|
72
|
-
write(payload: string): void |
|
|
72
|
+
write(payload: string): void | PromiseLike<void>;
|
|
73
73
|
/**
|
|
74
74
|
* Optional lifecycle hook — called once during NestJS module init.
|
|
75
75
|
* Use for opening connections, allocating buffers, scheduling flushers.
|
|
76
76
|
*/
|
|
77
|
-
onInit?(): void |
|
|
77
|
+
onInit?(): void | PromiseLike<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Optional lifecycle hook — called once after EVERY destination's `onInit`
|
|
80
|
+
* has settled, successfully or not, and before the bootstrap entry is emitted.
|
|
81
|
+
*
|
|
82
|
+
* Only useful to a destination that holds entries written before its own
|
|
83
|
+
* `onInit` ran: until this point it cannot know what became of them. The
|
|
84
|
+
* fan-out hands each entry to every registered destination whose level accepts
|
|
85
|
+
* it, so a held copy may be a SECOND copy.
|
|
86
|
+
*
|
|
87
|
+
* **This is a deduplication signal, not a proof — and the difference is the
|
|
88
|
+
* whole contract.** A held copy may be a second copy, so emitting it duplicates
|
|
89
|
+
* a line and dropping it may lose one. The flag says which risk is smaller, as
|
|
90
|
+
* far as this library can see; it does not say the entry is safe.
|
|
91
|
+
*
|
|
92
|
+
* The library's own destinations act on it by discarding when it is `true` and
|
|
93
|
+
* emitting otherwise, because a duplicated boot line is a smaller harm than a
|
|
94
|
+
* lost one. **A destination that cannot tolerate ANY loss should emit
|
|
95
|
+
* regardless** — see the accounting limits on the flag below.
|
|
96
|
+
*
|
|
97
|
+
* Called on failed destinations too, which is the point: the one that could not
|
|
98
|
+
* initialize is exactly the one still holding entries.
|
|
99
|
+
*
|
|
100
|
+
* @param status.heldEntriesDeliveredElsewhere - Whether another live sink
|
|
101
|
+
* accepted everything this destination accepted, as far as this library can
|
|
102
|
+
* see. `true` requires that sink to be not this one, initialized, at or below
|
|
103
|
+
* this level, with no write failure and no write still in flight; anything
|
|
104
|
+
* less certain is `false`.
|
|
105
|
+
*
|
|
106
|
+
* **What it cannot see:** a write still QUEUED inside the `Writable` adapter,
|
|
107
|
+
* behind a slow async sink that has not been called yet. So `true` can precede
|
|
108
|
+
* a queued write that later fails, and there is no flag that distinguishes
|
|
109
|
+
* that case — which is exactly why this is a tradeoff rather than a
|
|
110
|
+
* guarantee. It normally arrives, being queued rather than lost.
|
|
111
|
+
*
|
|
112
|
+
* @returns Nothing, or a promise the library AWAITS. Returning one is
|
|
113
|
+
* supported deliberately: TypeScript accepts an `async` implementation where
|
|
114
|
+
* a void-returning member is declared, so a hook that was not awaited would
|
|
115
|
+
* reject into nothing and let the bootstrap entry be emitted before the
|
|
116
|
+
* buffer had been resolved.
|
|
117
|
+
*/
|
|
118
|
+
onRegistryReady?(status: {
|
|
119
|
+
readonly heldEntriesDeliveredElsewhere: boolean;
|
|
120
|
+
}): void | PromiseLike<void>;
|
|
78
121
|
/**
|
|
79
122
|
* Optional lifecycle hook — called during NestJS `onApplicationShutdown`.
|
|
80
123
|
* MUST flush any pending writes and close resources.
|
|
@@ -82,7 +125,7 @@ interface ILogDestination {
|
|
|
82
125
|
* The library awaits this — graceful shutdown blocks until all destinations
|
|
83
126
|
* return.
|
|
84
127
|
*/
|
|
85
|
-
onShutdown?(): void |
|
|
128
|
+
onShutdown?(): void | PromiseLike<void>;
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
/**
|
|
@@ -636,10 +679,30 @@ declare class PinoLoggerService implements LoggerService, OnApplicationShutdown
|
|
|
636
679
|
private readonly redact;
|
|
637
680
|
private context?;
|
|
638
681
|
/**
|
|
682
|
+
* **A hand-built instance is NOT this library's behaviour.** Constructing
|
|
683
|
+
* `new PinoLoggerService(pino({...}, sink))` directly works in the sense of not
|
|
684
|
+
* throwing, and the parameter default below exists for exactly that — but the
|
|
685
|
+
* Pino instance you built has none of the wiring `buildPinoInstance` installs,
|
|
686
|
+
* and the omissions are silent:
|
|
687
|
+
*
|
|
688
|
+
* - **no `err` serializer**, so an `Error` is serialized by Pino's default.
|
|
689
|
+
* The `cause` chain disappears ENTIRELY — no marker, no warning, and a
|
|
690
|
+
* `cause` that reads as "the field was never set";
|
|
691
|
+
* - **no name-based redaction** of serializer output or child bindings;
|
|
692
|
+
* - **no trace-context mixin**, so `requestId` / `tenantId` / OTel ids are
|
|
693
|
+
* absent;
|
|
694
|
+
* - **no size bound**, so nothing is truncated.
|
|
695
|
+
*
|
|
696
|
+
* That matters most for tests, which is the use this doc used to invite without
|
|
697
|
+
* qualification: a consumer wrote one to check whether a `cause` carried its
|
|
698
|
+
* `code`, measured absence, and nearly filed it against this library. The
|
|
699
|
+
* absence was their harness. For a faithful harness, boot the module (or use
|
|
700
|
+
* `BymaxLoggerModule.forRoot` in a testing module) rather than hand-building.
|
|
701
|
+
*
|
|
639
702
|
* @param pino - The wrapped Pino instance.
|
|
640
703
|
* @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
|
|
641
|
-
* Defaults to the identity so a hand-built instance
|
|
642
|
-
*
|
|
704
|
+
* Defaults to the identity so a hand-built instance still works; the module
|
|
705
|
+
* always injects the configured one.
|
|
643
706
|
*/
|
|
644
707
|
constructor(pino: Logger, redact?: Redactor);
|
|
645
708
|
/**
|
|
@@ -932,16 +995,42 @@ interface PrettyViewOptions {
|
|
|
932
995
|
*/
|
|
933
996
|
ignore?: string;
|
|
934
997
|
/**
|
|
935
|
-
* Render only the message line, hiding the record entirely.
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
998
|
+
* Render only the message line, hiding the record entirely. Display-only, and
|
|
999
|
+
* scoped to THIS destination: a field hidden here is still delivered in full to
|
|
1000
|
+
* every other destination registered alongside it.
|
|
1001
|
+
*
|
|
1002
|
+
* When pretty is the ONLY destination, that scope makes it total — `destinations`
|
|
1003
|
+
* replaces the default stdout sink, so there is no JSON copy behind the
|
|
1004
|
+
* rendering and a hidden field, `logKey` included, is not visible anywhere. That
|
|
1005
|
+
* is the point of the option rather than a caveat; use `messageFormat` to pull a
|
|
1006
|
+
* specific field back into the line. Default: `false`.
|
|
940
1007
|
*/
|
|
941
1008
|
hideObject?: boolean;
|
|
942
1009
|
/**
|
|
943
1010
|
* Template for the message line, e.g. `'[{context}] {msg}'`. The way to keep a
|
|
944
1011
|
* chosen field visible when `hideObject` is on.
|
|
1012
|
+
*
|
|
1013
|
+
* **Every placeholder EXCEPT `{msg}` is interpolated raw, and that reopens
|
|
1014
|
+
* terminal log forging.** This library normalizes line separators and control
|
|
1015
|
+
* characters in `msg` and in the stack — not in metadata, because escaping data
|
|
1016
|
+
* would mean rewriting what you asked to be logged. `pino-pretty` substitutes
|
|
1017
|
+
* whatever the field holds, so a newline in an interpolated field splits one
|
|
1018
|
+
* entry into two, and the second reads like a genuine record. Measured, with a
|
|
1019
|
+
* newline in `context`:
|
|
1020
|
+
*
|
|
1021
|
+
* ```
|
|
1022
|
+
* lines produced by ONE entry: 2
|
|
1023
|
+
* 1: "[10:35:14.484] INFO: [Auth"
|
|
1024
|
+
* 2: "[10:00:00.000] INFO: FORGED admin promoted] real entry …"
|
|
1025
|
+
* ```
|
|
1026
|
+
*
|
|
1027
|
+
* So: interpolate only fields whose values your own code sets. Never a field
|
|
1028
|
+
* carrying user input — `{tenantId}`, `{userId}`, a header, a query value. If
|
|
1029
|
+
* you need one of those visible, leave it in the record instead of the format
|
|
1030
|
+
* string. That closes THIS path — the raw placeholder — and no more: a metadata
|
|
1031
|
+
* value is not safe terminal text either, because `JSON.stringify` escapes only
|
|
1032
|
+
* C0 and emits DEL, the C1 range (U+0085 NEL included), U+2028 and U+2029
|
|
1033
|
+
* verbatim. Only `msg` and the stack carry the one-entry-one-line guarantee.
|
|
945
1034
|
*/
|
|
946
1035
|
messageFormat?: string;
|
|
947
1036
|
/** Timestamp format, or `false` to leave it raw. Default: `'SYS:HH:MM:ss.l'`. */
|
|
@@ -1046,6 +1135,39 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
1046
1135
|
* @param payload — Newline-terminated JSON entry, UTF-8 encoded.
|
|
1047
1136
|
*/
|
|
1048
1137
|
write(payload: string): void;
|
|
1138
|
+
/**
|
|
1139
|
+
* Resolve anything still held, now that the registry knows what became of it.
|
|
1140
|
+
*
|
|
1141
|
+
* **Discard only where another sink is believed to have taken the same entries;
|
|
1142
|
+
* emit everything else.** Losing a boot entry is not an acceptable outcome, and
|
|
1143
|
+
* every richer policy tried here had a losing branch: trusting "a sink survived"
|
|
1144
|
+
* lost entries to a sink at a higher level, trusting a level lost them to a sink
|
|
1145
|
+
* that was itself the asker, then to one whose writes were throwing, then to one
|
|
1146
|
+
* whose write had not settled yet. Each of those is now folded into the single
|
|
1147
|
+
* signal handed in — and where it is uncertain, it says `false` and this drains.
|
|
1148
|
+
*
|
|
1149
|
+
* That signal is not a proof, and this hook does not treat it as one. It cannot
|
|
1150
|
+
* see an entry still queued inside the `Writable` adapter, because the adapter
|
|
1151
|
+
* has not called `destination.write()` for it yet. Discarding on it is a bet
|
|
1152
|
+
* that a duplicated boot line costs less than a lost one — which is the trade
|
|
1153
|
+
* this destination accepts, and which a destination that cannot lose anything
|
|
1154
|
+
* should decline by ignoring the flag and always emitting.
|
|
1155
|
+
*
|
|
1156
|
+
* The cost is stated rather than hidden: when nothing survived, every holding
|
|
1157
|
+
* destination drains, so N of them produce N copies of the boot entries. A
|
|
1158
|
+
* duplicated line in an already-degraded boot is a smaller harm than a lost one,
|
|
1159
|
+
* and it is loud rather than silent.
|
|
1160
|
+
*
|
|
1161
|
+
* A no-op once {@link onInit} succeeded — the buffer was flushed through the
|
|
1162
|
+
* transform there and is already empty.
|
|
1163
|
+
*
|
|
1164
|
+
* @param status.heldEntriesDeliveredElsewhere - Another live sink appears to
|
|
1165
|
+
* have taken everything this one took. A deduplication signal, not a proof —
|
|
1166
|
+
* see `ILogDestination.onRegistryReady` for what it cannot see.
|
|
1167
|
+
*/
|
|
1168
|
+
onRegistryReady(status: {
|
|
1169
|
+
readonly heldEntriesDeliveredElsewhere: boolean;
|
|
1170
|
+
}): void;
|
|
1049
1171
|
/**
|
|
1050
1172
|
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
1051
1173
|
*
|
package/dist/server/index.mjs
CHANGED
|
@@ -351,10 +351,30 @@ function assignIfDefined(payload, key, value) {
|
|
|
351
351
|
}
|
|
352
352
|
var PinoLoggerService = class {
|
|
353
353
|
/**
|
|
354
|
+
* **A hand-built instance is NOT this library's behaviour.** Constructing
|
|
355
|
+
* `new PinoLoggerService(pino({...}, sink))` directly works in the sense of not
|
|
356
|
+
* throwing, and the parameter default below exists for exactly that — but the
|
|
357
|
+
* Pino instance you built has none of the wiring `buildPinoInstance` installs,
|
|
358
|
+
* and the omissions are silent:
|
|
359
|
+
*
|
|
360
|
+
* - **no `err` serializer**, so an `Error` is serialized by Pino's default.
|
|
361
|
+
* The `cause` chain disappears ENTIRELY — no marker, no warning, and a
|
|
362
|
+
* `cause` that reads as "the field was never set";
|
|
363
|
+
* - **no name-based redaction** of serializer output or child bindings;
|
|
364
|
+
* - **no trace-context mixin**, so `requestId` / `tenantId` / OTel ids are
|
|
365
|
+
* absent;
|
|
366
|
+
* - **no size bound**, so nothing is truncated.
|
|
367
|
+
*
|
|
368
|
+
* That matters most for tests, which is the use this doc used to invite without
|
|
369
|
+
* qualification: a consumer wrote one to check whether a `cause` carried its
|
|
370
|
+
* `code`, measured absence, and nearly filed it against this library. The
|
|
371
|
+
* absence was their harness. For a faithful harness, boot the module (or use
|
|
372
|
+
* `BymaxLoggerModule.forRoot` in a testing module) rather than hand-building.
|
|
373
|
+
*
|
|
354
374
|
* @param pino - The wrapped Pino instance.
|
|
355
375
|
* @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
|
|
356
|
-
* Defaults to the identity so a hand-built instance
|
|
357
|
-
*
|
|
376
|
+
* Defaults to the identity so a hand-built instance still works; the module
|
|
377
|
+
* always injects the configured one.
|
|
358
378
|
*/
|
|
359
379
|
constructor(pino2, redact = (value) => value) {
|
|
360
380
|
this.pino = pino2;
|
|
@@ -1094,9 +1114,35 @@ function reportDestinationFailure(logKey, name, cause, msg) {
|
|
|
1094
1114
|
} catch {
|
|
1095
1115
|
}
|
|
1096
1116
|
}
|
|
1117
|
+
function safeDestinationName(destination) {
|
|
1118
|
+
try {
|
|
1119
|
+
return String(destination.name);
|
|
1120
|
+
} catch {
|
|
1121
|
+
return "unknown";
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function isLogLevel(value) {
|
|
1125
|
+
return LOG_LEVEL_PRIORITY.includes(value);
|
|
1126
|
+
}
|
|
1127
|
+
var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
|
|
1128
|
+
function safeMinLevel(destination) {
|
|
1129
|
+
if (resolvedMinLevels.has(destination)) {
|
|
1130
|
+
return resolvedMinLevels.get(destination);
|
|
1131
|
+
}
|
|
1132
|
+
let resolved;
|
|
1133
|
+
try {
|
|
1134
|
+
const read = destination.minLevel;
|
|
1135
|
+
resolved = isLogLevel(read) ? read : void 0;
|
|
1136
|
+
} catch {
|
|
1137
|
+
resolved = void 0;
|
|
1138
|
+
}
|
|
1139
|
+
resolvedMinLevels.set(destination, resolved);
|
|
1140
|
+
return resolved;
|
|
1141
|
+
}
|
|
1097
1142
|
|
|
1098
1143
|
// src/server/utils/destination-to-stream.ts
|
|
1099
|
-
function reportWriteFailure(
|
|
1144
|
+
function reportWriteFailure(destination, cause) {
|
|
1145
|
+
const name = safeDestinationName(destination);
|
|
1100
1146
|
reportDestinationFailure(
|
|
1101
1147
|
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1102
1148
|
name,
|
|
@@ -1116,7 +1162,8 @@ function destinationToStream(destination, health) {
|
|
|
1116
1162
|
decodeStrings: false,
|
|
1117
1163
|
write(chunk, _encoding, callback) {
|
|
1118
1164
|
const onFailure = (cause) => {
|
|
1119
|
-
|
|
1165
|
+
health.markWriteFailed(destination);
|
|
1166
|
+
reportWriteFailure(destination, cause);
|
|
1120
1167
|
callback();
|
|
1121
1168
|
};
|
|
1122
1169
|
try {
|
|
@@ -1129,10 +1176,20 @@ function destinationToStream(destination, health) {
|
|
|
1129
1176
|
return;
|
|
1130
1177
|
}
|
|
1131
1178
|
const result = destination.write(payload);
|
|
1132
|
-
if (result
|
|
1133
|
-
result.then(() => callback(), onFailure);
|
|
1134
|
-
} else {
|
|
1179
|
+
if (result === void 0) {
|
|
1135
1180
|
callback();
|
|
1181
|
+
} else {
|
|
1182
|
+
health.markWritePending(destination);
|
|
1183
|
+
Promise.resolve(result).then(
|
|
1184
|
+
() => {
|
|
1185
|
+
health.markWriteSettled(destination);
|
|
1186
|
+
callback();
|
|
1187
|
+
},
|
|
1188
|
+
(cause) => {
|
|
1189
|
+
health.markWriteSettled(destination);
|
|
1190
|
+
onFailure(cause);
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1136
1193
|
}
|
|
1137
1194
|
} catch (cause) {
|
|
1138
1195
|
onFailure(cause);
|
|
@@ -1577,7 +1634,11 @@ function buildPinoInstance(options, logContext, destinations, health) {
|
|
|
1577
1634
|
}
|
|
1578
1635
|
};
|
|
1579
1636
|
const streams = destinations.map((destination) => ({
|
|
1580
|
-
|
|
1637
|
+
// Guarded: this runs at provider construction, BEFORE any lifecycle hook, so a
|
|
1638
|
+
// consumer implementing `minLevel` as a throwing getter would fail the factory
|
|
1639
|
+
// and the application would never start — earlier than any fail-soft path can
|
|
1640
|
+
// contain. Unreadable and absent give the same answer: the module level.
|
|
1641
|
+
level: safeMinLevel(destination) ?? options.level,
|
|
1581
1642
|
stream: destinationToStream(destination, health)
|
|
1582
1643
|
}));
|
|
1583
1644
|
return pino(pinoOptions, pino.multistream(streams));
|
|
@@ -1587,24 +1648,91 @@ var DestinationHealth = class {
|
|
|
1587
1648
|
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1588
1649
|
this.failed = /* @__PURE__ */ new Set();
|
|
1589
1650
|
/**
|
|
1590
|
-
*
|
|
1651
|
+
* Destinations that initialized, each with its effective level as an index into
|
|
1652
|
+
* {@link LOG_LEVEL_PRIORITY}.
|
|
1653
|
+
*
|
|
1654
|
+
* Keyed by identity rather than reduced to a flag or a minimum, because the
|
|
1655
|
+
* question a buffering destination asks is "did SOMEONE ELSE receive what I
|
|
1656
|
+
* received?" — and a minimum cannot exclude the asker. A destination that is
|
|
1657
|
+
* itself the only healthy sink would otherwise be told its held entries were
|
|
1658
|
+
* delivered elsewhere, and discard its only copies.
|
|
1659
|
+
*
|
|
1660
|
+
* The level matters for the same reason it does everywhere here: `pino.multistream`
|
|
1661
|
+
* filters per stream, so "alive" and "received what I received" are different
|
|
1662
|
+
* facts.
|
|
1663
|
+
*/
|
|
1664
|
+
this.healthy = /* @__PURE__ */ new Map();
|
|
1665
|
+
/**
|
|
1666
|
+
* Destinations whose `write()` threw or rejected at least once.
|
|
1591
1667
|
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1668
|
+
* Init health says a sink is LIVE; it does not say the sink accepted anything.
|
|
1669
|
+
* A destination that throws on write is reported and skipped per entry, so
|
|
1670
|
+
* inferring delivery from level alone would credit it with entries it dropped.
|
|
1671
|
+
* Uncertainty drains, so any write failure disqualifies it as proof of delivery
|
|
1672
|
+
* for the whole window — coarser than per-entry tracking, and wrong only in the
|
|
1673
|
+
* direction that duplicates rather than loses.
|
|
1597
1674
|
*/
|
|
1598
|
-
this.
|
|
1675
|
+
this.writeFailed = /* @__PURE__ */ new Set();
|
|
1676
|
+
/**
|
|
1677
|
+
* In-flight asynchronous writes per destination.
|
|
1678
|
+
*
|
|
1679
|
+
* A rejected async write is only recorded when its promise settles, and the
|
|
1680
|
+
* registry can compute readiness while one is still pending — so "no failure
|
|
1681
|
+
* recorded" is not "every write landed". A destination with anything in flight
|
|
1682
|
+
* is UNPROVEN rather than trusted: counting it as delivery would let a
|
|
1683
|
+
* buffering sink discard its copy moments before the pending write rejects.
|
|
1684
|
+
*
|
|
1685
|
+
* A counter rather than a flag because writes overlap; it returns to zero only
|
|
1686
|
+
* when the last one settles.
|
|
1687
|
+
*/
|
|
1688
|
+
this.pendingWrites = /* @__PURE__ */ new Map();
|
|
1599
1689
|
/**
|
|
1600
1690
|
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1601
1691
|
* every real level so the first `markFailed` always wins the comparison.
|
|
1602
1692
|
*/
|
|
1603
1693
|
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1604
1694
|
}
|
|
1605
|
-
/**
|
|
1606
|
-
|
|
1607
|
-
|
|
1695
|
+
/**
|
|
1696
|
+
* Record that a destination's `write()` threw or rejected.
|
|
1697
|
+
*
|
|
1698
|
+
* Called from the fan-out's failure path, which already reports the entry as
|
|
1699
|
+
* dropped. This is what stops {@link deliveredByHealthySink} from crediting a
|
|
1700
|
+
* throwing sink with entries it never accepted.
|
|
1701
|
+
*
|
|
1702
|
+
* @param destination - The destination whose write failed.
|
|
1703
|
+
*/
|
|
1704
|
+
markWriteFailed(destination) {
|
|
1705
|
+
this.writeFailed.add(destination);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Record that an asynchronous write to a destination has started.
|
|
1709
|
+
*
|
|
1710
|
+
* @param destination - The destination being written to.
|
|
1711
|
+
*/
|
|
1712
|
+
markWritePending(destination) {
|
|
1713
|
+
this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 0) + 1);
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Record that an asynchronous write has settled, however it settled.
|
|
1717
|
+
*
|
|
1718
|
+
* Paired with {@link markWritePending} on both the fulfilled and rejected
|
|
1719
|
+
* paths — a counter that only decremented on success would never return to
|
|
1720
|
+
* zero for a failing sink, which is a different claim from the one this makes.
|
|
1721
|
+
*
|
|
1722
|
+
* @param destination - The destination whose write settled.
|
|
1723
|
+
*/
|
|
1724
|
+
markWriteSettled(destination) {
|
|
1725
|
+
this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 1) - 1);
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Record that a destination initialized successfully.
|
|
1729
|
+
*
|
|
1730
|
+
* @param destination - The destination that initialized.
|
|
1731
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1732
|
+
* the module-wide `level`.
|
|
1733
|
+
*/
|
|
1734
|
+
markHealthy(destination, effectiveLevel) {
|
|
1735
|
+
this.healthy.set(destination, LOG_LEVEL_PRIORITY.indexOf(effectiveLevel));
|
|
1608
1736
|
}
|
|
1609
1737
|
/**
|
|
1610
1738
|
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
@@ -1655,7 +1783,31 @@ var DestinationHealth = class {
|
|
|
1655
1783
|
* rescuer.
|
|
1656
1784
|
*/
|
|
1657
1785
|
shouldRescue(destination) {
|
|
1658
|
-
return
|
|
1786
|
+
return this.healthy.size === 0 && this.rescuer === destination;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Whether a LIVE sink received everything a destination at `effectiveLevel`
|
|
1790
|
+
* received — the question a destination holding pre-init entries actually has
|
|
1791
|
+
* before discarding them.
|
|
1792
|
+
*
|
|
1793
|
+
* Not "did any sink survive": `pino.multistream` filters per stream, so a
|
|
1794
|
+
* healthy `error` sink never saw the `info` entries a pretty sink at `info`
|
|
1795
|
+
* buffered. Discarding those because something else was alive would lose them,
|
|
1796
|
+
* which is the failure the buffer exists to prevent. A healthy sink whose level
|
|
1797
|
+
* is at or below this one accepted a superset, so those entries are delivered.
|
|
1798
|
+
*
|
|
1799
|
+
* @param effectiveLevel - The asking destination's multistream level.
|
|
1800
|
+
* @returns `true` when a live sink accepted everything this destination did.
|
|
1801
|
+
*/
|
|
1802
|
+
deliveredByHealthySink(asking, effectiveLevel) {
|
|
1803
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1804
|
+
for (const [destination, healthyLevel] of this.healthy) {
|
|
1805
|
+
const unproven = this.writeFailed.has(destination) || (this.pendingWrites.get(destination) ?? 0) > 0;
|
|
1806
|
+
if (destination !== asking && !unproven && healthyLevel <= level) {
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return false;
|
|
1659
1811
|
}
|
|
1660
1812
|
};
|
|
1661
1813
|
DestinationHealth = __decorateClass([
|
|
@@ -1707,14 +1859,70 @@ var DestinationRegistry = class {
|
|
|
1707
1859
|
try {
|
|
1708
1860
|
await destination.onInit?.();
|
|
1709
1861
|
this.active.push(destination);
|
|
1710
|
-
this.health.markHealthy();
|
|
1862
|
+
this.health.markHealthy(destination, this.effectiveLevelOf(destination));
|
|
1711
1863
|
} catch (cause) {
|
|
1712
|
-
this.health.markFailed(destination,
|
|
1713
|
-
this.reportInitFailure(destination
|
|
1864
|
+
this.health.markFailed(destination, this.effectiveLevelOf(destination));
|
|
1865
|
+
this.reportInitFailure(destination, cause);
|
|
1714
1866
|
}
|
|
1715
1867
|
}
|
|
1868
|
+
await this.notifyRegistryReady();
|
|
1716
1869
|
this.announceBootstrap();
|
|
1717
1870
|
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Tell every REGISTERED destination — live or failed — what happened to the
|
|
1873
|
+
* entries it may be holding, before the first post-init entry is emitted.
|
|
1874
|
+
*
|
|
1875
|
+
* The signal it carries is one the destination cannot compute: whether ANOTHER
|
|
1876
|
+
* live sink appears to have accepted everything this one accepted.
|
|
1877
|
+
* `pino.multistream` filters per stream, a destination is not its own witness, a
|
|
1878
|
+
* sink whose write threw did not receive that entry, and one whose write has not
|
|
1879
|
+
* settled has shown nothing yet — all of which live in `deliveredByHealthySink`.
|
|
1880
|
+
*
|
|
1881
|
+
* A signal rather than a proof, deliberately: a write still queued inside the
|
|
1882
|
+
* `Writable` adapter is invisible to that accounting, so the flag says which
|
|
1883
|
+
* risk is smaller, not that the entry is safe. See `ILogDestination.onRegistryReady`.
|
|
1884
|
+
*
|
|
1885
|
+
* Each notification is isolated: a destination that throws here is reported and
|
|
1886
|
+
* skipped rather than aborting the remaining ones or the bootstrap entry.
|
|
1887
|
+
* Failing at this hook cannot be allowed to cost more than the hook was worth.
|
|
1888
|
+
*/
|
|
1889
|
+
async notifyRegistryReady() {
|
|
1890
|
+
for (const destination of this.registered) {
|
|
1891
|
+
try {
|
|
1892
|
+
await destination.onRegistryReady?.({
|
|
1893
|
+
heldEntriesDeliveredElsewhere: this.health.deliveredByHealthySink(
|
|
1894
|
+
destination,
|
|
1895
|
+
this.effectiveLevelOf(destination)
|
|
1896
|
+
)
|
|
1897
|
+
});
|
|
1898
|
+
} catch (cause) {
|
|
1899
|
+
this.reportHookFailure(destination, cause);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* The severity a destination actually receives: the STRICTER of the module
|
|
1905
|
+
* level and its own `minLevel`.
|
|
1906
|
+
*
|
|
1907
|
+
* Pino filters at the instance level BEFORE `pino.multistream` sees an entry,
|
|
1908
|
+
* so a `minLevel` below the global one cannot widen what a destination gets —
|
|
1909
|
+
* the factory's own docs say so. Recording the raw `minLevel` therefore
|
|
1910
|
+
* understated delivery: with a global `error` and a healthy sink at `info`,
|
|
1911
|
+
* both sinks in fact receive the same `error` entries, but a pretty sink
|
|
1912
|
+
* declared at `trace` compared as `trace`, delivery read as unproven, and the
|
|
1913
|
+
* buffer drained raw — a duplicate produced by arithmetic rather than by any
|
|
1914
|
+
* real gap.
|
|
1915
|
+
*
|
|
1916
|
+
* @param destination - The destination whose threshold is being computed.
|
|
1917
|
+
* @returns The level entries must clear to reach it.
|
|
1918
|
+
*/
|
|
1919
|
+
effectiveLevelOf(destination) {
|
|
1920
|
+
const configured = safeMinLevel(destination);
|
|
1921
|
+
if (configured === void 0) {
|
|
1922
|
+
return this.options.level;
|
|
1923
|
+
}
|
|
1924
|
+
return LOG_LEVEL_PRIORITY.indexOf(configured) > LOG_LEVEL_PRIORITY.indexOf(this.options.level) ? configured : this.options.level;
|
|
1925
|
+
}
|
|
1718
1926
|
/**
|
|
1719
1927
|
* Report one destination init failure as a structured
|
|
1720
1928
|
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
@@ -1723,10 +1931,13 @@ var DestinationRegistry = class {
|
|
|
1723
1931
|
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1724
1932
|
* which stage failed — and the two cannot drift apart.
|
|
1725
1933
|
*
|
|
1726
|
-
* @param
|
|
1934
|
+
* @param destination - The failing destination. Passed whole, so its `name` is
|
|
1935
|
+
* read under `safeDestinationName` rather than at the call site — which sits
|
|
1936
|
+
* inside the catch, where a throwing getter would abort bootstrap.
|
|
1727
1937
|
* @param cause - The thrown or rejected value.
|
|
1728
1938
|
*/
|
|
1729
|
-
reportInitFailure(
|
|
1939
|
+
reportInitFailure(destination, cause) {
|
|
1940
|
+
const name = safeDestinationName(destination);
|
|
1730
1941
|
reportDestinationFailure(
|
|
1731
1942
|
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1732
1943
|
name,
|
|
@@ -1734,6 +1945,30 @@ var DestinationRegistry = class {
|
|
|
1734
1945
|
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1735
1946
|
);
|
|
1736
1947
|
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Report a failure from the readiness hook, with wording that matches the
|
|
1950
|
+
* destination's actual state.
|
|
1951
|
+
*
|
|
1952
|
+
* The loop calls every registered destination, healthy and failed alike, so one
|
|
1953
|
+
* message cannot be true for both. A healthy one keeps receiving entries and was
|
|
1954
|
+
* never an init failure; a failed one was already dropped from the fan-out and
|
|
1955
|
+
* saying it "remains active" would contradict the report it just got. The key
|
|
1956
|
+
* stays `LOGGER_DESTINATION_INIT_FAILED` either way so an operator greps one
|
|
1957
|
+
* thing across every destination-lifecycle problem.
|
|
1958
|
+
*
|
|
1959
|
+
* @param destination - The destination whose hook threw.
|
|
1960
|
+
* @param cause - The thrown value.
|
|
1961
|
+
*/
|
|
1962
|
+
reportHookFailure(destination, cause) {
|
|
1963
|
+
const stillActive = !this.health.isFailed(destination);
|
|
1964
|
+
const name = safeDestinationName(destination);
|
|
1965
|
+
reportDestinationFailure(
|
|
1966
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1967
|
+
name,
|
|
1968
|
+
cause,
|
|
1969
|
+
`Log destination "${name}" threw from onRegistryReady. ` + (stillActive ? "It initialized successfully and keeps receiving entries; only anything it was holding from before init may be affected." : "It had already failed to initialize and receives no entries; anything it was still holding is lost.")
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1737
1972
|
/**
|
|
1738
1973
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1739
1974
|
* initialized.
|
|
@@ -1790,14 +2025,50 @@ var DestinationRegistry = class {
|
|
|
1790
2025
|
try {
|
|
1791
2026
|
await destination.onShutdown?.();
|
|
1792
2027
|
} catch (cause) {
|
|
1793
|
-
|
|
1794
|
-
writeStderrSafely(
|
|
1795
|
-
`[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
|
|
1796
|
-
`
|
|
1797
|
-
);
|
|
2028
|
+
this.reportShutdownFailure(destination, cause);
|
|
1798
2029
|
}
|
|
1799
2030
|
}
|
|
1800
2031
|
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Report a failing `onShutdown` on stderr without letting the report itself
|
|
2034
|
+
* abort the teardown of the destinations still queued behind it.
|
|
2035
|
+
*
|
|
2036
|
+
* The coercion is INSIDE the guard, not above it. Reading `stack`/`message` on
|
|
2037
|
+
* an `Error` with hostile getters, and `String(cause)` on a value with a
|
|
2038
|
+
* throwing `toString`/`Symbol.toPrimitive`, both throw — and this runs inside
|
|
2039
|
+
* the `catch` that exists to keep one bad sink from mattering, so an escape
|
|
2040
|
+
* here would propagate out of `onApplicationShutdown` and skip every remaining
|
|
2041
|
+
* destination's flush. The never-throw contract has to cover READING the value,
|
|
2042
|
+
* not just writing it, which is the same correction `reportDestinationFailure`
|
|
2043
|
+
* already carries.
|
|
2044
|
+
*
|
|
2045
|
+
* The text is escaped for the same reason every other terminal-bound path is:
|
|
2046
|
+
* a stack is legitimately multi-line, so `escapeControlCharacters` keeps its
|
|
2047
|
+
* newlines and neutralizes everything else that can drive a terminal.
|
|
2048
|
+
*
|
|
2049
|
+
* The DESTINATION is passed rather than its name, so that read happens in here
|
|
2050
|
+
* too. `name` is declared `readonly name: string` but nothing stops a consumer
|
|
2051
|
+
* implementing it as a getter, and reading it at the call site would put it back
|
|
2052
|
+
* outside the guard — inside the `catch`, where a throw has the same cost.
|
|
2053
|
+
*
|
|
2054
|
+
* Two separate guards, not one: a name that cannot be read must not also cost
|
|
2055
|
+
* the failure detail, and a detail that cannot be read must not cost the name.
|
|
2056
|
+
* Either alone still identifies which sink failed.
|
|
2057
|
+
*
|
|
2058
|
+
* @param destination - The destination whose `onShutdown` threw.
|
|
2059
|
+
* @param cause - The thrown value, of any shape.
|
|
2060
|
+
*/
|
|
2061
|
+
reportShutdownFailure(destination, cause) {
|
|
2062
|
+
const name = toSingleLineMessage(safeDestinationName(destination));
|
|
2063
|
+
let detail = "UnknownError";
|
|
2064
|
+
try {
|
|
2065
|
+
const stack = isErrorLike(cause) ? cause.stack : void 0;
|
|
2066
|
+
detail = stack === void 0 ? toSingleLineMessage(isErrorLike(cause) ? String(cause.message) : String(cause)) : escapeControlCharacters(String(stack));
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
writeStderrSafely(`[DestinationRegistry] Shutdown failed for "${name}": ${detail}
|
|
2070
|
+
`);
|
|
2071
|
+
}
|
|
1801
2072
|
/**
|
|
1802
2073
|
* The destinations that initialized successfully.
|
|
1803
2074
|
*
|
|
@@ -2148,9 +2419,6 @@ var PrettyDevDestination = class {
|
|
|
2148
2419
|
});
|
|
2149
2420
|
} catch {
|
|
2150
2421
|
this.initFailed = true;
|
|
2151
|
-
this.flushBuffer((payload) => {
|
|
2152
|
-
writeRawToStdout(payload);
|
|
2153
|
-
});
|
|
2154
2422
|
throw new Error(
|
|
2155
2423
|
"[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
|
|
2156
2424
|
);
|
|
@@ -2201,6 +2469,41 @@ var PrettyDevDestination = class {
|
|
|
2201
2469
|
this.buffer.push(payload);
|
|
2202
2470
|
this.bufferedBytes += payloadBytes;
|
|
2203
2471
|
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Resolve anything still held, now that the registry knows what became of it.
|
|
2474
|
+
*
|
|
2475
|
+
* **Discard only where another sink is believed to have taken the same entries;
|
|
2476
|
+
* emit everything else.** Losing a boot entry is not an acceptable outcome, and
|
|
2477
|
+
* every richer policy tried here had a losing branch: trusting "a sink survived"
|
|
2478
|
+
* lost entries to a sink at a higher level, trusting a level lost them to a sink
|
|
2479
|
+
* that was itself the asker, then to one whose writes were throwing, then to one
|
|
2480
|
+
* whose write had not settled yet. Each of those is now folded into the single
|
|
2481
|
+
* signal handed in — and where it is uncertain, it says `false` and this drains.
|
|
2482
|
+
*
|
|
2483
|
+
* That signal is not a proof, and this hook does not treat it as one. It cannot
|
|
2484
|
+
* see an entry still queued inside the `Writable` adapter, because the adapter
|
|
2485
|
+
* has not called `destination.write()` for it yet. Discarding on it is a bet
|
|
2486
|
+
* that a duplicated boot line costs less than a lost one — which is the trade
|
|
2487
|
+
* this destination accepts, and which a destination that cannot lose anything
|
|
2488
|
+
* should decline by ignoring the flag and always emitting.
|
|
2489
|
+
*
|
|
2490
|
+
* The cost is stated rather than hidden: when nothing survived, every holding
|
|
2491
|
+
* destination drains, so N of them produce N copies of the boot entries. A
|
|
2492
|
+
* duplicated line in an already-degraded boot is a smaller harm than a lost one,
|
|
2493
|
+
* and it is loud rather than silent.
|
|
2494
|
+
*
|
|
2495
|
+
* A no-op once {@link onInit} succeeded — the buffer was flushed through the
|
|
2496
|
+
* transform there and is already empty.
|
|
2497
|
+
*
|
|
2498
|
+
* @param status.heldEntriesDeliveredElsewhere - Another live sink appears to
|
|
2499
|
+
* have taken everything this one took. A deduplication signal, not a proof —
|
|
2500
|
+
* see `ILogDestination.onRegistryReady` for what it cannot see.
|
|
2501
|
+
*/
|
|
2502
|
+
onRegistryReady(status) {
|
|
2503
|
+
this.flushBuffer(
|
|
2504
|
+
status.heldEntriesDeliveredElsewhere ? () => void 0 : writeRawToStdout
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2204
2507
|
/**
|
|
2205
2508
|
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
2206
2509
|
*
|