@bymax-one/nest-logger 1.2.2 → 1.2.3
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 +56 -1
- package/README.md +32 -18
- package/dist/server/index.cjs +177 -31
- package/dist/server/index.d.cts +18 -9
- package/dist/server/index.d.ts +18 -9
- package/dist/server/index.mjs +177 -31
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,60 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.3] - 2026-08-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A destination that fails to initialize no longer silences the entire application.** Reported
|
|
19
|
+
by a consumer while wiring pretty local output, and reproduced end to end: `destinations`
|
|
20
|
+
**replaces** the default stdout sink, so `destinations: [new PrettyDevDestination()]` without the
|
|
21
|
+
optional `pino-pretty` installed left the app booting, running, exiting `0` and writing **not one
|
|
22
|
+
byte to stdout or stderr** — including the `LOGGER_DESTINATION_INIT_FAILED` entry that exists to
|
|
23
|
+
explain exactly this, which was delivered into the dead sink. Not a crash and not a degraded
|
|
24
|
+
mode: silence that looks like a quiet service.
|
|
25
|
+
|
|
26
|
+
Four independently reasonable behaviours composed into it — `destinations` replacing rather than
|
|
27
|
+
adding, the failure being reported through `this.logger`, the multistream being built from the
|
|
28
|
+
full registered list (it is wired while NestJS builds the DI graph, before any `onInit` runs), and
|
|
29
|
+
a failed sink dropping its writes. Three changes break the chain:
|
|
30
|
+
|
|
31
|
+
- the init failure is written **directly to `process.stderr`**, matching what the shutdown path
|
|
32
|
+
and the write path already do, and for the same reason — reporting a broken sink through the
|
|
33
|
+
sink set is a feedback loop;
|
|
34
|
+
- a destination whose `onInit` rejected is **excluded from the write fan-out**;
|
|
35
|
+
- when **no** destination initializes, entries **fall back to raw NDJSON on stdout**, so a
|
|
36
|
+
misconfiguration degrades visibly instead of going quiet. The fallback is elected before the
|
|
37
|
+
bootstrap entries are announced, so `LOGGER_BOOTSTRAP_WARNING` — the signal that PII redaction
|
|
38
|
+
was disabled — survives the failure it would otherwise have been lost to.
|
|
39
|
+
|
|
40
|
+
The defect belonged to `destinations` as a general contract, not to `PrettyDevDestination`: any
|
|
41
|
+
consumer-written sink that failed `onInit` as the only registered destination did the same thing.
|
|
42
|
+
`pino-pretty` remains an optional peer and nothing was added to `dependencies`, which stays `{}`.
|
|
43
|
+
|
|
44
|
+
### Breaking
|
|
45
|
+
|
|
46
|
+
- **`isPretty` is removed from `BymaxLoggerModuleOptions`.** It has been `@deprecated` and **inert**
|
|
47
|
+
— a `true` value never changed any output on its own. An option that does nothing is worse than
|
|
48
|
+
an absent one, because it is a knob a consumer turns and then reasons from.
|
|
49
|
+
|
|
50
|
+
**Migration:** delete the option. Nothing about your output changes, because nothing about your
|
|
51
|
+
output depended on it. For pretty local logs, add `new PrettyDevDestination()` to `destinations`
|
|
52
|
+
and install the optional `pino-pretty` peer — which is what the option's own documentation had
|
|
53
|
+
been telling you to do.
|
|
54
|
+
|
|
55
|
+
Type-level only: `applyDefaults` no longer emits the field, and no runtime behaviour is affected.
|
|
56
|
+
|
|
57
|
+
### Documentation
|
|
58
|
+
|
|
59
|
+
- **`destinations` documents that it REPLACES the default stdout sink**, rather than claiming to add
|
|
60
|
+
"custom destinations beyond `DefaultStdoutDestination`". Replacement is the correct behaviour — a
|
|
61
|
+
file-only or socket-only deployment must be able to turn stdout off — but the doc described the
|
|
62
|
+
opposite, and it is the assumption that makes the failure above easy to arrive at.
|
|
63
|
+
- **`PrettyDevDestination` no longer implies it follows Pino's recommended transport form.** It runs
|
|
64
|
+
`pino-pretty` as a main-thread transform, a documented pino-pretty API; Pino's own docs recommend
|
|
65
|
+
`transport: { target: 'pino-pretty' }` in a worker, which is unreachable here because this library
|
|
66
|
+
owns the Pino instance and fans out through `pino.multistream`.
|
|
67
|
+
|
|
14
68
|
## [1.2.2] - 2026-08-13
|
|
15
69
|
|
|
16
70
|
### Added
|
|
@@ -814,7 +868,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
814
868
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
815
869
|
`release.yml`, Dependabot, and issue templates
|
|
816
870
|
|
|
817
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
871
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...HEAD
|
|
872
|
+
[1.2.3]: https://github.com/bymaxone/nest-logger/compare/v1.2.2...v1.2.3
|
|
818
873
|
[1.2.2]: https://github.com/bymaxone/nest-logger/compare/v1.2.1...v1.2.2
|
|
819
874
|
[1.2.1]: https://github.com/bymaxone/nest-logger/compare/v1.2.0...v1.2.1
|
|
820
875
|
[1.2.0]: https://github.com/bymaxone/nest-logger/compare/v1.1.0...v1.2.0
|
package/README.md
CHANGED
|
@@ -546,24 +546,23 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
|
|
|
546
546
|
|
|
547
547
|
### Top-level options
|
|
548
548
|
|
|
549
|
-
| Option | Type | Default
|
|
550
|
-
| ---------------------------- | ------------------------------- |
|
|
551
|
-
| `service.name` | `string` | **Required**
|
|
552
|
-
| `service.version` | `string` | **Required**
|
|
553
|
-
| `service.namespace` | `string` | —
|
|
554
|
-
| `service.instanceId` | `string` | —
|
|
555
|
-
| `service.environment` | `string` | `NODE_ENV`
|
|
556
|
-
| `resourceFormat` | `'nested' \| 'flat'` | `'nested'`
|
|
557
|
-
| `eventNameField` | `string \| false` | `'event.name'`
|
|
558
|
-
| `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'`
|
|
559
|
-
| `level` | `LogLevel` | `'info'`
|
|
560
|
-
| `
|
|
561
|
-
| `
|
|
562
|
-
| `
|
|
563
|
-
| `
|
|
564
|
-
| `
|
|
565
|
-
| `
|
|
566
|
-
| `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
|
|
549
|
+
| Option | Type | Default | Description |
|
|
550
|
+
| ---------------------------- | ------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
551
|
+
| `service.name` | `string` | **Required** | Service name emitted in every log entry |
|
|
552
|
+
| `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
|
|
553
|
+
| `service.namespace` | `string` | — | OTel `service.namespace` (**Stable**) — the group the service belongs to |
|
|
554
|
+
| `service.instanceId` | `string` | — | OTel `service.instance.id` (**Stable**). **Never generated** — supply it from the platform. See [Resource identity](#6b-resource-identity) |
|
|
555
|
+
| `service.environment` | `string` | `NODE_ENV` | OTel `deployment.environment.name` (**Stable**). The deprecated `deployment.environment` is never emitted |
|
|
556
|
+
| `resourceFormat` | `'nested' \| 'flat'` | `'nested'` | Shape of the identity fields. `'flat'` emits the dotted attribute names verbatim |
|
|
557
|
+
| `eventNameField` | `string \| false` | `'event.name'` | Field carrying the derived event name (`PAYMENT_FAILED` → `payment.failed`). `false` disables it |
|
|
558
|
+
| `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'` | `'both'` adds `exception.*` and `error.type` beside `err.*`; `'semconv'` replaces them |
|
|
559
|
+
| `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
|
|
560
|
+
| `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths, applied on top of the default coverage |
|
|
561
|
+
| `redactStrategy` | `'names' \| 'paths'` | `'names'` | Engine behind the DEFAULT set. `'paths'` restores the pre-1.2 `fast-redact` expansion (four-level ceiling, ~100× slower) |
|
|
562
|
+
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the default PII coverage entirely. ⚠️ Emits `LOGGER_BOOTSTRAP_WARNING` at startup — document why |
|
|
563
|
+
| `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
|
|
564
|
+
| `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
|
|
565
|
+
| `destinations` | `ILogDestination[]` | `[new DefaultStdoutDestination()]` | The sinks entries are written to. ⚠️ A non-empty list **replaces** that default — see [Destinations](#destinations-replace-stdout) |
|
|
567
566
|
|
|
568
567
|
### `http` options
|
|
569
568
|
|
|
@@ -713,6 +712,21 @@ BymaxLoggerModule.forRootAsync({
|
|
|
713
712
|
})
|
|
714
713
|
```
|
|
715
714
|
|
|
715
|
+
### Destinations replace stdout
|
|
716
|
+
|
|
717
|
+
A non-empty `destinations` **replaces** `DefaultStdoutDestination` — it does not add to it. That is deliberate: a file-only or socket-only deployment has to be able to turn structured stdout off. To keep stdout alongside a custom sink, list it explicitly:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
destinations: [new DefaultStdoutDestination(), new LokiDestination({ ... })]
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
The consequence worth knowing: a sink you supply may be the **only** one the application has, so its failure is the application's silence. Two guarantees cover that, and neither requires anything from you:
|
|
724
|
+
|
|
725
|
+
- **A destination that fails `onInit` is reported as `LOGGER_DESTINATION_INIT_FAILED` on `stderr`** — not through the logger, whose sinks are the ones that just failed — and receives no entries.
|
|
726
|
+
- **If _every_ destination fails to initialize, entries fall back to raw NDJSON on `stdout`.** Degraded and ugly, but visible; nothing is lost, including the bootstrap entries.
|
|
727
|
+
|
|
728
|
+
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.
|
|
729
|
+
|
|
716
730
|
### Postgres destination (Prisma)
|
|
717
731
|
|
|
718
732
|
```typescript
|
package/dist/server/index.cjs
CHANGED
|
@@ -76,7 +76,6 @@ function applyDefaults(options) {
|
|
|
76
76
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
77
77
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
78
78
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
79
|
-
isPretty: options.isPretty ?? !isProduction,
|
|
80
79
|
http: Object.freeze({
|
|
81
80
|
...http,
|
|
82
81
|
excludePaths: Object.freeze([...http.excludePaths])
|
|
@@ -1021,22 +1020,43 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
|
1021
1020
|
const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
|
|
1022
1021
|
return Array.from(new Set(sources.flat()));
|
|
1023
1022
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1023
|
+
|
|
1024
|
+
// src/server/utils/report-destination-failure.util.ts
|
|
1025
|
+
function reportDestinationFailure(logKey, name, cause, msg) {
|
|
1026
1026
|
try {
|
|
1027
|
+
const err = cause instanceof Error ? {
|
|
1028
|
+
type: toSingleLineMessage(String(cause.name)),
|
|
1029
|
+
message: toSingleLineMessage(String(cause.message))
|
|
1030
|
+
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1027
1031
|
process.stderr.write(
|
|
1028
1032
|
JSON.stringify({
|
|
1029
1033
|
level: "error",
|
|
1030
|
-
logKey
|
|
1031
|
-
destination: name,
|
|
1034
|
+
logKey,
|
|
1035
|
+
destination: toSingleLineMessage(name),
|
|
1032
1036
|
err,
|
|
1033
|
-
msg:
|
|
1037
|
+
msg: toSingleLineMessage(msg)
|
|
1034
1038
|
}) + "\n"
|
|
1035
1039
|
);
|
|
1036
1040
|
} catch {
|
|
1037
1041
|
}
|
|
1038
1042
|
}
|
|
1039
|
-
|
|
1043
|
+
|
|
1044
|
+
// src/server/utils/destination-to-stream.ts
|
|
1045
|
+
function reportWriteFailure(name, cause) {
|
|
1046
|
+
reportDestinationFailure(
|
|
1047
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1048
|
+
name,
|
|
1049
|
+
cause,
|
|
1050
|
+
`Log destination "${name}" failed to write; the entry was dropped`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
function rescueToStdout(payload) {
|
|
1054
|
+
try {
|
|
1055
|
+
process.stdout.write(payload);
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function destinationToStream(destination, health) {
|
|
1040
1060
|
return new stream.Writable({
|
|
1041
1061
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
1042
1062
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
@@ -1050,6 +1070,13 @@ function destinationToStream(destination) {
|
|
|
1050
1070
|
};
|
|
1051
1071
|
try {
|
|
1052
1072
|
const payload = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1073
|
+
if (health.isFailed(destination)) {
|
|
1074
|
+
if (health.shouldRescue(destination)) {
|
|
1075
|
+
rescueToStdout(payload);
|
|
1076
|
+
}
|
|
1077
|
+
callback();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1053
1080
|
const result = destination.write(payload);
|
|
1054
1081
|
if (result instanceof Promise) {
|
|
1055
1082
|
result.then(() => callback(), onFailure);
|
|
@@ -1439,7 +1466,7 @@ function resolveRedactOption(options) {
|
|
|
1439
1466
|
}
|
|
1440
1467
|
return { paths, censor: options.redactCensor };
|
|
1441
1468
|
}
|
|
1442
|
-
function buildPinoInstance(options, logContext, destinations) {
|
|
1469
|
+
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1443
1470
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1444
1471
|
const redact = resolveNameRedactor(options);
|
|
1445
1472
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
@@ -1511,55 +1538,162 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1511
1538
|
};
|
|
1512
1539
|
const streams = destinations.map((destination) => ({
|
|
1513
1540
|
level: destination.minLevel ?? options.level,
|
|
1514
|
-
stream: destinationToStream(destination)
|
|
1541
|
+
stream: destinationToStream(destination, health)
|
|
1515
1542
|
}));
|
|
1516
1543
|
return pino__default.default(pinoOptions, pino__default.default.multistream(streams));
|
|
1517
1544
|
}
|
|
1545
|
+
var DestinationHealth = class {
|
|
1546
|
+
constructor() {
|
|
1547
|
+
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1548
|
+
this.failed = /* @__PURE__ */ new Set();
|
|
1549
|
+
/**
|
|
1550
|
+
* Whether ANY destination initialized successfully.
|
|
1551
|
+
*
|
|
1552
|
+
* A boolean rather than a count, deliberately: the only question ever asked of
|
|
1553
|
+
* it is "is there somewhere to write?". Mutation testing made the point — a
|
|
1554
|
+
* counter incremented with `+=` survived being flipped to `-=`, because
|
|
1555
|
+
* `count === 0` means the same thing either way. A value whose arithmetic
|
|
1556
|
+
* cannot be observed was not a counter, it was a flag wearing a number.
|
|
1557
|
+
*/
|
|
1558
|
+
this.hasHealthy = false;
|
|
1559
|
+
/**
|
|
1560
|
+
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1561
|
+
* every real level so the first `markFailed` always wins the comparison.
|
|
1562
|
+
*/
|
|
1563
|
+
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1564
|
+
}
|
|
1565
|
+
/** Record that a destination initialized successfully. */
|
|
1566
|
+
markHealthy() {
|
|
1567
|
+
this.hasHealthy = true;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
1571
|
+
*
|
|
1572
|
+
* The rescuer is the failed destination with the LOWEST effective level, ties
|
|
1573
|
+
* broken by registration order (the comparison is strict). Registration order
|
|
1574
|
+
* alone is deterministic but arbitrary: a `warn` sink registered before an
|
|
1575
|
+
* `info` one would rescue at `warn` and silently drop the `info` entries the
|
|
1576
|
+
* second was configured to receive, and swapping the array would change the
|
|
1577
|
+
* output for the same configuration. Electing by level rescues the UNION of
|
|
1578
|
+
* what the consumer asked to see.
|
|
1579
|
+
*
|
|
1580
|
+
* Inheriting a level filter at all is deliberate — `minLevel` is the consumer's
|
|
1581
|
+
* own instruction, so the rescue delivers what a working sink would have, never
|
|
1582
|
+
* more. The one entry that must never be filtered, the init failure itself, goes
|
|
1583
|
+
* straight to stderr, outside the multistream.
|
|
1584
|
+
*
|
|
1585
|
+
* @param destination - The destination whose `onInit()` rejected.
|
|
1586
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1587
|
+
* the module-wide `level`.
|
|
1588
|
+
*/
|
|
1589
|
+
markFailed(destination, effectiveLevel) {
|
|
1590
|
+
this.failed.add(destination);
|
|
1591
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1592
|
+
if (level < this.rescuerLevel) {
|
|
1593
|
+
this.rescuer = destination;
|
|
1594
|
+
this.rescuerLevel = level;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Whether a destination failed to initialize and must not receive writes.
|
|
1599
|
+
*
|
|
1600
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1601
|
+
* @returns `true` when its `onInit()` rejected.
|
|
1602
|
+
*/
|
|
1603
|
+
isFailed(destination) {
|
|
1604
|
+
return this.failed.has(destination);
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Whether this destination is the one that must write the entry as raw NDJSON
|
|
1608
|
+
* to stdout because NOTHING initialized.
|
|
1609
|
+
*
|
|
1610
|
+
* Exactly one failed destination can satisfy this, so N failed sinks rescue an
|
|
1611
|
+
* entry once rather than N times.
|
|
1612
|
+
*
|
|
1613
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1614
|
+
* @returns `true` when no destination initialized and this one is the elected
|
|
1615
|
+
* rescuer.
|
|
1616
|
+
*/
|
|
1617
|
+
shouldRescue(destination) {
|
|
1618
|
+
return !this.hasHealthy && this.rescuer === destination;
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
DestinationHealth = __decorateClass([
|
|
1622
|
+
common.Injectable()
|
|
1623
|
+
], DestinationHealth);
|
|
1518
1624
|
var DestinationRegistry = class {
|
|
1519
1625
|
/**
|
|
1520
1626
|
* @param registered - Every destination declared under
|
|
1521
1627
|
* `LOGGER_DESTINATIONS_TOKEN`. Injected by token explicitly because the
|
|
1522
1628
|
* package is bundled without `emitDecoratorMetadata`.
|
|
1523
|
-
* @param logger - Structured logger used
|
|
1524
|
-
* token explicitly for the same bundling reason.
|
|
1629
|
+
* @param logger - Structured logger used for the bootstrap entries. Injected by
|
|
1630
|
+
* token explicitly for the same bundling reason. Deliberately NOT used to
|
|
1631
|
+
* report init failures — see the file-level note.
|
|
1632
|
+
* @param options - Resolved module options, read for the bootstrap warnings and
|
|
1633
|
+
* for each destination's effective level.
|
|
1634
|
+
* @param health - Init-health record shared with the write fan-out.
|
|
1525
1635
|
*/
|
|
1526
|
-
constructor(registered, logger, options) {
|
|
1636
|
+
constructor(registered, logger, options, health) {
|
|
1527
1637
|
this.registered = registered;
|
|
1528
1638
|
this.logger = logger;
|
|
1529
1639
|
this.options = options;
|
|
1640
|
+
this.health = health;
|
|
1530
1641
|
/**
|
|
1531
1642
|
* Destinations that initialized successfully, in registration order.
|
|
1532
1643
|
*
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* write fan-out; a sink that failed `onInit` still receives writes, contained
|
|
1538
|
-
* fail-soft by the wrapper.
|
|
1644
|
+
* Authoritative for BOTH shutdown (reverse order) and — via the shared
|
|
1645
|
+
* `DestinationHealth` record written alongside it — the write fan-out. The two
|
|
1646
|
+
* are kept in step here because the multistream itself is wired before any
|
|
1647
|
+
* `onInit` runs and cannot be rebuilt afterwards; see `DestinationHealth`.
|
|
1539
1648
|
*/
|
|
1540
1649
|
this.active = [];
|
|
1541
1650
|
}
|
|
1542
1651
|
/**
|
|
1543
1652
|
* Initialize every registered destination. A destination whose `onInit`
|
|
1544
|
-
* rejects is skipped (not added to the active set)
|
|
1545
|
-
*
|
|
1653
|
+
* rejects is skipped (not added to the active set), marked failed so the
|
|
1654
|
+
* fan-out stops writing to it, and reported to stderr — it must not block
|
|
1655
|
+
* application bootstrap.
|
|
1656
|
+
*
|
|
1657
|
+
* The loop completes BEFORE {@link announceBootstrap}, and that ordering is
|
|
1658
|
+
* load-bearing rather than incidental: the health record is what lets a
|
|
1659
|
+
* last-resort sink carry the bootstrap entries when every destination failed.
|
|
1660
|
+
* Announcing first would emit them into a fan-out that has not yet been told
|
|
1661
|
+
* anything is wrong, and `LOGGER_BOOTSTRAP_WARNING` — the signal that exists so
|
|
1662
|
+
* a security review can see PII redaction was disabled — would be lost exactly
|
|
1663
|
+
* when the configuration is already known to be broken.
|
|
1546
1664
|
*/
|
|
1547
1665
|
async onModuleInit() {
|
|
1548
1666
|
for (const destination of this.registered) {
|
|
1549
1667
|
try {
|
|
1550
1668
|
await destination.onInit?.();
|
|
1551
1669
|
this.active.push(destination);
|
|
1670
|
+
this.health.markHealthy();
|
|
1552
1671
|
} catch (cause) {
|
|
1553
|
-
this.
|
|
1554
|
-
|
|
1555
|
-
cause instanceof Error ? cause : new Error(String(cause)),
|
|
1556
|
-
void 0,
|
|
1557
|
-
{ destination: destination.name }
|
|
1558
|
-
);
|
|
1672
|
+
this.health.markFailed(destination, destination.minLevel ?? this.options.level);
|
|
1673
|
+
this.reportInitFailure(destination.name, cause);
|
|
1559
1674
|
}
|
|
1560
1675
|
}
|
|
1561
1676
|
this.announceBootstrap();
|
|
1562
1677
|
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Report one destination init failure as a structured
|
|
1680
|
+
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
1681
|
+
*
|
|
1682
|
+
* Shares {@link reportDestinationFailure} with the write-failure path rather
|
|
1683
|
+
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1684
|
+
* which stage failed — and the two cannot drift apart.
|
|
1685
|
+
*
|
|
1686
|
+
* @param name - The failing destination's name.
|
|
1687
|
+
* @param cause - The thrown or rejected value.
|
|
1688
|
+
*/
|
|
1689
|
+
reportInitFailure(name, cause) {
|
|
1690
|
+
reportDestinationFailure(
|
|
1691
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1692
|
+
name,
|
|
1693
|
+
cause,
|
|
1694
|
+
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1563
1697
|
/**
|
|
1564
1698
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1565
1699
|
* initialized.
|
|
@@ -1637,7 +1771,8 @@ DestinationRegistry = __decorateClass([
|
|
|
1637
1771
|
common.Injectable(),
|
|
1638
1772
|
__decorateParam(0, common.Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1639
1773
|
__decorateParam(1, common.Inject(exports.PinoLoggerService)),
|
|
1640
|
-
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
|
|
1774
|
+
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN)),
|
|
1775
|
+
__decorateParam(3, common.Inject(DestinationHealth))
|
|
1641
1776
|
], DestinationRegistry);
|
|
1642
1777
|
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1643
1778
|
exports.LogContextService = class LogContextService {
|
|
@@ -1732,6 +1867,10 @@ function buildCommonProviders() {
|
|
|
1732
1867
|
return [
|
|
1733
1868
|
exports.LogContextService,
|
|
1734
1869
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: exports.LogContextService },
|
|
1870
|
+
// Shared by the Pino factory (which closes over it in every fan-out stream)
|
|
1871
|
+
// and by DestinationRegistry (which writes to it during onModuleInit). Both
|
|
1872
|
+
// depend on this provider and it depends on nothing, so there is no cycle.
|
|
1873
|
+
DestinationHealth,
|
|
1735
1874
|
{
|
|
1736
1875
|
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1737
1876
|
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
@@ -1742,8 +1881,13 @@ function buildCommonProviders() {
|
|
|
1742
1881
|
},
|
|
1743
1882
|
{
|
|
1744
1883
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1745
|
-
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
1746
|
-
inject: [
|
|
1884
|
+
useFactory: (options, logContext, destinations, health) => buildPinoInstance(options, logContext, destinations, health),
|
|
1885
|
+
inject: [
|
|
1886
|
+
LOGGER_OPTIONS_TOKEN,
|
|
1887
|
+
exports.LogContextService,
|
|
1888
|
+
LOGGER_DESTINATIONS_TOKEN,
|
|
1889
|
+
DestinationHealth
|
|
1890
|
+
]
|
|
1747
1891
|
},
|
|
1748
1892
|
{
|
|
1749
1893
|
provide: LOGGER_DESTINATIONS_TOKEN,
|
|
@@ -1934,8 +2078,10 @@ var PrettyDevDestination = class {
|
|
|
1934
2078
|
*
|
|
1935
2079
|
* Three states:
|
|
1936
2080
|
* - initialized → forward to the pino-pretty transform;
|
|
1937
|
-
* - init failed → DROP
|
|
1938
|
-
*
|
|
2081
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
2082
|
+
* fan-out already skips a failed destination before reaching this method
|
|
2083
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2084
|
+
* belt to that, for any caller holding the destination directly;
|
|
1939
2085
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
1940
2086
|
* → fall back to raw stdout so the entry is not lost.
|
|
1941
2087
|
*
|
package/dist/server/index.d.cts
CHANGED
|
@@ -334,15 +334,22 @@ interface BymaxLoggerModuleOptions {
|
|
|
334
334
|
redactCensor?: string;
|
|
335
335
|
/** Disable default redact paths (use with caution). Default: false. */
|
|
336
336
|
shouldDisableDefaultRedact?: boolean;
|
|
337
|
-
/** Custom destinations beyond `DefaultStdoutDestination`. */
|
|
338
|
-
destinations?: readonly ILogDestination[];
|
|
339
337
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
338
|
+
* The sinks every entry is written to.
|
|
339
|
+
*
|
|
340
|
+
* A non-empty list **REPLACES** `DefaultStdoutDestination` — it does not add to
|
|
341
|
+
* it. That is deliberate: a file-only or socket-only deployment has to be able
|
|
342
|
+
* to turn stdout off. It also means a sink supplied here may be the only one
|
|
343
|
+
* the application has, so a destination that fails `onInit` is reported on
|
|
344
|
+
* stderr and, if nothing else initialized, entries fall back to raw NDJSON on
|
|
345
|
+
* stdout rather than disappearing.
|
|
346
|
+
*
|
|
347
|
+
* Include `new DefaultStdoutDestination()` explicitly to keep structured stdout
|
|
348
|
+
* alongside a custom sink.
|
|
349
|
+
*
|
|
350
|
+
* Default: `[new DefaultStdoutDestination()]`.
|
|
344
351
|
*/
|
|
345
|
-
|
|
352
|
+
destinations?: readonly ILogDestination[];
|
|
346
353
|
/** HTTP module configuration. */
|
|
347
354
|
http?: HttpOptions;
|
|
348
355
|
/** OpenTelemetry integration tuning. */
|
|
@@ -910,8 +917,10 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
910
917
|
*
|
|
911
918
|
* Three states:
|
|
912
919
|
* - initialized → forward to the pino-pretty transform;
|
|
913
|
-
* - init failed → DROP
|
|
914
|
-
*
|
|
920
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
921
|
+
* fan-out already skips a failed destination before reaching this method
|
|
922
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
|
+
* belt to that, for any caller holding the destination directly;
|
|
915
924
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
916
925
|
* → fall back to raw stdout so the entry is not lost.
|
|
917
926
|
*
|
package/dist/server/index.d.ts
CHANGED
|
@@ -334,15 +334,22 @@ interface BymaxLoggerModuleOptions {
|
|
|
334
334
|
redactCensor?: string;
|
|
335
335
|
/** Disable default redact paths (use with caution). Default: false. */
|
|
336
336
|
shouldDisableDefaultRedact?: boolean;
|
|
337
|
-
/** Custom destinations beyond `DefaultStdoutDestination`. */
|
|
338
|
-
destinations?: readonly ILogDestination[];
|
|
339
337
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
338
|
+
* The sinks every entry is written to.
|
|
339
|
+
*
|
|
340
|
+
* A non-empty list **REPLACES** `DefaultStdoutDestination` — it does not add to
|
|
341
|
+
* it. That is deliberate: a file-only or socket-only deployment has to be able
|
|
342
|
+
* to turn stdout off. It also means a sink supplied here may be the only one
|
|
343
|
+
* the application has, so a destination that fails `onInit` is reported on
|
|
344
|
+
* stderr and, if nothing else initialized, entries fall back to raw NDJSON on
|
|
345
|
+
* stdout rather than disappearing.
|
|
346
|
+
*
|
|
347
|
+
* Include `new DefaultStdoutDestination()` explicitly to keep structured stdout
|
|
348
|
+
* alongside a custom sink.
|
|
349
|
+
*
|
|
350
|
+
* Default: `[new DefaultStdoutDestination()]`.
|
|
344
351
|
*/
|
|
345
|
-
|
|
352
|
+
destinations?: readonly ILogDestination[];
|
|
346
353
|
/** HTTP module configuration. */
|
|
347
354
|
http?: HttpOptions;
|
|
348
355
|
/** OpenTelemetry integration tuning. */
|
|
@@ -910,8 +917,10 @@ declare class PrettyDevDestination implements ILogDestination {
|
|
|
910
917
|
*
|
|
911
918
|
* Three states:
|
|
912
919
|
* - initialized → forward to the pino-pretty transform;
|
|
913
|
-
* - init failed → DROP
|
|
914
|
-
*
|
|
920
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
921
|
+
* fan-out already skips a failed destination before reaching this method
|
|
922
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
923
|
+
* belt to that, for any caller holding the destination directly;
|
|
915
924
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
916
925
|
* → fall back to raw stdout so the entry is not lost.
|
|
917
926
|
*
|
package/dist/server/index.mjs
CHANGED
|
@@ -73,7 +73,6 @@ function applyDefaults(options) {
|
|
|
73
73
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
74
74
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
75
75
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
76
|
-
isPretty: options.isPretty ?? !isProduction,
|
|
77
76
|
http: Object.freeze({
|
|
78
77
|
...http,
|
|
79
78
|
excludePaths: Object.freeze([...http.excludePaths])
|
|
@@ -1018,22 +1017,43 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
|
1018
1017
|
const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
|
|
1019
1018
|
return Array.from(new Set(sources.flat()));
|
|
1020
1019
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1020
|
+
|
|
1021
|
+
// src/server/utils/report-destination-failure.util.ts
|
|
1022
|
+
function reportDestinationFailure(logKey, name, cause, msg) {
|
|
1023
1023
|
try {
|
|
1024
|
+
const err = cause instanceof Error ? {
|
|
1025
|
+
type: toSingleLineMessage(String(cause.name)),
|
|
1026
|
+
message: toSingleLineMessage(String(cause.message))
|
|
1027
|
+
} : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
|
|
1024
1028
|
process.stderr.write(
|
|
1025
1029
|
JSON.stringify({
|
|
1026
1030
|
level: "error",
|
|
1027
|
-
logKey
|
|
1028
|
-
destination: name,
|
|
1031
|
+
logKey,
|
|
1032
|
+
destination: toSingleLineMessage(name),
|
|
1029
1033
|
err,
|
|
1030
|
-
msg:
|
|
1034
|
+
msg: toSingleLineMessage(msg)
|
|
1031
1035
|
}) + "\n"
|
|
1032
1036
|
);
|
|
1033
1037
|
} catch {
|
|
1034
1038
|
}
|
|
1035
1039
|
}
|
|
1036
|
-
|
|
1040
|
+
|
|
1041
|
+
// src/server/utils/destination-to-stream.ts
|
|
1042
|
+
function reportWriteFailure(name, cause) {
|
|
1043
|
+
reportDestinationFailure(
|
|
1044
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
|
|
1045
|
+
name,
|
|
1046
|
+
cause,
|
|
1047
|
+
`Log destination "${name}" failed to write; the entry was dropped`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
function rescueToStdout(payload) {
|
|
1051
|
+
try {
|
|
1052
|
+
process.stdout.write(payload);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function destinationToStream(destination, health) {
|
|
1037
1057
|
return new Writable({
|
|
1038
1058
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
1039
1059
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
@@ -1047,6 +1067,13 @@ function destinationToStream(destination) {
|
|
|
1047
1067
|
};
|
|
1048
1068
|
try {
|
|
1049
1069
|
const payload = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1070
|
+
if (health.isFailed(destination)) {
|
|
1071
|
+
if (health.shouldRescue(destination)) {
|
|
1072
|
+
rescueToStdout(payload);
|
|
1073
|
+
}
|
|
1074
|
+
callback();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1050
1077
|
const result = destination.write(payload);
|
|
1051
1078
|
if (result instanceof Promise) {
|
|
1052
1079
|
result.then(() => callback(), onFailure);
|
|
@@ -1436,7 +1463,7 @@ function resolveRedactOption(options) {
|
|
|
1436
1463
|
}
|
|
1437
1464
|
return { paths, censor: options.redactCensor };
|
|
1438
1465
|
}
|
|
1439
|
-
function buildPinoInstance(options, logContext, destinations) {
|
|
1466
|
+
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1440
1467
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1441
1468
|
const redact = resolveNameRedactor(options);
|
|
1442
1469
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
@@ -1508,55 +1535,162 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1508
1535
|
};
|
|
1509
1536
|
const streams = destinations.map((destination) => ({
|
|
1510
1537
|
level: destination.minLevel ?? options.level,
|
|
1511
|
-
stream: destinationToStream(destination)
|
|
1538
|
+
stream: destinationToStream(destination, health)
|
|
1512
1539
|
}));
|
|
1513
1540
|
return pino(pinoOptions, pino.multistream(streams));
|
|
1514
1541
|
}
|
|
1542
|
+
var DestinationHealth = class {
|
|
1543
|
+
constructor() {
|
|
1544
|
+
/** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
|
|
1545
|
+
this.failed = /* @__PURE__ */ new Set();
|
|
1546
|
+
/**
|
|
1547
|
+
* Whether ANY destination initialized successfully.
|
|
1548
|
+
*
|
|
1549
|
+
* A boolean rather than a count, deliberately: the only question ever asked of
|
|
1550
|
+
* it is "is there somewhere to write?". Mutation testing made the point — a
|
|
1551
|
+
* counter incremented with `+=` survived being flipped to `-=`, because
|
|
1552
|
+
* `count === 0` means the same thing either way. A value whose arithmetic
|
|
1553
|
+
* cannot be observed was not a counter, it was a flag wearing a number.
|
|
1554
|
+
*/
|
|
1555
|
+
this.hasHealthy = false;
|
|
1556
|
+
/**
|
|
1557
|
+
* The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
|
|
1558
|
+
* every real level so the first `markFailed` always wins the comparison.
|
|
1559
|
+
*/
|
|
1560
|
+
this.rescuerLevel = Number.POSITIVE_INFINITY;
|
|
1561
|
+
}
|
|
1562
|
+
/** Record that a destination initialized successfully. */
|
|
1563
|
+
markHealthy() {
|
|
1564
|
+
this.hasHealthy = true;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Record that a destination failed to initialize, and re-elect the rescuer.
|
|
1568
|
+
*
|
|
1569
|
+
* The rescuer is the failed destination with the LOWEST effective level, ties
|
|
1570
|
+
* broken by registration order (the comparison is strict). Registration order
|
|
1571
|
+
* alone is deterministic but arbitrary: a `warn` sink registered before an
|
|
1572
|
+
* `info` one would rescue at `warn` and silently drop the `info` entries the
|
|
1573
|
+
* second was configured to receive, and swapping the array would change the
|
|
1574
|
+
* output for the same configuration. Electing by level rescues the UNION of
|
|
1575
|
+
* what the consumer asked to see.
|
|
1576
|
+
*
|
|
1577
|
+
* Inheriting a level filter at all is deliberate — `minLevel` is the consumer's
|
|
1578
|
+
* own instruction, so the rescue delivers what a working sink would have, never
|
|
1579
|
+
* more. The one entry that must never be filtered, the init failure itself, goes
|
|
1580
|
+
* straight to stderr, outside the multistream.
|
|
1581
|
+
*
|
|
1582
|
+
* @param destination - The destination whose `onInit()` rejected.
|
|
1583
|
+
* @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
|
|
1584
|
+
* the module-wide `level`.
|
|
1585
|
+
*/
|
|
1586
|
+
markFailed(destination, effectiveLevel) {
|
|
1587
|
+
this.failed.add(destination);
|
|
1588
|
+
const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
|
|
1589
|
+
if (level < this.rescuerLevel) {
|
|
1590
|
+
this.rescuer = destination;
|
|
1591
|
+
this.rescuerLevel = level;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Whether a destination failed to initialize and must not receive writes.
|
|
1596
|
+
*
|
|
1597
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1598
|
+
* @returns `true` when its `onInit()` rejected.
|
|
1599
|
+
*/
|
|
1600
|
+
isFailed(destination) {
|
|
1601
|
+
return this.failed.has(destination);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Whether this destination is the one that must write the entry as raw NDJSON
|
|
1605
|
+
* to stdout because NOTHING initialized.
|
|
1606
|
+
*
|
|
1607
|
+
* Exactly one failed destination can satisfy this, so N failed sinks rescue an
|
|
1608
|
+
* entry once rather than N times.
|
|
1609
|
+
*
|
|
1610
|
+
* @param destination - The destination backing a fan-out stream.
|
|
1611
|
+
* @returns `true` when no destination initialized and this one is the elected
|
|
1612
|
+
* rescuer.
|
|
1613
|
+
*/
|
|
1614
|
+
shouldRescue(destination) {
|
|
1615
|
+
return !this.hasHealthy && this.rescuer === destination;
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
DestinationHealth = __decorateClass([
|
|
1619
|
+
Injectable()
|
|
1620
|
+
], DestinationHealth);
|
|
1515
1621
|
var DestinationRegistry = class {
|
|
1516
1622
|
/**
|
|
1517
1623
|
* @param registered - Every destination declared under
|
|
1518
1624
|
* `LOGGER_DESTINATIONS_TOKEN`. Injected by token explicitly because the
|
|
1519
1625
|
* package is bundled without `emitDecoratorMetadata`.
|
|
1520
|
-
* @param logger - Structured logger used
|
|
1521
|
-
* token explicitly for the same bundling reason.
|
|
1626
|
+
* @param logger - Structured logger used for the bootstrap entries. Injected by
|
|
1627
|
+
* token explicitly for the same bundling reason. Deliberately NOT used to
|
|
1628
|
+
* report init failures — see the file-level note.
|
|
1629
|
+
* @param options - Resolved module options, read for the bootstrap warnings and
|
|
1630
|
+
* for each destination's effective level.
|
|
1631
|
+
* @param health - Init-health record shared with the write fan-out.
|
|
1522
1632
|
*/
|
|
1523
|
-
constructor(registered, logger, options) {
|
|
1633
|
+
constructor(registered, logger, options, health) {
|
|
1524
1634
|
this.registered = registered;
|
|
1525
1635
|
this.logger = logger;
|
|
1526
1636
|
this.options = options;
|
|
1637
|
+
this.health = health;
|
|
1527
1638
|
/**
|
|
1528
1639
|
* Destinations that initialized successfully, in registration order.
|
|
1529
1640
|
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* write fan-out; a sink that failed `onInit` still receives writes, contained
|
|
1535
|
-
* fail-soft by the wrapper.
|
|
1641
|
+
* Authoritative for BOTH shutdown (reverse order) and — via the shared
|
|
1642
|
+
* `DestinationHealth` record written alongside it — the write fan-out. The two
|
|
1643
|
+
* are kept in step here because the multistream itself is wired before any
|
|
1644
|
+
* `onInit` runs and cannot be rebuilt afterwards; see `DestinationHealth`.
|
|
1536
1645
|
*/
|
|
1537
1646
|
this.active = [];
|
|
1538
1647
|
}
|
|
1539
1648
|
/**
|
|
1540
1649
|
* Initialize every registered destination. A destination whose `onInit`
|
|
1541
|
-
* rejects is skipped (not added to the active set)
|
|
1542
|
-
*
|
|
1650
|
+
* rejects is skipped (not added to the active set), marked failed so the
|
|
1651
|
+
* fan-out stops writing to it, and reported to stderr — it must not block
|
|
1652
|
+
* application bootstrap.
|
|
1653
|
+
*
|
|
1654
|
+
* The loop completes BEFORE {@link announceBootstrap}, and that ordering is
|
|
1655
|
+
* load-bearing rather than incidental: the health record is what lets a
|
|
1656
|
+
* last-resort sink carry the bootstrap entries when every destination failed.
|
|
1657
|
+
* Announcing first would emit them into a fan-out that has not yet been told
|
|
1658
|
+
* anything is wrong, and `LOGGER_BOOTSTRAP_WARNING` — the signal that exists so
|
|
1659
|
+
* a security review can see PII redaction was disabled — would be lost exactly
|
|
1660
|
+
* when the configuration is already known to be broken.
|
|
1543
1661
|
*/
|
|
1544
1662
|
async onModuleInit() {
|
|
1545
1663
|
for (const destination of this.registered) {
|
|
1546
1664
|
try {
|
|
1547
1665
|
await destination.onInit?.();
|
|
1548
1666
|
this.active.push(destination);
|
|
1667
|
+
this.health.markHealthy();
|
|
1549
1668
|
} catch (cause) {
|
|
1550
|
-
this.
|
|
1551
|
-
|
|
1552
|
-
cause instanceof Error ? cause : new Error(String(cause)),
|
|
1553
|
-
void 0,
|
|
1554
|
-
{ destination: destination.name }
|
|
1555
|
-
);
|
|
1669
|
+
this.health.markFailed(destination, destination.minLevel ?? this.options.level);
|
|
1670
|
+
this.reportInitFailure(destination.name, cause);
|
|
1556
1671
|
}
|
|
1557
1672
|
}
|
|
1558
1673
|
this.announceBootstrap();
|
|
1559
1674
|
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Report one destination init failure as a structured
|
|
1677
|
+
* `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
|
|
1678
|
+
*
|
|
1679
|
+
* Shares {@link reportDestinationFailure} with the write-failure path rather
|
|
1680
|
+
* than restating the shape, so an operator greps one `logKey` regardless of
|
|
1681
|
+
* which stage failed — and the two cannot drift apart.
|
|
1682
|
+
*
|
|
1683
|
+
* @param name - The failing destination's name.
|
|
1684
|
+
* @param cause - The thrown or rejected value.
|
|
1685
|
+
*/
|
|
1686
|
+
reportInitFailure(name, cause) {
|
|
1687
|
+
reportDestinationFailure(
|
|
1688
|
+
RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
|
|
1689
|
+
name,
|
|
1690
|
+
cause,
|
|
1691
|
+
`Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1560
1694
|
/**
|
|
1561
1695
|
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1562
1696
|
* initialized.
|
|
@@ -1634,7 +1768,8 @@ DestinationRegistry = __decorateClass([
|
|
|
1634
1768
|
Injectable(),
|
|
1635
1769
|
__decorateParam(0, Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1636
1770
|
__decorateParam(1, Inject(PinoLoggerService)),
|
|
1637
|
-
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN))
|
|
1771
|
+
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN)),
|
|
1772
|
+
__decorateParam(3, Inject(DestinationHealth))
|
|
1638
1773
|
], DestinationRegistry);
|
|
1639
1774
|
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1640
1775
|
var LogContextService = class {
|
|
@@ -1729,6 +1864,10 @@ function buildCommonProviders() {
|
|
|
1729
1864
|
return [
|
|
1730
1865
|
LogContextService,
|
|
1731
1866
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: LogContextService },
|
|
1867
|
+
// Shared by the Pino factory (which closes over it in every fan-out stream)
|
|
1868
|
+
// and by DestinationRegistry (which writes to it during onModuleInit). Both
|
|
1869
|
+
// depend on this provider and it depends on nothing, so there is no cycle.
|
|
1870
|
+
DestinationHealth,
|
|
1732
1871
|
{
|
|
1733
1872
|
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1734
1873
|
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
@@ -1739,8 +1878,13 @@ function buildCommonProviders() {
|
|
|
1739
1878
|
},
|
|
1740
1879
|
{
|
|
1741
1880
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1742
|
-
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
1743
|
-
inject: [
|
|
1881
|
+
useFactory: (options, logContext, destinations, health) => buildPinoInstance(options, logContext, destinations, health),
|
|
1882
|
+
inject: [
|
|
1883
|
+
LOGGER_OPTIONS_TOKEN,
|
|
1884
|
+
LogContextService,
|
|
1885
|
+
LOGGER_DESTINATIONS_TOKEN,
|
|
1886
|
+
DestinationHealth
|
|
1887
|
+
]
|
|
1744
1888
|
},
|
|
1745
1889
|
{
|
|
1746
1890
|
provide: LOGGER_DESTINATIONS_TOKEN,
|
|
@@ -1931,8 +2075,10 @@ var PrettyDevDestination = class {
|
|
|
1931
2075
|
*
|
|
1932
2076
|
* Three states:
|
|
1933
2077
|
* - initialized → forward to the pino-pretty transform;
|
|
1934
|
-
* - init failed → DROP
|
|
1935
|
-
*
|
|
2078
|
+
* - init failed → DROP, so a co-registered stdout sink is not duplicated. The
|
|
2079
|
+
* fan-out already skips a failed destination before reaching this method
|
|
2080
|
+
* (and rescues the entry when nothing else initialized); this branch is the
|
|
2081
|
+
* belt to that, for any caller holding the destination directly;
|
|
1936
2082
|
* - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
|
|
1937
2083
|
* → fall back to raw stdout so the entry is not lost.
|
|
1938
2084
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-logger",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"test:cov:all": "jest --config jest.coverage.config.ts --coverage",
|
|
76
76
|
"test:e2e": "jest --config jest.e2e.config.ts",
|
|
77
77
|
"test:watch": "jest --watch",
|
|
78
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json"
|
|
78
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.bench.json"
|
|
79
79
|
},
|
|
80
80
|
"lint-staged": {
|
|
81
81
|
"*.{ts,tsx,js,mjs,cjs}": [
|
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
"prettier --write"
|
|
87
87
|
]
|
|
88
88
|
},
|
|
89
|
-
"dependencies": {},
|
|
90
89
|
"peerDependencies": {
|
|
91
90
|
"@nestjs/common": "^11.0.16",
|
|
92
91
|
"@nestjs/core": "^11.1.18",
|