@bymax-one/nest-logger 1.2.6 → 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 +616 -2
- package/README.md +71 -5
- package/dist/server/index.cjs +366 -55
- package/dist/server/index.d.cts +149 -12
- package/dist/server/index.d.ts +149 -12
- package/dist/server/index.mjs +366 -55
- package/package.json +3 -2
package/dist/server/index.cjs
CHANGED
|
@@ -232,6 +232,13 @@ function escapeControlCharacters(text) {
|
|
|
232
232
|
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
233
233
|
var MAX_AGGREGATE_ERRORS = 10;
|
|
234
234
|
var CIRCULAR_SENTINEL = "[Circular]";
|
|
235
|
+
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
236
|
+
"name",
|
|
237
|
+
"message",
|
|
238
|
+
"stack",
|
|
239
|
+
"cause",
|
|
240
|
+
"errors"
|
|
241
|
+
]);
|
|
235
242
|
function sanitizeError(err, options) {
|
|
236
243
|
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
237
244
|
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
@@ -267,11 +274,23 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
|
267
274
|
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
268
275
|
}
|
|
269
276
|
}
|
|
277
|
+
assignOwnErrorFields(result, value);
|
|
270
278
|
return result;
|
|
271
279
|
} catch {
|
|
272
280
|
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
273
281
|
}
|
|
274
282
|
}
|
|
283
|
+
function assignOwnErrorFields(target, source) {
|
|
284
|
+
try {
|
|
285
|
+
const own = Array.isArray(source) ? {} : source;
|
|
286
|
+
for (const [key, value] of Object.entries(own)) {
|
|
287
|
+
if (!Object.hasOwn(target, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
288
|
+
Object.defineProperty(target, key, { value, enumerable: true });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
275
294
|
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
276
295
|
if (isObject(value) && seen.has(value)) {
|
|
277
296
|
return CIRCULAR_SENTINEL;
|
|
@@ -335,10 +354,30 @@ function assignIfDefined(payload, key, value) {
|
|
|
335
354
|
}
|
|
336
355
|
exports.PinoLoggerService = class PinoLoggerService {
|
|
337
356
|
/**
|
|
357
|
+
* **A hand-built instance is NOT this library's behaviour.** Constructing
|
|
358
|
+
* `new PinoLoggerService(pino({...}, sink))` directly works in the sense of not
|
|
359
|
+
* throwing, and the parameter default below exists for exactly that — but the
|
|
360
|
+
* Pino instance you built has none of the wiring `buildPinoInstance` installs,
|
|
361
|
+
* and the omissions are silent:
|
|
362
|
+
*
|
|
363
|
+
* - **no `err` serializer**, so an `Error` is serialized by Pino's default.
|
|
364
|
+
* The `cause` chain disappears ENTIRELY — no marker, no warning, and a
|
|
365
|
+
* `cause` that reads as "the field was never set";
|
|
366
|
+
* - **no name-based redaction** of serializer output or child bindings;
|
|
367
|
+
* - **no trace-context mixin**, so `requestId` / `tenantId` / OTel ids are
|
|
368
|
+
* absent;
|
|
369
|
+
* - **no size bound**, so nothing is truncated.
|
|
370
|
+
*
|
|
371
|
+
* That matters most for tests, which is the use this doc used to invite without
|
|
372
|
+
* qualification: a consumer wrote one to check whether a `cause` carried its
|
|
373
|
+
* `code`, measured absence, and nearly filed it against this library. The
|
|
374
|
+
* absence was their harness. For a faithful harness, boot the module (or use
|
|
375
|
+
* `BymaxLoggerModule.forRoot` in a testing module) rather than hand-building.
|
|
376
|
+
*
|
|
338
377
|
* @param pino - The wrapped Pino instance.
|
|
339
378
|
* @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
|
|
340
|
-
* Defaults to the identity so a hand-built instance
|
|
341
|
-
*
|
|
379
|
+
* Defaults to the identity so a hand-built instance still works; the module
|
|
380
|
+
* always injects the configured one.
|
|
342
381
|
*/
|
|
343
382
|
constructor(pino2, redact = (value) => value) {
|
|
344
383
|
this.pino = pino2;
|
|
@@ -1078,9 +1117,35 @@ function reportDestinationFailure(logKey, name, cause, msg) {
|
|
|
1078
1117
|
} catch {
|
|
1079
1118
|
}
|
|
1080
1119
|
}
|
|
1120
|
+
function safeDestinationName(destination) {
|
|
1121
|
+
try {
|
|
1122
|
+
return String(destination.name);
|
|
1123
|
+
} catch {
|
|
1124
|
+
return "unknown";
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function isLogLevel(value) {
|
|
1128
|
+
return LOG_LEVEL_PRIORITY.includes(value);
|
|
1129
|
+
}
|
|
1130
|
+
var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
function safeMinLevel(destination) {
|
|
1132
|
+
if (resolvedMinLevels.has(destination)) {
|
|
1133
|
+
return resolvedMinLevels.get(destination);
|
|
1134
|
+
}
|
|
1135
|
+
let resolved;
|
|
1136
|
+
try {
|
|
1137
|
+
const read = destination.minLevel;
|
|
1138
|
+
resolved = isLogLevel(read) ? read : void 0;
|
|
1139
|
+
} catch {
|
|
1140
|
+
resolved = void 0;
|
|
1141
|
+
}
|
|
1142
|
+
resolvedMinLevels.set(destination, resolved);
|
|
1143
|
+
return resolved;
|
|
1144
|
+
}
|
|
1081
1145
|
|
|
1082
1146
|
// src/server/utils/destination-to-stream.ts
|
|
1083
|
-
function reportWriteFailure(
|
|
1147
|
+
function reportWriteFailure(destination, cause) {
|
|
1148
|
+
const name = safeDestinationName(destination);
|
|
1084
1149
|
reportDestinationFailure(
|
|
1085
1150
|
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1086
1151
|
name,
|
|
@@ -1100,7 +1165,8 @@ function destinationToStream(destination, health) {
|
|
|
1100
1165
|
decodeStrings: false,
|
|
1101
1166
|
write(chunk, _encoding, callback) {
|
|
1102
1167
|
const onFailure = (cause) => {
|
|
1103
|
-
|
|
1168
|
+
health.markWriteFailed(destination);
|
|
1169
|
+
reportWriteFailure(destination, cause);
|
|
1104
1170
|
callback();
|
|
1105
1171
|
};
|
|
1106
1172
|
try {
|
|
@@ -1113,10 +1179,20 @@ function destinationToStream(destination, health) {
|
|
|
1113
1179
|
return;
|
|
1114
1180
|
}
|
|
1115
1181
|
const result = destination.write(payload);
|
|
1116
|
-
if (result
|
|
1117
|
-
result.then(() => callback(), onFailure);
|
|
1118
|
-
} else {
|
|
1182
|
+
if (result === void 0) {
|
|
1119
1183
|
callback();
|
|
1184
|
+
} else {
|
|
1185
|
+
health.markWritePending(destination);
|
|
1186
|
+
Promise.resolve(result).then(
|
|
1187
|
+
() => {
|
|
1188
|
+
health.markWriteSettled(destination);
|
|
1189
|
+
callback();
|
|
1190
|
+
},
|
|
1191
|
+
(cause) => {
|
|
1192
|
+
health.markWriteSettled(destination);
|
|
1193
|
+
onFailure(cause);
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1120
1196
|
}
|
|
1121
1197
|
} catch (cause) {
|
|
1122
1198
|
onFailure(cause);
|
|
@@ -1401,13 +1477,6 @@ function leafNameOf(path) {
|
|
|
1401
1477
|
}
|
|
1402
1478
|
return leaf;
|
|
1403
1479
|
}
|
|
1404
|
-
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
1405
|
-
"name",
|
|
1406
|
-
"message",
|
|
1407
|
-
"stack",
|
|
1408
|
-
"cause",
|
|
1409
|
-
"errors"
|
|
1410
|
-
]);
|
|
1411
1480
|
function serializeErrorValue(value_) {
|
|
1412
1481
|
const sanitized = sanitizeError(value_);
|
|
1413
1482
|
const serialized = {
|
|
@@ -1424,18 +1493,7 @@ function serializeErrorValue(value_) {
|
|
|
1424
1493
|
if (sanitized.errors !== void 0) {
|
|
1425
1494
|
serialized["errors"] = sanitized.errors;
|
|
1426
1495
|
}
|
|
1427
|
-
|
|
1428
|
-
const source = (
|
|
1429
|
-
// Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
|
|
1430
|
-
typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
|
|
1431
|
-
);
|
|
1432
|
-
for (const [key, value] of Object.entries(source)) {
|
|
1433
|
-
if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
1434
|
-
Reflect.set(serialized, key, value);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
} catch {
|
|
1438
|
-
}
|
|
1496
|
+
assignOwnErrorFields(serialized, sanitized);
|
|
1439
1497
|
return serialized;
|
|
1440
1498
|
}
|
|
1441
1499
|
function withSemconvException(record, format) {
|
|
@@ -1485,6 +1543,9 @@ function resolveNameRedactor(options) {
|
|
|
1485
1543
|
if (options.redactStrategy !== "names") {
|
|
1486
1544
|
return PASS_THROUGH;
|
|
1487
1545
|
}
|
|
1546
|
+
return buildNameRedactor(options);
|
|
1547
|
+
}
|
|
1548
|
+
function buildNameRedactor(options) {
|
|
1488
1549
|
const names = [
|
|
1489
1550
|
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1490
1551
|
...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
|
|
@@ -1503,8 +1564,9 @@ function resolveRedactOption(options) {
|
|
|
1503
1564
|
}
|
|
1504
1565
|
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1505
1566
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1506
|
-
const
|
|
1507
|
-
const
|
|
1567
|
+
const redactRecord = resolveNameRedactor(options);
|
|
1568
|
+
const redactSerialized = buildNameRedactor(options);
|
|
1569
|
+
const bound = (serializer) => createSizeBoundedSerializer((input) => redactSerialized(serializer(input)), maxBytes);
|
|
1508
1570
|
const serializers = {
|
|
1509
1571
|
err: bound(serializeErrorValue),
|
|
1510
1572
|
...Object.fromEntries(
|
|
@@ -1541,14 +1603,17 @@ function buildPinoInstance(options, logContext, destinations, health) {
|
|
|
1541
1603
|
// consumer's extra metadata instead of silently dropping it. Runs ONCE, at
|
|
1542
1604
|
// logger construction — no per-entry cost. `true`: bindings are a record
|
|
1543
1605
|
// root, iterated rather than serialized.
|
|
1544
|
-
bindings: (bindings) =>
|
|
1606
|
+
bindings: (bindings) => redactRecord(bindings, true),
|
|
1545
1607
|
// The name-walk redactor runs here, on the merged record (mixin output +
|
|
1546
1608
|
// the caller's object), which is exactly the surface a caller controls.
|
|
1547
1609
|
// `base` and child bindings are NOT visible at this hook — they are
|
|
1548
1610
|
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1549
1611
|
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1550
1612
|
log: (record) => withSemconvException(
|
|
1551
|
-
withEventName(
|
|
1613
|
+
withEventName(
|
|
1614
|
+
redactRecord(record, true),
|
|
1615
|
+
options.eventNameField
|
|
1616
|
+
),
|
|
1552
1617
|
options.errorFormat
|
|
1553
1618
|
)
|
|
1554
1619
|
},
|
|
@@ -1572,7 +1637,11 @@ function buildPinoInstance(options, logContext, destinations, health) {
|
|
|
1572
1637
|
}
|
|
1573
1638
|
};
|
|
1574
1639
|
const streams = destinations.map((destination) => ({
|
|
1575
|
-
|
|
1640
|
+
// Guarded: this runs at provider construction, BEFORE any lifecycle hook, so a
|
|
1641
|
+
// consumer implementing `minLevel` as a throwing getter would fail the factory
|
|
1642
|
+
// and the application would never start — earlier than any fail-soft path can
|
|
1643
|
+
// contain. Unreadable and absent give the same answer: the module level.
|
|
1644
|
+
level: safeMinLevel(destination) ?? options.level,
|
|
1576
1645
|
stream: destinationToStream(destination, health)
|
|
1577
1646
|
}));
|
|
1578
1647
|
return pino__default.default(pinoOptions, pino__default.default.multistream(streams));
|
|
@@ -1582,24 +1651,91 @@ var DestinationHealth = class {
|
|
|
1582
1651
|
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1583
1652
|
this.failed = /* @__PURE__ */ new Set();
|
|
1584
1653
|
/**
|
|
1585
|
-
*
|
|
1654
|
+
* Destinations that initialized, each with its effective level as an index into
|
|
1655
|
+
* {@link LOG_LEVEL_PRIORITY}.
|
|
1656
|
+
*
|
|
1657
|
+
* Keyed by identity rather than reduced to a flag or a minimum, because the
|
|
1658
|
+
* question a buffering destination asks is "did SOMEONE ELSE receive what I
|
|
1659
|
+
* received?" — and a minimum cannot exclude the asker. A destination that is
|
|
1660
|
+
* itself the only healthy sink would otherwise be told its held entries were
|
|
1661
|
+
* delivered elsewhere, and discard its only copies.
|
|
1662
|
+
*
|
|
1663
|
+
* The level matters for the same reason it does everywhere here: `pino.multistream`
|
|
1664
|
+
* filters per stream, so "alive" and "received what I received" are different
|
|
1665
|
+
* facts.
|
|
1666
|
+
*/
|
|
1667
|
+
this.healthy = /* @__PURE__ */ new Map();
|
|
1668
|
+
/**
|
|
1669
|
+
* Destinations whose `write()` threw or rejected at least once.
|
|
1670
|
+
*
|
|
1671
|
+
* Init health says a sink is LIVE; it does not say the sink accepted anything.
|
|
1672
|
+
* A destination that throws on write is reported and skipped per entry, so
|
|
1673
|
+
* inferring delivery from level alone would credit it with entries it dropped.
|
|
1674
|
+
* Uncertainty drains, so any write failure disqualifies it as proof of delivery
|
|
1675
|
+
* for the whole window — coarser than per-entry tracking, and wrong only in the
|
|
1676
|
+
* direction that duplicates rather than loses.
|
|
1677
|
+
*/
|
|
1678
|
+
this.writeFailed = /* @__PURE__ */ new Set();
|
|
1679
|
+
/**
|
|
1680
|
+
* In-flight asynchronous writes per destination.
|
|
1681
|
+
*
|
|
1682
|
+
* A rejected async write is only recorded when its promise settles, and the
|
|
1683
|
+
* registry can compute readiness while one is still pending — so "no failure
|
|
1684
|
+
* recorded" is not "every write landed". A destination with anything in flight
|
|
1685
|
+
* is UNPROVEN rather than trusted: counting it as delivery would let a
|
|
1686
|
+
* buffering sink discard its copy moments before the pending write rejects.
|
|
1586
1687
|
*
|
|
1587
|
-
* A
|
|
1588
|
-
*
|
|
1589
|
-
* counter incremented with `+=` survived being flipped to `-=`, because
|
|
1590
|
-
* `count === 0` means the same thing either way. A value whose arithmetic
|
|
1591
|
-
* cannot be observed was not a counter, it was a flag wearing a number.
|
|
1688
|
+
* A counter rather than a flag because writes overlap; it returns to zero only
|
|
1689
|
+
* when the last one settles.
|
|
1592
1690
|
*/
|
|
1593
|
-
this.
|
|
1691
|
+
this.pendingWrites = /* @__PURE__ */ new Map();
|
|
1594
1692
|
/**
|
|
1595
1693
|
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1596
1694
|
* every real level so the first `markFailed` always wins the comparison.
|
|
1597
1695
|
*/
|
|
1598
1696
|
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1599
1697
|
}
|
|
1600
|
-
/**
|
|
1601
|
-
|
|
1602
|
-
|
|
1698
|
+
/**
|
|
1699
|
+
* Record that a destination's `write()` threw or rejected.
|
|
1700
|
+
*
|
|
1701
|
+
* Called from the fan-out's failure path, which already reports the entry as
|
|
1702
|
+
* dropped. This is what stops {@link deliveredByHealthySink} from crediting a
|
|
1703
|
+
* throwing sink with entries it never accepted.
|
|
1704
|
+
*
|
|
1705
|
+
* @param destination - The destination whose write failed.
|
|
1706
|
+
*/
|
|
1707
|
+
markWriteFailed(destination) {
|
|
1708
|
+
this.writeFailed.add(destination);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Record that an asynchronous write to a destination has started.
|
|
1712
|
+
*
|
|
1713
|
+
* @param destination - The destination being written to.
|
|
1714
|
+
*/
|
|
1715
|
+
markWritePending(destination) {
|
|
1716
|
+
this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 0) + 1);
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Record that an asynchronous write has settled, however it settled.
|
|
1720
|
+
*
|
|
1721
|
+
* Paired with {@link markWritePending} on both the fulfilled and rejected
|
|
1722
|
+
* paths — a counter that only decremented on success would never return to
|
|
1723
|
+
* zero for a failing sink, which is a different claim from the one this makes.
|
|
1724
|
+
*
|
|
1725
|
+
* @param destination - The destination whose write settled.
|
|
1726
|
+
*/
|
|
1727
|
+
markWriteSettled(destination) {
|
|
1728
|
+
this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 1) - 1);
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Record that a destination initialized successfully.
|
|
1732
|
+
*
|
|
1733
|
+
* @param destination - The destination that initialized.
|
|
1734
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1735
|
+
* the module-wide `level`.
|
|
1736
|
+
*/
|
|
1737
|
+
markHealthy(destination, effectiveLevel) {
|
|
1738
|
+
this.healthy.set(destination, LOG_LEVEL_PRIORITY.indexOf(effectiveLevel));
|
|
1603
1739
|
}
|
|
1604
1740
|
/**
|
|
1605
1741
|
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
@@ -1650,7 +1786,31 @@ var DestinationHealth = class {
|
|
|
1650
1786
|
* rescuer.
|
|
1651
1787
|
*/
|
|
1652
1788
|
shouldRescue(destination) {
|
|
1653
|
-
return
|
|
1789
|
+
return this.healthy.size === 0 && this.rescuer === destination;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Whether a LIVE sink received everything a destination at `effectiveLevel`
|
|
1793
|
+
* received — the question a destination holding pre-init entries actually has
|
|
1794
|
+
* before discarding them.
|
|
1795
|
+
*
|
|
1796
|
+
* Not "did any sink survive": `pino.multistream` filters per stream, so a
|
|
1797
|
+
* healthy `error` sink never saw the `info` entries a pretty sink at `info`
|
|
1798
|
+
* buffered. Discarding those because something else was alive would lose them,
|
|
1799
|
+
* which is the failure the buffer exists to prevent. A healthy sink whose level
|
|
1800
|
+
* is at or below this one accepted a superset, so those entries are delivered.
|
|
1801
|
+
*
|
|
1802
|
+
* @param effectiveLevel - The asking destination's multistream level.
|
|
1803
|
+
* @returns `true` when a live sink accepted everything this destination did.
|
|
1804
|
+
*/
|
|
1805
|
+
deliveredByHealthySink(asking, effectiveLevel) {
|
|
1806
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1807
|
+
for (const [destination, healthyLevel] of this.healthy) {
|
|
1808
|
+
const unproven = this.writeFailed.has(destination) || (this.pendingWrites.get(destination) ?? 0) > 0;
|
|
1809
|
+
if (destination !== asking && !unproven && healthyLevel <= level) {
|
|
1810
|
+
return true;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return false;
|
|
1654
1814
|
}
|
|
1655
1815
|
};
|
|
1656
1816
|
DestinationHealth = __decorateClass([
|
|
@@ -1702,14 +1862,70 @@ var DestinationRegistry = class {
|
|
|
1702
1862
|
try {
|
|
1703
1863
|
await destination.onInit?.();
|
|
1704
1864
|
this.active.push(destination);
|
|
1705
|
-
this.health.markHealthy();
|
|
1865
|
+
this.health.markHealthy(destination, this.effectiveLevelOf(destination));
|
|
1706
1866
|
} catch (cause) {
|
|
1707
|
-
this.health.markFailed(destination,
|
|
1708
|
-
this.reportInitFailure(destination
|
|
1867
|
+
this.health.markFailed(destination, this.effectiveLevelOf(destination));
|
|
1868
|
+
this.reportInitFailure(destination, cause);
|
|
1709
1869
|
}
|
|
1710
1870
|
}
|
|
1871
|
+
await this.notifyRegistryReady();
|
|
1711
1872
|
this.announceBootstrap();
|
|
1712
1873
|
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Tell every REGISTERED destination — live or failed — what happened to the
|
|
1876
|
+
* entries it may be holding, before the first post-init entry is emitted.
|
|
1877
|
+
*
|
|
1878
|
+
* The signal it carries is one the destination cannot compute: whether ANOTHER
|
|
1879
|
+
* live sink appears to have accepted everything this one accepted.
|
|
1880
|
+
* `pino.multistream` filters per stream, a destination is not its own witness, a
|
|
1881
|
+
* sink whose write threw did not receive that entry, and one whose write has not
|
|
1882
|
+
* settled has shown nothing yet — all of which live in `deliveredByHealthySink`.
|
|
1883
|
+
*
|
|
1884
|
+
* A signal rather than a proof, deliberately: a write still queued inside the
|
|
1885
|
+
* `Writable` adapter is invisible to that accounting, so the flag says which
|
|
1886
|
+
* risk is smaller, not that the entry is safe. See `ILogDestination.onRegistryReady`.
|
|
1887
|
+
*
|
|
1888
|
+
* Each notification is isolated: a destination that throws here is reported and
|
|
1889
|
+
* skipped rather than aborting the remaining ones or the bootstrap entry.
|
|
1890
|
+
* Failing at this hook cannot be allowed to cost more than the hook was worth.
|
|
1891
|
+
*/
|
|
1892
|
+
async notifyRegistryReady() {
|
|
1893
|
+
for (const destination of this.registered) {
|
|
1894
|
+
try {
|
|
1895
|
+
await destination.onRegistryReady?.({
|
|
1896
|
+
heldEntriesDeliveredElsewhere: this.health.deliveredByHealthySink(
|
|
1897
|
+
destination,
|
|
1898
|
+
this.effectiveLevelOf(destination)
|
|
1899
|
+
)
|
|
1900
|
+
});
|
|
1901
|
+
} catch (cause) {
|
|
1902
|
+
this.reportHookFailure(destination, cause);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* The severity a destination actually receives: the STRICTER of the module
|
|
1908
|
+
* level and its own `minLevel`.
|
|
1909
|
+
*
|
|
1910
|
+
* Pino filters at the instance level BEFORE `pino.multistream` sees an entry,
|
|
1911
|
+
* so a `minLevel` below the global one cannot widen what a destination gets —
|
|
1912
|
+
* the factory's own docs say so. Recording the raw `minLevel` therefore
|
|
1913
|
+
* understated delivery: with a global `error` and a healthy sink at `info`,
|
|
1914
|
+
* both sinks in fact receive the same `error` entries, but a pretty sink
|
|
1915
|
+
* declared at `trace` compared as `trace`, delivery read as unproven, and the
|
|
1916
|
+
* buffer drained raw — a duplicate produced by arithmetic rather than by any
|
|
1917
|
+
* real gap.
|
|
1918
|
+
*
|
|
1919
|
+
* @param destination - The destination whose threshold is being computed.
|
|
1920
|
+
* @returns The level entries must clear to reach it.
|
|
1921
|
+
*/
|
|
1922
|
+
effectiveLevelOf(destination) {
|
|
1923
|
+
const configured = safeMinLevel(destination);
|
|
1924
|
+
if (configured === void 0) {
|
|
1925
|
+
return this.options.level;
|
|
1926
|
+
}
|
|
1927
|
+
return LOG_LEVEL_PRIORITY.indexOf(configured) > LOG_LEVEL_PRIORITY.indexOf(this.options.level) ? configured : this.options.level;
|
|
1928
|
+
}
|
|
1713
1929
|
/**
|
|
1714
1930
|
* Report one destination init failure as a structured
|
|
1715
1931
|
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
@@ -1718,10 +1934,13 @@ var DestinationRegistry = class {
|
|
|
1718
1934
|
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1719
1935
|
* which stage failed — and the two cannot drift apart.
|
|
1720
1936
|
*
|
|
1721
|
-
* @param
|
|
1937
|
+
* @param destination - The failing destination. Passed whole, so its `name` is
|
|
1938
|
+
* read under `safeDestinationName` rather than at the call site — which sits
|
|
1939
|
+
* inside the catch, where a throwing getter would abort bootstrap.
|
|
1722
1940
|
* @param cause - The thrown or rejected value.
|
|
1723
1941
|
*/
|
|
1724
|
-
reportInitFailure(
|
|
1942
|
+
reportInitFailure(destination, cause) {
|
|
1943
|
+
const name = safeDestinationName(destination);
|
|
1725
1944
|
reportDestinationFailure(
|
|
1726
1945
|
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1727
1946
|
name,
|
|
@@ -1729,6 +1948,30 @@ var DestinationRegistry = class {
|
|
|
1729
1948
|
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1730
1949
|
);
|
|
1731
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Report a failure from the readiness hook, with wording that matches the
|
|
1953
|
+
* destination's actual state.
|
|
1954
|
+
*
|
|
1955
|
+
* The loop calls every registered destination, healthy and failed alike, so one
|
|
1956
|
+
* message cannot be true for both. A healthy one keeps receiving entries and was
|
|
1957
|
+
* never an init failure; a failed one was already dropped from the fan-out and
|
|
1958
|
+
* saying it "remains active" would contradict the report it just got. The key
|
|
1959
|
+
* stays `LOGGER_DESTINATION_INIT_FAILED` either way so an operator greps one
|
|
1960
|
+
* thing across every destination-lifecycle problem.
|
|
1961
|
+
*
|
|
1962
|
+
* @param destination - The destination whose hook threw.
|
|
1963
|
+
* @param cause - The thrown value.
|
|
1964
|
+
*/
|
|
1965
|
+
reportHookFailure(destination, cause) {
|
|
1966
|
+
const stillActive = !this.health.isFailed(destination);
|
|
1967
|
+
const name = safeDestinationName(destination);
|
|
1968
|
+
reportDestinationFailure(
|
|
1969
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1970
|
+
name,
|
|
1971
|
+
cause,
|
|
1972
|
+
`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.")
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1732
1975
|
/**
|
|
1733
1976
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1734
1977
|
* initialized.
|
|
@@ -1785,14 +2028,50 @@ var DestinationRegistry = class {
|
|
|
1785
2028
|
try {
|
|
1786
2029
|
await destination.onShutdown?.();
|
|
1787
2030
|
} catch (cause) {
|
|
1788
|
-
|
|
1789
|
-
writeStderrSafely(
|
|
1790
|
-
`[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
|
|
1791
|
-
`
|
|
1792
|
-
);
|
|
2031
|
+
this.reportShutdownFailure(destination, cause);
|
|
1793
2032
|
}
|
|
1794
2033
|
}
|
|
1795
2034
|
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Report a failing `onShutdown` on stderr without letting the report itself
|
|
2037
|
+
* abort the teardown of the destinations still queued behind it.
|
|
2038
|
+
*
|
|
2039
|
+
* The coercion is INSIDE the guard, not above it. Reading `stack`/`message` on
|
|
2040
|
+
* an `Error` with hostile getters, and `String(cause)` on a value with a
|
|
2041
|
+
* throwing `toString`/`Symbol.toPrimitive`, both throw — and this runs inside
|
|
2042
|
+
* the `catch` that exists to keep one bad sink from mattering, so an escape
|
|
2043
|
+
* here would propagate out of `onApplicationShutdown` and skip every remaining
|
|
2044
|
+
* destination's flush. The never-throw contract has to cover READING the value,
|
|
2045
|
+
* not just writing it, which is the same correction `reportDestinationFailure`
|
|
2046
|
+
* already carries.
|
|
2047
|
+
*
|
|
2048
|
+
* The text is escaped for the same reason every other terminal-bound path is:
|
|
2049
|
+
* a stack is legitimately multi-line, so `escapeControlCharacters` keeps its
|
|
2050
|
+
* newlines and neutralizes everything else that can drive a terminal.
|
|
2051
|
+
*
|
|
2052
|
+
* The DESTINATION is passed rather than its name, so that read happens in here
|
|
2053
|
+
* too. `name` is declared `readonly name: string` but nothing stops a consumer
|
|
2054
|
+
* implementing it as a getter, and reading it at the call site would put it back
|
|
2055
|
+
* outside the guard — inside the `catch`, where a throw has the same cost.
|
|
2056
|
+
*
|
|
2057
|
+
* Two separate guards, not one: a name that cannot be read must not also cost
|
|
2058
|
+
* the failure detail, and a detail that cannot be read must not cost the name.
|
|
2059
|
+
* Either alone still identifies which sink failed.
|
|
2060
|
+
*
|
|
2061
|
+
* @param destination - The destination whose `onShutdown` threw.
|
|
2062
|
+
* @param cause - The thrown value, of any shape.
|
|
2063
|
+
*/
|
|
2064
|
+
reportShutdownFailure(destination, cause) {
|
|
2065
|
+
const name = toSingleLineMessage(safeDestinationName(destination));
|
|
2066
|
+
let detail = "UnknownError";
|
|
2067
|
+
try {
|
|
2068
|
+
const stack = isErrorLike(cause) ? cause.stack : void 0;
|
|
2069
|
+
detail = stack === void 0 ? toSingleLineMessage(isErrorLike(cause) ? String(cause.message) : String(cause)) : escapeControlCharacters(String(stack));
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
writeStderrSafely(`[DestinationRegistry] Shutdown failed for "${name}": ${detail}
|
|
2073
|
+
`);
|
|
2074
|
+
}
|
|
1796
2075
|
/**
|
|
1797
2076
|
* The destinations that initialized successfully.
|
|
1798
2077
|
*
|
|
@@ -2143,9 +2422,6 @@ var PrettyDevDestination = class {
|
|
|
2143
2422
|
});
|
|
2144
2423
|
} catch {
|
|
2145
2424
|
this.initFailed = true;
|
|
2146
|
-
this.flushBuffer((payload) => {
|
|
2147
|
-
writeRawToStdout(payload);
|
|
2148
|
-
});
|
|
2149
2425
|
throw new Error(
|
|
2150
2426
|
"[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
|
|
2151
2427
|
);
|
|
@@ -2196,6 +2472,41 @@ var PrettyDevDestination = class {
|
|
|
2196
2472
|
this.buffer.push(payload);
|
|
2197
2473
|
this.bufferedBytes += payloadBytes;
|
|
2198
2474
|
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Resolve anything still held, now that the registry knows what became of it.
|
|
2477
|
+
*
|
|
2478
|
+
* **Discard only where another sink is believed to have taken the same entries;
|
|
2479
|
+
* emit everything else.** Losing a boot entry is not an acceptable outcome, and
|
|
2480
|
+
* every richer policy tried here had a losing branch: trusting "a sink survived"
|
|
2481
|
+
* lost entries to a sink at a higher level, trusting a level lost them to a sink
|
|
2482
|
+
* that was itself the asker, then to one whose writes were throwing, then to one
|
|
2483
|
+
* whose write had not settled yet. Each of those is now folded into the single
|
|
2484
|
+
* signal handed in — and where it is uncertain, it says `false` and this drains.
|
|
2485
|
+
*
|
|
2486
|
+
* That signal is not a proof, and this hook does not treat it as one. It cannot
|
|
2487
|
+
* see an entry still queued inside the `Writable` adapter, because the adapter
|
|
2488
|
+
* has not called `destination.write()` for it yet. Discarding on it is a bet
|
|
2489
|
+
* that a duplicated boot line costs less than a lost one — which is the trade
|
|
2490
|
+
* this destination accepts, and which a destination that cannot lose anything
|
|
2491
|
+
* should decline by ignoring the flag and always emitting.
|
|
2492
|
+
*
|
|
2493
|
+
* The cost is stated rather than hidden: when nothing survived, every holding
|
|
2494
|
+
* destination drains, so N of them produce N copies of the boot entries. A
|
|
2495
|
+
* duplicated line in an already-degraded boot is a smaller harm than a lost one,
|
|
2496
|
+
* and it is loud rather than silent.
|
|
2497
|
+
*
|
|
2498
|
+
* A no-op once {@link onInit} succeeded — the buffer was flushed through the
|
|
2499
|
+
* transform there and is already empty.
|
|
2500
|
+
*
|
|
2501
|
+
* @param status.heldEntriesDeliveredElsewhere - Another live sink appears to
|
|
2502
|
+
* have taken everything this one took. A deduplication signal, not a proof —
|
|
2503
|
+
* see `ILogDestination.onRegistryReady` for what it cannot see.
|
|
2504
|
+
*/
|
|
2505
|
+
onRegistryReady(status) {
|
|
2506
|
+
this.flushBuffer(
|
|
2507
|
+
status.heldEntriesDeliveredElsewhere ? () => void 0 : writeRawToStdout
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2199
2510
|
/**
|
|
2200
2511
|
* Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
|
|
2201
2512
|
*
|