@bymax-one/nest-logger 1.2.9 → 1.3.1
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 +86 -1
- package/README.md +22 -0
- package/dist/server/index.cjs +6 -9
- package/dist/server/index.d.cts +20 -3
- package/dist/server/index.d.ts +20 -3
- package/dist/server/index.mjs +6 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,80 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.3.1] - 2026-08-18
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- **`excludePaths` says what its defaults do not reach.** The defaults are
|
|
19
|
+
`[/^\/health$/, /^\/metrics$/]`, and `@bymax-one/nest-core` serves liveness and readiness at
|
|
20
|
+
`/health/live` and `/health/ready` with no bare `/health` at all — so the default excludes a
|
|
21
|
+
route that does not exist while logging both probes. A consumer running both measured **8272 of
|
|
22
|
+
8274** HTTP entries as the liveness probe, at one container healthcheck every ten seconds; at
|
|
23
|
+
orchestrator volume it is the same shape with more zeros. There is no error and no warning, and
|
|
24
|
+
the config reads as though it were handled.
|
|
25
|
+
|
|
26
|
+
The defaults are **not** widened, and the reason is checkable rather than a preference: both
|
|
27
|
+
prefixes are configurable there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`). A default
|
|
28
|
+
naming specific subpaths would be less wrong rather than correct, and a prefix pattern would
|
|
29
|
+
silently swallow a consumer's own `/health/*` route they did want logged. Excluding a path
|
|
30
|
+
decides which requests vanish from the record, so it stays explicit — the docblock now carries
|
|
31
|
+
the one-line fix for the stock layout, and it ships in the `.d.ts` where someone wiring the two
|
|
32
|
+
libraries reads it.
|
|
33
|
+
|
|
34
|
+
### Internal
|
|
35
|
+
|
|
36
|
+
- **The last surviving mutant is gone, by deletion rather than annotation.** A `catch` in
|
|
37
|
+
`report-destination-failure` re-assigned `undefined` to a variable already `undefined` from its
|
|
38
|
+
declaration, so emptying the block changed no output for any input and no test could kill it. A
|
|
39
|
+
Stryker directive was tried first and was not honoured at that position. The project scores
|
|
40
|
+
**100.00** on a cold run: 1515 mutants, 841 killed, zero survivors, 145 documented equivalents.
|
|
41
|
+
|
|
42
|
+
## [1.3.0] - 2026-08-18
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **A frozen `Error` lost every field, including its own message.** A consumer that freezes its
|
|
47
|
+
errors — deliberate and documented practice, so nobody mutates an issue list after the throw —
|
|
48
|
+
hands over properties that are neither writable nor configurable. The redactor's clone inherited
|
|
49
|
+
those descriptors, and rewriting any key whose walked value is a fresh object (a censored secret,
|
|
50
|
+
or simply the structural copy the walk makes of a nested array) could not redefine it. That
|
|
51
|
+
failure reached the fail-closed guard and dropped the **whole** record, so the entry became
|
|
52
|
+
`_redactionFailed: true` and nothing else — total silent loss on the error that crashed the
|
|
53
|
+
process, which is the one entry worth logging.
|
|
54
|
+
|
|
55
|
+
A frozen error carrying only scalars never showed it, because redefining a property with the same
|
|
56
|
+
primitive succeeds; it took a nested object to expose it. Reported against 1.2.7 by a consumer and
|
|
57
|
+
reproduced unchanged on 1.2.9.
|
|
58
|
+
|
|
59
|
+
The clone is transient — built, serialized, discarded — so it is now built with relaxed
|
|
60
|
+
descriptors and the write always lands. **The caller's error is never touched**, and freezing
|
|
61
|
+
remains the caller's guarantee about the caller's own object.
|
|
62
|
+
|
|
63
|
+
A non-configurable secret is now **censored** rather than dropped: same protection, without
|
|
64
|
+
losing the surrounding fields. The frozen error also keeps its **stack** again — V8 exposes it as
|
|
65
|
+
an own property bound to the original's internal state, so the clone pins a resolved copy, and on
|
|
66
|
+
a frozen error that pin was failing silently too. The case that did NOT drop the record was
|
|
67
|
+
therefore already reaching the sink with its trace erased, which is the same defect wearing a
|
|
68
|
+
healthy face; a consumer measured that independently. `LOGGER_REDACTION_FAILED` is unchanged for the case it was written
|
|
69
|
+
for — a record the walk cannot READ, such as a throwing getter or a hostile proxy.
|
|
70
|
+
|
|
71
|
+
### Documentation
|
|
72
|
+
|
|
73
|
+
- **The limit of field-based redaction now ships.** A value interpolated into the message has no
|
|
74
|
+
field name for any strategy to match — not an uncovered one, none — so no configuration closes
|
|
75
|
+
that gap and the rule is "a value that matters is a field, not text". It was stated only in
|
|
76
|
+
`docs/OBSERVABILITY-CONTRACT.md`, which is not in `files`: it reached whoever had the repository
|
|
77
|
+
and nobody who installs from npm. The decision it governs is made at a call site in a derived
|
|
78
|
+
backend whose only copy of this contract is what the registry delivers, so it is in the README's
|
|
79
|
+
security model now, beside the case it sharpens — that one covers a secret inside a field's
|
|
80
|
+
**value**, this one a value with no field at all.
|
|
81
|
+
|
|
82
|
+
- **The `minLevel` entry below, under 1.2.9, now names the direction of its change.** The behaviour
|
|
83
|
+
changed in that release and its record stays there; what shipped here is the paragraph explaining
|
|
84
|
+
what upgrading does. A sink configured with an unrecognised level was silent and starts delivering
|
|
85
|
+
at the module level, which arrives as a step change in ingestion cost and reads as a noise
|
|
86
|
+
regression unless the note says which way it moves. Two consumers asked for this independently.
|
|
87
|
+
|
|
14
88
|
## [1.2.9] - 2026-08-16
|
|
15
89
|
|
|
16
90
|
> **`1.2.8` was merged but never published.** The defect below was found in it after the merge and
|
|
@@ -33,6 +107,15 @@ heading here.
|
|
|
33
107
|
`validateOptions` already held `options.level` to that same list; this is the check reaching
|
|
34
108
|
the one place it had not.
|
|
35
109
|
|
|
110
|
+
**On upgrade this changes your log volume, upward, and it will not look like a fix.** A sink
|
|
111
|
+
that was configured with an unrecognised level and was therefore silent starts delivering at
|
|
112
|
+
the module level the moment you install this version. Read as a graph, that is a step change in
|
|
113
|
+
ingestion cost arriving with a patch; read correctly, it is entries that were always supposed
|
|
114
|
+
to be delivered and never were. If you see one, check that destination's `minLevel` before
|
|
115
|
+
treating it as a regression — the value has to be one of `'trace'`, `'debug'`, `'info'`,
|
|
116
|
+
`'warn'`, `'error'` or `'fatal'`. `'verbose'` and `'log'` are NestJS level names, not Pino
|
|
117
|
+
ones, and are the likely culprits for anyone arriving from `LoggerService`.
|
|
118
|
+
|
|
36
119
|
- **A write returning a thenable that is not `instanceof Promise` no longer takes the synchronous
|
|
37
120
|
path, where its rejection escaped and its entry could be discarded.** `instanceof` is realm-local:
|
|
38
121
|
it answers `false` for a promise built in another realm — a worker, a `vm` context — and for any
|
|
@@ -1739,7 +1822,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1739
1822
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1740
1823
|
`release.yml`, Dependabot, and issue templates
|
|
1741
1824
|
|
|
1742
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.
|
|
1825
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.3.1...HEAD
|
|
1826
|
+
[1.3.1]: https://github.com/bymaxone/nest-logger/compare/v1.3.0...v1.3.1
|
|
1827
|
+
[1.3.0]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...v1.3.0
|
|
1743
1828
|
[1.2.9]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...v1.2.9
|
|
1744
1829
|
|
|
1745
1830
|
<!-- 1.2.8 has no link: it was merged but never tagged, so there is no v1.2.8 to compare against. See its section above. -->
|
package/README.md
CHANGED
|
@@ -1070,6 +1070,28 @@ There is no name rule that reaches the second one, and scanning inside values fo
|
|
|
1070
1070
|
|
|
1071
1071
|
`redactStrategy` governs **your** payload — the record you log, your child bindings, and the output of any serializer you supply. The built-in `err` serializer is the library's own output and is always name-walked, including under `'paths'`, because `fast-redact`'s four-wildcard ceiling cannot reach the depth a `cause` chain serializes to.
|
|
1072
1072
|
|
|
1073
|
+
#### A value interpolated into the message has no name to redact
|
|
1074
|
+
|
|
1075
|
+
The case above is a secret inside a field's **value**. The sharper version is a value interpolated into the message itself, where there is no field at all:
|
|
1076
|
+
|
|
1077
|
+
```ts
|
|
1078
|
+
// Both identifiers are now message text: there is no `userId` field and no
|
|
1079
|
+
// `familyId` field for any strategy to match, only a string that happens to
|
|
1080
|
+
// contain them.
|
|
1081
|
+
logger.warn(`reissueTokens: family revoked userId=${userId} familyId=${familyId}`)
|
|
1082
|
+
|
|
1083
|
+
// Same facts, as fields. The logKey is yours — `RESERVED_LOG_KEYS` holds the
|
|
1084
|
+
// library's own keys, not your domain's — and follows MODULE_ACTION_RESULT:
|
|
1085
|
+
// module AUTH, action REUSE, result DETECTED.
|
|
1086
|
+
logger.warnStructured('AUTH_REUSE_DETECTED', 'token family revoked after reuse detection', userId, {
|
|
1087
|
+
familyId
|
|
1088
|
+
})
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
Redaction matches names. An interpolated value does not have one — not an uncovered name, none — so there is nothing for any strategy to match, and no configuration you can add will change that. This is not a gap to be closed later: scanning message text for secret-shaped substrings would mean rewriting what you asked to be logged, which is the wrong trade in both directions.
|
|
1092
|
+
|
|
1093
|
+
The rule that follows is short: **a value that matters is a field, not text.** A field can be redacted, queried, aggregated and correlated; the same value inside `msg` can only be read by a human who happens to be looking. That applies most to the values you would least like to interpolate — tokens, identifiers, addresses — and it is the one part of this contract the library cannot enforce for you, because the decision is made at the call site.
|
|
1094
|
+
|
|
1073
1095
|
### A closed pipe does not kill the process
|
|
1074
1096
|
|
|
1075
1097
|
`node app | head` closes the read end of stdout while the application is still writing. The stream reports that as `EPIPE` **asynchronously**, through its `'error'` event, after `write()` has already returned — so a `try/catch` around the write catches nothing, and because Node attaches no default handler to `process.stdout`, the emit becomes an uncaught exception and the process dies. Measured, not assumed: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42.
|
package/dist/server/index.cjs
CHANGED
|
@@ -1137,7 +1137,6 @@ function safeMinLevel(destination) {
|
|
|
1137
1137
|
const read = destination.minLevel;
|
|
1138
1138
|
resolved = isLogLevel(read) ? read : void 0;
|
|
1139
1139
|
} catch {
|
|
1140
|
-
resolved = void 0;
|
|
1141
1140
|
}
|
|
1142
1141
|
resolvedMinLevels.set(destination, resolved);
|
|
1143
1142
|
return resolved;
|
|
@@ -1224,10 +1223,11 @@ function resolveToJson(value) {
|
|
|
1224
1223
|
};
|
|
1225
1224
|
}
|
|
1226
1225
|
function cloneError(error) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1226
|
+
const descriptors = Object.getOwnPropertyDescriptors(error);
|
|
1227
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1228
|
+
descriptor.configurable = true;
|
|
1229
|
+
}
|
|
1230
|
+
const clone = Object.create(Object.getPrototypeOf(error), descriptors);
|
|
1231
1231
|
const { stack } = error;
|
|
1232
1232
|
if (typeof stack === "string") {
|
|
1233
1233
|
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
@@ -1235,11 +1235,8 @@ function cloneError(error) {
|
|
|
1235
1235
|
return clone;
|
|
1236
1236
|
}
|
|
1237
1237
|
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1238
|
-
var WRITE_FAILED = new Error();
|
|
1239
1238
|
function defineOwn(target, key, value) {
|
|
1240
|
-
|
|
1241
|
-
throw WRITE_FAILED;
|
|
1242
|
-
}
|
|
1239
|
+
Reflect.defineProperty(target, key, { value, enumerable: true });
|
|
1243
1240
|
}
|
|
1244
1241
|
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1245
1242
|
const length = value.length;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -260,6 +260,21 @@ interface HttpOptions {
|
|
|
260
260
|
* Paths that bypass HTTP logging (health checks, metrics). Defaults:
|
|
261
261
|
* `/health`, `/metrics`.
|
|
262
262
|
*
|
|
263
|
+
* The defaults do NOT cover `@bymax-one/nest-core`, and that is the common case
|
|
264
|
+
* worth knowing before you rely on them. It serves liveness and readiness at
|
|
265
|
+
* `/health/live` and `/health/ready`, and no bare `/health` at all, so
|
|
266
|
+
* `/^\/health$/` excludes a route that does not exist while both probes are
|
|
267
|
+
* logged. Measured on one deployment: 8272 of 8274 HTTP entries were the
|
|
268
|
+
* liveness probe. Set `excludePaths` yourself when wiring the two together —
|
|
269
|
+
* `[/^\/health\/(live|ready)$/, /^\/metrics$/]` matches the stock layout.
|
|
270
|
+
*
|
|
271
|
+
* The defaults are not widened to include those subpaths because both prefixes
|
|
272
|
+
* are CONFIGURABLE there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`): any
|
|
273
|
+
* default naming specific subpaths would be less wrong rather than correct, and
|
|
274
|
+
* a prefix pattern would silently swallow a consumer's own `/health/*` route
|
|
275
|
+
* that they did want logged. Excluding a path is a decision about which requests
|
|
276
|
+
* disappear from the record, so it stays explicit.
|
|
277
|
+
*
|
|
263
278
|
* SECURITY: each pattern is `.test()`-ed against the (attacker-controllable)
|
|
264
279
|
* request path on every request. Supply only anchored, linear-time regexes
|
|
265
280
|
* (e.g. `/^\/health$/`); a pattern with catastrophic backtracking would turn a
|
|
@@ -639,9 +654,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
|
|
|
639
654
|
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
640
655
|
* faster than the path expansion it replaced.
|
|
641
656
|
*
|
|
642
|
-
* Any internal failure —
|
|
643
|
-
* whole record to a marked, secret-free envelope rather
|
|
644
|
-
* that produced it.
|
|
657
|
+
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
658
|
+
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
659
|
+
* than crashing the request that produced it. Writing the censor is not among those
|
|
660
|
+
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
661
|
+
* error keeps its fields instead of costing the record.
|
|
645
662
|
*
|
|
646
663
|
* @see {@link createNameRedactor}
|
|
647
664
|
*/
|
package/dist/server/index.d.ts
CHANGED
|
@@ -260,6 +260,21 @@ interface HttpOptions {
|
|
|
260
260
|
* Paths that bypass HTTP logging (health checks, metrics). Defaults:
|
|
261
261
|
* `/health`, `/metrics`.
|
|
262
262
|
*
|
|
263
|
+
* The defaults do NOT cover `@bymax-one/nest-core`, and that is the common case
|
|
264
|
+
* worth knowing before you rely on them. It serves liveness and readiness at
|
|
265
|
+
* `/health/live` and `/health/ready`, and no bare `/health` at all, so
|
|
266
|
+
* `/^\/health$/` excludes a route that does not exist while both probes are
|
|
267
|
+
* logged. Measured on one deployment: 8272 of 8274 HTTP entries were the
|
|
268
|
+
* liveness probe. Set `excludePaths` yourself when wiring the two together —
|
|
269
|
+
* `[/^\/health\/(live|ready)$/, /^\/metrics$/]` matches the stock layout.
|
|
270
|
+
*
|
|
271
|
+
* The defaults are not widened to include those subpaths because both prefixes
|
|
272
|
+
* are CONFIGURABLE there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`): any
|
|
273
|
+
* default naming specific subpaths would be less wrong rather than correct, and
|
|
274
|
+
* a prefix pattern would silently swallow a consumer's own `/health/*` route
|
|
275
|
+
* that they did want logged. Excluding a path is a decision about which requests
|
|
276
|
+
* disappear from the record, so it stays explicit.
|
|
277
|
+
*
|
|
263
278
|
* SECURITY: each pattern is `.test()`-ed against the (attacker-controllable)
|
|
264
279
|
* request path on every request. Supply only anchored, linear-time regexes
|
|
265
280
|
* (e.g. `/^\/health$/`); a pattern with catastrophic backtracking would turn a
|
|
@@ -639,9 +654,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
|
|
|
639
654
|
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
640
655
|
* faster than the path expansion it replaced.
|
|
641
656
|
*
|
|
642
|
-
* Any internal failure —
|
|
643
|
-
* whole record to a marked, secret-free envelope rather
|
|
644
|
-
* that produced it.
|
|
657
|
+
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
658
|
+
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
659
|
+
* than crashing the request that produced it. Writing the censor is not among those
|
|
660
|
+
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
661
|
+
* error keeps its fields instead of costing the record.
|
|
645
662
|
*
|
|
646
663
|
* @see {@link createNameRedactor}
|
|
647
664
|
*/
|
package/dist/server/index.mjs
CHANGED
|
@@ -1134,7 +1134,6 @@ function safeMinLevel(destination) {
|
|
|
1134
1134
|
const read = destination.minLevel;
|
|
1135
1135
|
resolved = isLogLevel(read) ? read : void 0;
|
|
1136
1136
|
} catch {
|
|
1137
|
-
resolved = void 0;
|
|
1138
1137
|
}
|
|
1139
1138
|
resolvedMinLevels.set(destination, resolved);
|
|
1140
1139
|
return resolved;
|
|
@@ -1221,10 +1220,11 @@ function resolveToJson(value) {
|
|
|
1221
1220
|
};
|
|
1222
1221
|
}
|
|
1223
1222
|
function cloneError(error) {
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1223
|
+
const descriptors = Object.getOwnPropertyDescriptors(error);
|
|
1224
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1225
|
+
descriptor.configurable = true;
|
|
1226
|
+
}
|
|
1227
|
+
const clone = Object.create(Object.getPrototypeOf(error), descriptors);
|
|
1228
1228
|
const { stack } = error;
|
|
1229
1229
|
if (typeof stack === "string") {
|
|
1230
1230
|
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
@@ -1232,11 +1232,8 @@ function cloneError(error) {
|
|
|
1232
1232
|
return clone;
|
|
1233
1233
|
}
|
|
1234
1234
|
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1235
|
-
var WRITE_FAILED = new Error();
|
|
1236
1235
|
function defineOwn(target, key, value) {
|
|
1237
|
-
|
|
1238
|
-
throw WRITE_FAILED;
|
|
1239
|
-
}
|
|
1236
|
+
Reflect.defineProperty(target, key, { value, enumerable: true });
|
|
1240
1237
|
}
|
|
1241
1238
|
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1242
1239
|
const length = value.length;
|
package/package.json
CHANGED