@bymax-one/nest-logger 1.1.0 → 1.2.0

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 CHANGED
@@ -11,6 +11,313 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.2.0] - 2026-08-13
15
+
16
+ Remediation of the P0 findings from the observability audit
17
+ ([`docs/observability_audit.md`](./docs/observability_audit.md)): a credential leak in the
18
+ default redaction set, a correlation field silently dropped from every structured entry, a
19
+ published type that never matched the runtime, and reserved log keys that were documented as
20
+ signals but never written. The redaction engine was replaced in the process, which made the
21
+ shipped logging path **~50× faster**.
22
+
23
+ A second pass moved the HTTP access log from an interceptor to middleware, because an interceptor
24
+ cannot see a request a guard rejected: 401, 403, 429 and 404 produced no log line at all.
25
+
26
+ ### Security
27
+
28
+ - **Guard rejections and unmatched routes are logged.** NestJS runs
29
+ middleware → guards → interceptors → handler, so the interceptor-based access log never observed
30
+ a request rejected by a guard or one that matched no route. Measured against a real backend over
31
+ 20 minutes of traffic: lines existed for 200/201/400, and **zero** for 401 — brute force,
32
+ credential stuffing and route enumeration were invisible, and invisible without a `requestId` to
33
+ correlate them by. `HttpAccessLogMiddleware` now records the access log before guards run, so
34
+ every request produces `HTTP_REQUEST_START` and a terminal entry carrying `ip`, `userAgent` and
35
+ the correlation context.
36
+ - **The correlation scope no longer skips the prefixed root route.** `applyRequestIdMiddleware`
37
+ defaulted to the route pattern `'*'`, and under NestJS 11 (Express 5 / path-to-regexp v8) a
38
+ wildcard is a named parameter with segment-count semantics. Measured: both `'*'` and `'{*splat}'`
39
+ stop matching once the app calls `setGlobalPrefix`, so `GET /api` silently had no correlation
40
+ scope and, with the change above, no access log either. The default is now `'/'`, which MOUNTS at
41
+ the root rather than matching a pattern. The failure mode was absence — no error, just a request
42
+ that never appeared.
43
+
44
+ ### Fixed
45
+
46
+ - **An aborted request is no longer logged as a success.** Destroying the socket does not cancel
47
+ the handler: it runs to completion, writes to a dead connection, the observable completes, and
48
+ the interceptor reported the `200` it intended. Delivery is now reported on its own axis —
49
+ `HTTP_REQUEST_ABORTED`, emitted from the response's `'close'` event when `writableFinished` is
50
+ false — while the status stays whatever the server produced. Inventing one was rejected: nginx's
51
+ `499` is not an HTTP status (IANA leaves 452–499 unassigned), so recording it would assert a code
52
+ the protocol has no name for and break any consumer grouping by class. The limit is stated in the
53
+ README: a fast handler whose client hangs up after the bytes were flushed is still reported as
54
+ the success it was, because the server cannot know whether the peer read them.
55
+
56
+ ### Breaking
57
+
58
+ - **`HttpAccessLogMiddleware` must be wired for the new coverage.** It is registered by
59
+ `applyRequestIdMiddleware(consumer)`, which most consumers already call — no code change needed
60
+ for them. A consumer who never wired that helper keeps the previous interceptor-based behaviour
61
+ (including its blind spot) rather than losing HTTP logs. **Migration:** call
62
+ `applyRequestIdMiddleware(consumer)` in your module's `configure()` hook.
63
+ - **The default middleware route changed from `'*'` to `'/'`.** A consumer passing an explicit
64
+ route is unaffected. One relying on the default now also gets the prefixed root route, which is
65
+ the fix. **Migration:** none; if you deliberately excluded the root, pass an explicit route.
66
+ - **`LoggableResponse` gained `writableFinished` and `on('close', …)`.** An Express response
67
+ satisfies both already. **Migration:** only a consumer who hand-implements the interface — for a
68
+ test double, say — needs to add them.
69
+ - **`HTTP_REQUEST_START` no longer carries `userId`.** The entry is now emitted before guards run,
70
+ which is the whole point of the change — and authentication runs in a guard, so at that moment
71
+ there is no principal to read. The previous interceptor-based START ran after guards and did
72
+ include it. The acting user is still logged: it is read at the terminal entry
73
+ (`HTTP_REQUEST_SUCCESS` / `_REDIRECT` / `_CLIENT_ERROR` / `_SERVER_ERROR` / `_ABORTED`), where
74
+ the guard has populated it. **Migration:** query the terminal entry for `userId`, not START. A
75
+ dashboard joining on START's `userId` should join on `requestId` instead, which both entries
76
+ carry.
77
+ - **The logged URL now includes the app's global prefix.** The middleware is mounted, and a mounted
78
+ middleware sees `url` relative to its mount point, so the entry is built from `originalUrl`.
79
+ Under `setGlobalPrefix('api')` a request for `/api/users/7` used to be logged by the interceptor
80
+ as `/api/users/:id`; that is preserved. **Migration:** none — this keeps the previous value. An
81
+ `excludePaths` pattern is likewise matched against the full path, as before.
82
+ - **New reserved log key `HTTP_REQUEST_ABORTED`.** A consumer asserting an exhaustive list of
83
+ `RESERVED_LOG_KEYS` must add it. **Migration:** add the key to the assertion.
84
+
85
+ ### Security
86
+
87
+ - **Credential-bearing HTTP headers are redacted by name.** `authorization`, `cookie`,
88
+ `set-cookie`, `x-api-key` and `x-auth-token` were covered ONLY by the absolute paths
89
+ `req.headers.*` / `res.headers.*`. A headers bag logged under any other key — for example
90
+ `logger.info(key, msg, userId, { headers: req.headers })` — wrote the bearer token in clear.
91
+ They are now first-class field names, caught wherever they appear.
92
+ - **The four-level nesting cap is gone, and what replaces it fails closed.** The previous wildcard
93
+ expansion reached `*.*.*.*.field` and anything deeper was emitted in clear. The walk now reaches
94
+ 100 levels, and past that a CONTAINER is DROPPED rather than passed through — a traversal ceiling
95
+ exists only so a pathological self-similar structure cannot exhaust the call stack, and it can
96
+ never become a leak the way the old one was. A primitive at the boundary is still emitted, and
97
+ that is not a gap: its key was matched by the parent at level 100, the last container walked.
98
+ - **Prototype-polluting metadata keys are dropped instead of silently swapping the copy.** A log
99
+ call's `metadata` was copied with `Reflect.set`, which does NOT create an own property for
100
+ `__proto__` — the write walks the prototype chain, finds `Object.prototype`'s inherited setter
101
+ and invokes it. An own `__proto__` (what `JSON.parse` of an untrusted body produces) therefore
102
+ vanished from the entry AND swapped the copy's prototype for the caller's value. `__proto__`,
103
+ `constructor` and `prototype` are now dropped, from the same constant the ALS context path
104
+ already enforced, so the two guards cannot drift. `Object.prototype` itself was never reachable,
105
+ which is what kept this a correctness bug rather than a pollution vulnerability.
106
+ - **An accessor `toJSON` can no longer smuggle a value through the binary fast path.** That path
107
+ hands back the ORIGINAL reference, and `JSON.stringify` reads `toJSON` again from it — so a
108
+ GETTER returning `Buffer.prototype.toJSON` to the walk took the fast path, and the stringifier
109
+ then read a factory synthesizing `{ password }`, in clear, past both hooks. Identity proves
110
+ nothing about the next read unless the property is a data property, so an accessor no longer
111
+ selects any path that returns a caller-controlled reference. `toJSON` is now resolved along the
112
+ prototype chain exactly ONCE per value, where the walk used to read it twice.
113
+ - **A `toJSON` chain can no longer exhaust the stack.** Following the method's output did not
114
+ advance the depth counter, so a chain where each `toJSON()` returns a fresh object carrying
115
+ another one recursed forever — nothing repeats, so the ancestor set never matched, and the
116
+ ceiling was never reached. The root catch contained it as the fail-closed envelope rather than a
117
+ crash, but it cost the whole record; the ceiling costs only the pathological value.
118
+ - **A `toJSON` can no longer rename a field around the matcher.** Only the method's OUTPUT was
119
+ inspected, so `{ password, toJSON: () => ({ value: this.password }) }` emitted the secret under
120
+ `value` — a name nobody declared sensitive. When the SOURCE carries a sensitive own key the
121
+ method is no longer trusted and the whole value is censored. This over-redacts an object that
122
+ holds a sensitive key AND correctly omits it, deliberately: the alternative — invoking `toJSON`
123
+ against a sanitized copy — throws on every method that reads an internal slot instead of an own
124
+ property, which is `Date`, `Decimal` and Luxon.
125
+ - **A terminal array index in `redactPaths` no longer covers nothing.** `tokens[0]` fed the leaf
126
+ `0` to the walk, which compares NAMES and never array positions — so the element stayed raw
127
+ through the size-bounded `_preview`, while any object key literally named `0` was censored
128
+ instead. An unquoted numeric segment is now read as an index and skipped like a wildcard, so
129
+ `tokens[0]` covers `tokens`: broader than the path, in the safe direction. The quoted form
130
+ `["0"]` stays a name.
131
+ - **Base bindings are redacted.** `service` is consumer-supplied and `applyDefaults` keeps whatever
132
+ it was handed, so a `{ name, version, apiKey }` reached the sink in clear once base stopped going
133
+ through the path expansion that had covered it via `*.*.apiKey`. Redacted at
134
+ `formatters.bindings`, which runs once at logger construction — no per-entry cost — and which
135
+ preserves a consumer's extra non-sensitive metadata rather than trimming base to the two declared
136
+ fields.
137
+ - **Child-logger bindings are redacted.** `PinoLoggerService.child(bindings)` accepts any record,
138
+ and Pino pre-serializes child bindings into the instance's `chindings` fragment before any
139
+ formatter runs — so no factory hook can reach them. `logger.child({ password })` stamped the
140
+ value in clear on every entry that child emitted. Redaction is applied in `child()` itself.
141
+ - **An `Error` is redacted wherever it is logged, not only under a key with a serializer.** The
142
+ walk skipped `Error` instances to preserve the instance Pino's `err` serializer keys off, and
143
+ the compensating serializer hook only fires for keys that actually have one — so
144
+ `{ failure: err }` carrying an `apiKey` reached the sink in clear. Errors are now cloned through
145
+ their prototype and descriptors, which censors the enumerable properties while keeping
146
+ `instanceof`, `message` and `stack` intact.
147
+ - **Sensitive field names are matched case-insensitively.** HTTP header names are
148
+ case-insensitive by spec, and only INBOUND Node headers arrive lower-cased — a hand-built or
149
+ outbound bag carrying `Authorization`, `Cookie` or `X-API-Key` was left in clear while the
150
+ documentation claimed header coverage. Matching now lower-cases the key, which also covers
151
+ `Password` / `Email` and errs toward redacting.
152
+ - **An array with a custom `toJSON()` is redacted.** `JSON.stringify` gives the method precedence
153
+ over array serialization, so an array whose `toJSON()` synthesized a secret was walked as an
154
+ ordinary array — finding nothing in its elements — and then emitted the secret. The `toJSON`
155
+ branch now runs before the array branch.
156
+ - **A secret synthesized by `toJSON()` is redacted.** A value with `toJSON` decides its own
157
+ serialized form, so the walk cannot inspect its own properties — but skipping it let
158
+ `{ toJSON: () => ({ accessToken }) }` emit the token untouched. The walk now redacts the
159
+ method's output, substituting it only when something was actually censored so a clean `Date` or
160
+ `Decimal` is still passed through by reference.
161
+ - **Binary values are fast-pathed by IDENTITY, not by shape.** `ArrayBuffer.isView` was too wide:
162
+ it also matched extended views, and three shapes went straight through both hooks — an own
163
+ `toJSON` synthesizing a payload, and an enumerable property on a `Uint8Array` or `DataView`,
164
+ neither of which has a `toJSON` to hide it. Narrowing it to "a view with no OWN `toJSON`" was
165
+ still too wide, because a SUBCLASS can define one on its own prototype. The check is now an
166
+ identity comparison against `Buffer.prototype.toJSON` itself, the one function known to produce a
167
+ `{ type, data }` output that cannot carry a caller's key — which keeps a real binary payload from
168
+ being recursed over byte by byte, and inspects everything else.
169
+ - **Arrays are indexed numerically rather than iterated.** `for...of` runs the array's
170
+ `Symbol.iterator`, which a caller can override, while `JSON.stringify` reads `length` and
171
+ numeric indices. An iterator that never returned `done` hung the log call — a loop that does not
172
+ end cannot be caught by the never-throw guard — and one yielding values unrelated to the indices
173
+ made the walk inspect something the array does not hold. Reading the way the serializer reads
174
+ also makes holes render identically to the native output.
175
+ - **A callable carrying `toJSON` is inspected.** `JSON.stringify` applies a callable `toJSON` to a
176
+ FUNCTION object too — the spec runs that step BEFORE the "callable serializes to undefined" rule
177
+ — so `Object.assign(() => {}, { toJSON: () => ({ accessToken }) })` emitted its payload with
178
+ nothing having inspected it. An ordinary function is still handed back untouched, so it keeps
179
+ being omitted from the output rather than becoming `{}`.
180
+ - **The root of a log record never honours `toJSON`.** Pino ITERATES that object rather than
181
+ serializing it, so honouring the method replaced the whole record with its return value:
182
+ `logger.info(key, msg, userId, { toJSON: () => 'x' })` emitted `{"0":"x"}` and lost `logKey`,
183
+ `userId` and every other field. Nested values and serializer outputs do reach `JSON.stringify`
184
+ and keep it. The same applies to `child()` bindings, which Pino also iterates.
185
+ - **An object that reports no keys is snapshot, not passed through.** The empty-key fast path
186
+ returned the original reference, which a Proxy exploits: answer `[]` to the walk's
187
+ `Object.keys`, then expose an enumerable secret when Pino serializes the reference.
188
+ - **A censor that cannot be written fails closed.** `Reflect.defineProperty` reports failure by
189
+ returning `false` rather than throwing, so an `Error` carrying a non-configurable enumerable
190
+ secret kept its raw value while the redaction silently no-opped. A failed write is now a
191
+ traversal failure.
192
+ - **A stateful `toJSON()` or getter cannot differ between inspection and serialization.** The walk
193
+ probed `toJSON()` once and, when the result was clean, returned the original object — leaving
194
+ `JSON.stringify` to call the method a second time (with the property key, which the probe does
195
+ not pass). The inspected result is now what gets serialized, and the same applies to accessors.
196
+ - **Hostile metadata and hostile errors no longer crash the caller.** The never-throw guarantee
197
+ now starts at the FIRST read of caller-controlled data: `Object.keys` fires a Proxy's `ownKeys`
198
+ trap and `Reflect.get` fires a getter, both before anything reached the redaction pipeline, so
199
+ `logger.info(key, msg, undefined, hostileMetadata)` threw outright. Unreadable metadata is
200
+ dropped whole and marked with the redaction-failure envelope while the entry keeps its real
201
+ `logKey`, message and correlation ids. The same guard covers the error path — `name`, `message`
202
+ and `stack` are ordinary properties a caller can redefine as throwing accessors, and `message`
203
+ is read outside the serializer.
204
+ - **A record whose getter throws is dropped WHOLE, not up to the failing property.** The mixin
205
+ merge used `Object.assign` on the mixin's own object, which copies key by key — so everything
206
+ read before the hostile getter was already written into it, and the failure path emitted that
207
+ prefix while claiming to drop the record. Merging into a disposable target keeps the partial
208
+ writes in the value that is discarded.
209
+ - **A throwing getter no longer crashes the log call.** Pino merges the mixin result with the
210
+ caller's object before `formatters.log` runs, and the default strategy's `Object.assign` invokes
211
+ every own getter — so a hostile getter threw before the redactor's fail-closed envelope could
212
+ apply. The factory now owns the merge, and the never-throw guarantee holds through the real
213
+ pipeline.
214
+ - **Bootstrap entries are emitted after destination initialization.** They were written from an
215
+ eagerly instantiated provider factory, which runs before `DestinationRegistry.onModuleInit()` —
216
+ so a sink that only accepts writes once its own `onInit()` has run could drop them, including
217
+ `LOGGER_BOOTSTRAP_WARNING`. They now come from the registry that owns that initialization, which
218
+ makes the ordering structural. Note for test authors: `Test.createTestingModule().compile()`
219
+ builds the graph, `init()` runs the lifecycle hooks — the bootstrap entries appear after `init()`.
220
+ - **`LOGGER_BOOTSTRAP_WARNING` is emitted when `shouldDisableDefaultRedact` is on.** The README
221
+ has always described this entry as the audit trail proving when PII protection was
222
+ intentionally reduced. It was never written, so a deployment running without redaction was
223
+ indistinguishable from a protected one.
224
+
225
+ ### Changed
226
+
227
+ - **Default redaction is now a single name-based recursive walk** instead of 140 compiled
228
+ `fast-redact` paths. A value is censored when its key name is in `REDACT_COMMON_FIELDS`, at any
229
+ depth, in one snapshotting traversal that mutates nothing of the caller's and reads every value
230
+ exactly once.
231
+ Circular references collapse to `[Circular]`; a record that cannot be walked
232
+ degrades to a marked, data-free envelope rather than being emitted unredacted.
233
+
234
+ Measured on the full production path (`forRoot({ service })`, no other option set):
235
+ **9,311 → ~274,000 logs/s**, ~107 µs → ~3.6 µs per entry. Every value is read exactly once and
236
+ pinned into a fresh structure, so what reaches the sink is guaranteed to be what was inspected —
237
+ an earlier copy-on-write draft returned clean subtrees by reference and let `JSON.stringify`
238
+ re-evaluate their accessors, which a stateful getter can answer differently.
239
+
240
+ - **`redactPaths` is unchanged** — consumer paths are still `fast-redact` paths, applied on top of
241
+ the default coverage. `fast-redact` is now configured only when there are consumer paths to
242
+ apply.
243
+ - **Bundle-size budget raised** 13.5 → 16.0 KiB brotli for the server subpath, and the benchmark's
244
+ throughput floor raised 0.004 → 0.20; the old floor was calibrated to the wildcard engine and
245
+ could no longer fail on anything short of a 100× regression.
246
+
247
+ ### Added
248
+
249
+ - **`redactStrategy: 'names' | 'paths'`** (default `'names'`). `'paths'` restores the pre-1.2
250
+ engine for a consumer depending on exact `fast-redact` path semantics — with its four-level
251
+ ceiling and its cost. Expect it to be removed in a future major.
252
+ - **`PINO_LEVEL_NUMBERS` / `PINO_LEVEL_NAMES` are exported** from the server subpath, so a
253
+ destination writing a numeric level column can convert the string label without hard-coding the
254
+ mapping.
255
+ - **`RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED`** — the marker on the envelope substituted for a
256
+ record whose traversal threw.
257
+ - **`RESERVED_LOG_KEYS_NOT_EMITTED`** — the keys that are reserved but intentionally never
258
+ written, each with its reason. A unit test now asserts that every other declared key has a
259
+ writer in production source, so a key can no longer be declared, documented as a signal, and
260
+ silently never emitted.
261
+
262
+ ### Fixed
263
+
264
+ - **AsyncLocalStorage context reaches structured entries.** `emitStructured` / `errorStructured` /
265
+ the NestJS-variadic path wrote `userId` and `context` as own properties even when they were
266
+ `undefined`, and Pino's default `mixinMergeStrategy` (`Object.assign(mixinResult, mergeObject)`)
267
+ let that `undefined` overwrite the value the trace mixin had just read from the ALS store. The
268
+ key then vanished during serialization, so `logContext.set('userId', …)` — the documented way to
269
+ attach the authenticated user once per request — never reached a log unless every call site
270
+ repeated it. `requestId` and `tenantId` only ever survived because those names are not written
271
+ by this class. Precedence is now explicit argument > ALS store > field absent.
272
+ - **A caller's `metadata` can never occupy `logKey`, `userId` or `context`.** The invariant was
273
+ previously enforced by overwriting those fields after the spread — the same unconditional write
274
+ that clobbered the ALS context. Now that they are written only when defined, the owned names are
275
+ stripped from `metadata` on the way in, so a metadata bag cannot forge the acting user that the
276
+ mixin read from the authenticated request scope.
277
+ - **`RESERVED_LOG_KEYS_NOT_EMITTED` is exported** from both subpaths, so the `{@link}` in
278
+ `RESERVED_LOG_KEYS`'s documentation resolves in the published declarations.
279
+ - **`LOGGER_SHUTDOWN_OK` is emitted** at the start of `onApplicationShutdown`, before the
280
+ destinations are torn down, with an event-loop barrier so an async sink's write is not raced by
281
+ its own teardown (the authoritative contract remains `ILogDestination.onShutdown`, which MUST
282
+ flush pending writes). It is the bookend to `LOGGER_BOOTSTRAP_OK`: its absence in a log
283
+ stream is how an operator tells a graceful shutdown from a killed process.
284
+ - **A consumer path's leaf name reaches the walk, so it cannot leak through a truncation preview.**
285
+ Consumer `redactPaths` are applied by Pino's stringifier, which runs AFTER the per-field size
286
+ bound — so a field covered only by a path was still raw when the 200-character `_preview` was
287
+ built. The leaf name is now matched by the walk as well, which closes that and covers the same
288
+ name on every other surface the walk reaches. Deliberately broader than the path itself:
289
+ `redactPaths: ['user.ssn']` censors `ssn` wherever it appears.
290
+ - **An oversized field's truncation `_preview` is redacted.** Redaction now runs before the size
291
+ bound, so the 200-character preview of a truncated value carries `[REDACTED]` instead of the
292
+ head of a secret.
293
+
294
+ ### Documentation
295
+
296
+ - **`README.md` API reference corrected.** The table described `warn` / `debug` / `error` / `fatal`
297
+ as structured methods taking a log key — they are the NestJS variadic bridge — documented a
298
+ `fatalStructured` that does not exist, labelled the structured API's third parameter `context`
299
+ when it is `userId`, and described `@LogContext` as a method decorator that opens a
300
+ `logContext.run()` scope when it is a class decorator that only records metadata.
301
+ - **The Loki and Prisma destination examples now run.** The Loki example called
302
+ `BigInt(entry.time)` on an ISO 8601 string, which throws; the Prisma example wrote the string
303
+ level label into a numeric column. Both conversions are now correct and are exercised by
304
+ `test/e2e/log-entry-contract.e2e-spec.ts`.
305
+ - **The architecture diagram no longer inverts the pipeline** — `RequestIdMiddleware` runs before
306
+ `HttpLoggingInterceptor`, which is why the interceptor's entries carry a `requestId` at all.
307
+
308
+ ### Breaking
309
+
310
+ - **`LogEntry.level` is `LogLevel` (a string) and `LogEntry.time` is `string`.** They were declared
311
+ `number` and `string | number`; the runtime has always emitted the Pino string label and an ISO
312
+ 8601 string. This is a **type-level** break only — code relying on the old declaration was
313
+ already failing at runtime — but it can newly fail to compile. Convert with the now-exported
314
+ `PINO_LEVEL_NUMBERS` and `Date.parse`.
315
+
316
+ Shipped as a minor rather than a major deliberately: the library has no consumers yet, and
317
+ SemVer's major exists to protect the consumers a break would reach. Breaks are documented here
318
+ under this heading with their migration path, which is what carries the signal while the version
319
+ number does not.
320
+
14
321
  ## [1.1.0] - 2026-08-11
15
322
 
16
323
  Coordinated ecosystem release aligning every `@bymax-one/*` package after the ioredis 6 /
@@ -350,7 +657,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
350
657
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
351
658
  `release.yml`, Dependabot, and issue templates
352
659
 
353
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.1.0...HEAD
660
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.0...HEAD
661
+ [1.2.0]: https://github.com/bymaxone/nest-logger/compare/v1.1.0...v1.2.0
354
662
  [1.1.0]: https://github.com/bymaxone/nest-logger/compare/v1.0.8...v1.1.0
355
663
  [1.0.8]: https://github.com/bymaxone/nest-logger/compare/v1.0.7...v1.0.8
356
664
  [1.0.7]: https://github.com/bymaxone/nest-logger/compare/v1.0.6...v1.0.7