@bymax-one/nest-logger 1.2.4 → 1.2.5
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 +94 -1
- package/README.md +24 -9
- package/dist/server/index.cjs +77 -71
- package/dist/server/index.mjs +77 -71
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,98 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.5] - 2026-08-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **An `Error` handed to the NestJS logger methods is no longer silently dropped.** Reported from a
|
|
19
|
+
real run rather than a unit test. `@bymax-one/nest-auth` logs the cause the way every caller does,
|
|
20
|
+
an SMTP `STARTTLS 502` failed, and this is what came out:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// the call site, nest-auth
|
|
24
|
+
this.logger.error(`delivery failed for "${subject}"`, error)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"level": "error",
|
|
30
|
+
"context": "DefaultAuthEmailProvider",
|
|
31
|
+
"msg": "delivery failed for \"Verify your email address\""
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
No `err`, no `stack`, no warning. The cause existed the whole time and was discarded. The dispatch
|
|
36
|
+
is fire-and-forget, so the HTTP response was `204` while nothing had been delivered: **the log was
|
|
37
|
+
the only surface where that failure could appear, and it appeared without its reason.** In CI you
|
|
38
|
+
can raise the level and re-run; in production nobody runs at `debug`, so the reason was never
|
|
39
|
+
written at all.
|
|
40
|
+
|
|
41
|
+
Neither side was obviously wrong, which is what made it survive. NestJS types the slot as
|
|
42
|
+
`error(message, stack?: string, context?: string)`, so this library read a string and discarded
|
|
43
|
+
anything else — implementing the contract faithfully. Passing the `Error` itself is what callers
|
|
44
|
+
actually do, because every other logger in the ecosystem accepts it.
|
|
45
|
+
|
|
46
|
+
**Measured before the fix: eleven of the twelve level/position combinations lost the `Error`.**
|
|
47
|
+
Only `error(err)` survived — every other level (`log`, `warn`, `debug`, `verbose`, `fatal`) routes
|
|
48
|
+
through a shared helper that handled no `Error` in either position. `fatal` was the worst of them,
|
|
49
|
+
being the level a caller reaches for when the process is about to die.
|
|
50
|
+
|
|
51
|
+
An `Error` anywhere in the variadic tail is now attached as `err`, the same field the leading-`Error`
|
|
52
|
+
branch already wrote, at every level. A trailing _string_ is still the `stack`, and `stack` is never
|
|
53
|
+
populated from an `Error` — the `err` serializer derives it, so writing both would emit the same
|
|
54
|
+
trace under two names.
|
|
55
|
+
|
|
56
|
+
**Detection is structural, not `instanceof`.** The first version of this fix used
|
|
57
|
+
`instanceof Error`, which is realm-local — so it would still have dropped a genuine `Error` built in
|
|
58
|
+
a `vm` context, and one already normalized into a plain `{ name, message, stack }`, which is what
|
|
59
|
+
`HttpExceptionFilter` produces and what any error crossing a process or worker boundary becomes.
|
|
60
|
+
That is the population most likely to be carrying the reason something failed, and this repository
|
|
61
|
+
had already recorded the same lesson in `sanitizeError` ("too narrow, and the gap was a live
|
|
62
|
+
defect"). The bridge now shares `isErrorLike` with `sanitizeError`, so the two cannot disagree about
|
|
63
|
+
what an error is. The bar stays deliberately narrow — string `name` AND string `message` — so an
|
|
64
|
+
ordinary metadata object is not mistaken for a cause.
|
|
65
|
+
|
|
66
|
+
**Two output changes to expect, since "additive" would overstate it.** Nothing is removed or
|
|
67
|
+
renamed, and a caller passing a string stack is untouched. But a caller who was passing an `Error`
|
|
68
|
+
necessarily sees a difference, because that is the defect:
|
|
69
|
+
|
|
70
|
+
- entries that silently dropped a cause now carry an `err` object — a **new field** on lines that
|
|
71
|
+
previously had none, which a strict log-pipeline schema will see;
|
|
72
|
+
- `warn(new Error('x'))` and the other non-`error` levels now emit `msg: "x"` instead of
|
|
73
|
+
`msg: "Error: x"`, matching what `error(new Error('x'))` already produced. A query grepping for
|
|
74
|
+
the `"Error: "` prefix on those levels will stop matching.
|
|
75
|
+
|
|
76
|
+
Both are the point of the change rather than side effects, but neither is invisible.
|
|
77
|
+
|
|
78
|
+
Scope, as measured by the reporting consumer across the ten `@bymax-one/*` packages that backend
|
|
79
|
+
installs at its pinned versions: **37 affected call sites, all of them in `nest-auth`**, none
|
|
80
|
+
elsewhere. Two are security-relevant — a breach check that fails open and logs that it admitted a
|
|
81
|
+
password it could not verify, and the `onRefreshTokenReuseDetected` hook, the tripwire for a stolen
|
|
82
|
+
token. Both said something went wrong and threw away what.
|
|
83
|
+
|
|
84
|
+
### Documentation
|
|
85
|
+
|
|
86
|
+
- **The `pnpm prune --prod` note named the wrong mechanism.** It described the peer surviving as a
|
|
87
|
+
_residue_ of an incomplete prune. A second consumer whose Dockerfile never prunes — a clean
|
|
88
|
+
`pnpm install --prod --frozen-lockfile` in a fresh runtime stage — measured the peer present anyway.
|
|
89
|
+
In both images the peer was recorded in the lockfile and pnpm placed it in the store for a
|
|
90
|
+
production-only install. The distinction matters because "we do a clean prod install, not a prune"
|
|
91
|
+
reads like an exemption and is not one; it is what that consumer expected before measuring their
|
|
92
|
+
own image.
|
|
93
|
+
|
|
94
|
+
The note is now phrased as **two measurements rather than a law of pnpm** — whether a prod install
|
|
95
|
+
always carries an optional peer, however the lockfile was produced, has not been tested here. The
|
|
96
|
+
first version of this note over-generalized from the evidence, which is the same class of error as
|
|
97
|
+
the two corrections it documents.
|
|
98
|
+
|
|
99
|
+
The note now also says what is different **elsewhere**: under npm's or yarn's flat `node_modules` a
|
|
100
|
+
pruned devDependency really is gone, so the warning must not be read as universal. And it records
|
|
101
|
+
the probe that nearly produced the opposite conclusion — `require.resolve` from the library's
|
|
102
|
+
app-visible path returns `MODULE_NOT_FOUND` while the lazy import still succeeds, because the
|
|
103
|
+
library resolves from its real `.pnpm/` path. Verify by behaviour, not from a path the library
|
|
104
|
+
never uses.
|
|
105
|
+
|
|
14
106
|
## [1.2.4] - 2026-08-14
|
|
15
107
|
|
|
16
108
|
### Documentation
|
|
@@ -922,7 +1014,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
922
1014
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
923
1015
|
`release.yml`, Dependabot, and issue templates
|
|
924
1016
|
|
|
925
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
1017
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...HEAD
|
|
1018
|
+
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
|
926
1019
|
[1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
|
|
927
1020
|
[1.2.3]: https://github.com/bymaxone/nest-logger/compare/v1.2.2...v1.2.3
|
|
928
1021
|
[1.2.2]: https://github.com/bymaxone/nest-logger/compare/v1.2.1...v1.2.2
|
package/README.md
CHANGED
|
@@ -149,16 +149,31 @@ pnpm add @opentelemetry/api @opentelemetry/sdk-node
|
|
|
149
149
|
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
> ⚠️
|
|
153
|
-
>
|
|
154
|
-
>
|
|
155
|
-
>
|
|
156
|
-
>
|
|
152
|
+
> ⚠️ **Under pnpm, `-D` may not make it absent in production — measure, do not assume.** Two
|
|
153
|
+
> consumers who had assumed otherwise measured the peer **present** in their real production images,
|
|
154
|
+
> by two different build routes: `pnpm prune --prod` keeps the store entry, and a clean
|
|
155
|
+
> `pnpm install --prod --frozen-lockfile` in a fresh stage installs it too. In both, the peer was
|
|
156
|
+
> recorded in the lockfile. `PrettyDevDestination` imports it lazily, relative to the library's own
|
|
157
|
+
> directory under `.pnpm/`, where it is a sibling — so it resolves.
|
|
157
158
|
>
|
|
158
|
-
>
|
|
159
|
-
>
|
|
160
|
-
>
|
|
161
|
-
>
|
|
159
|
+
> That is two measurements, not a law of pnpm: whether a production install always carries an optional
|
|
160
|
+
> peer, however the lockfile was produced, has not been tested here. The advice below does not depend
|
|
161
|
+
> on it.
|
|
162
|
+
>
|
|
163
|
+
> The clean-install case is worth stating separately, because "we do a fresh prod install, not a
|
|
164
|
+
> prune" reads like an exemption and is not one. It is what one of those consumers concluded before
|
|
165
|
+
> measuring their own image.
|
|
166
|
+
>
|
|
167
|
+
> **On other package managers this is different**, and the difference cuts the other way: under npm's
|
|
168
|
+
> or yarn's flat `node_modules`, a pruned devDependency really is gone. If you install with those,
|
|
169
|
+
> do not read this warning as universal.
|
|
170
|
+
>
|
|
171
|
+
> Either way, the rule that survives both layouts: **"it is a devDependency, therefore this path
|
|
172
|
+
> cannot run in production" is not a safe premise, because the premise is about someone else's
|
|
173
|
+
> install command.** A `PrettyDevDestination` left registered in production will not crash and will
|
|
174
|
+
> not warn — it renders ANSI colour into a log pipeline that has silently failed to parse every line
|
|
175
|
+
> since the deploy, which is indistinguishable from a service that went quiet. Gate it on an explicit
|
|
176
|
+
> configuration value you can see, not on the packaging.
|
|
162
177
|
|
|
163
178
|
> [!NOTE]
|
|
164
179
|
> `@opentelemetry/api` is resolved lazily when the Pino instance is built. When it is absent the trace mixin silently steps aside — the logger never fails to start, and never warns, over an optional peer.
|
package/dist/server/index.cjs
CHANGED
|
@@ -228,6 +228,75 @@ function escapeControlCharacters(text) {
|
|
|
228
228
|
return text.replace(UNSAFE_IN_MULTILINE, toEscapeSequence);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// src/server/utils/sanitize-error.util.ts
|
|
232
|
+
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
233
|
+
var MAX_AGGREGATE_ERRORS = 10;
|
|
234
|
+
var CIRCULAR_SENTINEL = "[Circular]";
|
|
235
|
+
function sanitizeError(err, options) {
|
|
236
|
+
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
237
|
+
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
238
|
+
}
|
|
239
|
+
function isErrorLike(value) {
|
|
240
|
+
if (typeof value !== "object" || value === null) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const candidate = value;
|
|
244
|
+
return typeof candidate.name === "string" && typeof candidate.message === "string";
|
|
245
|
+
}
|
|
246
|
+
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
247
|
+
try {
|
|
248
|
+
if (!isErrorLike(value)) {
|
|
249
|
+
return { name: "UnknownError", message: String(value) };
|
|
250
|
+
}
|
|
251
|
+
seen.add(value);
|
|
252
|
+
const result = { name: value.name, message: value.message };
|
|
253
|
+
if (value.stack !== void 0) {
|
|
254
|
+
result.stack = scrubStack(value.stack);
|
|
255
|
+
}
|
|
256
|
+
const isAtDepthLimit = depth2 >= maxDepth;
|
|
257
|
+
if (value.cause !== void 0) {
|
|
258
|
+
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
|
|
259
|
+
}
|
|
260
|
+
const aggregated = readAggregatedErrors(value);
|
|
261
|
+
if (aggregated !== void 0) {
|
|
262
|
+
const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
|
|
263
|
+
result.errors = capped.map(
|
|
264
|
+
(inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
|
|
265
|
+
);
|
|
266
|
+
if (aggregated.length > MAX_AGGREGATE_ERRORS) {
|
|
267
|
+
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
} catch {
|
|
272
|
+
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
276
|
+
if (isObject(value) && seen.has(value)) {
|
|
277
|
+
return CIRCULAR_SENTINEL;
|
|
278
|
+
}
|
|
279
|
+
return toSanitizedError(value, seen, depth2, maxDepth);
|
|
280
|
+
}
|
|
281
|
+
function readAggregatedErrors(value) {
|
|
282
|
+
const candidate = value.errors;
|
|
283
|
+
return Array.isArray(candidate) ? candidate : void 0;
|
|
284
|
+
}
|
|
285
|
+
function scrubStack(stack) {
|
|
286
|
+
return escapeControlCharacters(
|
|
287
|
+
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
function isObject(value) {
|
|
291
|
+
return typeof value === "object" && value !== null;
|
|
292
|
+
}
|
|
293
|
+
function createDepthMarker() {
|
|
294
|
+
return { _truncated: true, _reason: "cause-depth-exceeded" };
|
|
295
|
+
}
|
|
296
|
+
function createWidthMarker(omitted) {
|
|
297
|
+
return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
|
|
298
|
+
}
|
|
299
|
+
|
|
231
300
|
// src/server/services/pino-logger.service.ts
|
|
232
301
|
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
233
302
|
function withoutOwnedKeys(metadata) {
|
|
@@ -256,6 +325,9 @@ function safeErrorMessage(error) {
|
|
|
256
325
|
return "Unreadable error message";
|
|
257
326
|
}
|
|
258
327
|
}
|
|
328
|
+
function findErrorParam(optionalParams) {
|
|
329
|
+
return optionalParams.find((param) => isErrorLike(param));
|
|
330
|
+
}
|
|
259
331
|
function assignIfDefined(payload, key, value) {
|
|
260
332
|
if (value !== void 0) {
|
|
261
333
|
Reflect.defineProperty(payload, key, { value, enumerable: true });
|
|
@@ -292,7 +364,7 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
292
364
|
* @param optionalParams - NestJS trailing params (`stack` at index 0, `context` at index 1).
|
|
293
365
|
*/
|
|
294
366
|
error(message, ...optionalParams) {
|
|
295
|
-
if (message
|
|
367
|
+
if (isErrorLike(message)) {
|
|
296
368
|
const payload2 = { err: message };
|
|
297
369
|
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
298
370
|
this.pino.error(payload2, safeErrorMessage(message));
|
|
@@ -303,6 +375,7 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
303
375
|
const payload = {};
|
|
304
376
|
assignIfDefined(payload, "context", context);
|
|
305
377
|
assignIfDefined(payload, "stack", stack);
|
|
378
|
+
assignIfDefined(payload, "err", findErrorParam(optionalParams));
|
|
306
379
|
this.pino.error(
|
|
307
380
|
payload,
|
|
308
381
|
toSingleLineMessage(typeof message === "string" ? message : String(message))
|
|
@@ -461,7 +534,9 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
461
534
|
emitNestStyle(level, message, optionalParams) {
|
|
462
535
|
const payload = {};
|
|
463
536
|
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
464
|
-
const
|
|
537
|
+
const leading = isErrorLike(message) ? message : void 0;
|
|
538
|
+
assignIfDefined(payload, "err", leading ?? findErrorParam(optionalParams));
|
|
539
|
+
const msg = leading === void 0 ? toSingleLineMessage(typeof message === "string" ? message : String(message)) : safeErrorMessage(leading);
|
|
465
540
|
switch (level) {
|
|
466
541
|
case "info":
|
|
467
542
|
this.pino.info(payload, msg);
|
|
@@ -548,75 +623,6 @@ function stripQueryString(url) {
|
|
|
548
623
|
return queryStart === -1 ? url : url.slice(0, queryStart);
|
|
549
624
|
}
|
|
550
625
|
|
|
551
|
-
// src/server/utils/sanitize-error.util.ts
|
|
552
|
-
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
553
|
-
var MAX_AGGREGATE_ERRORS = 10;
|
|
554
|
-
var CIRCULAR_SENTINEL = "[Circular]";
|
|
555
|
-
function sanitizeError(err, options) {
|
|
556
|
-
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
557
|
-
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
558
|
-
}
|
|
559
|
-
function isErrorLike(value) {
|
|
560
|
-
if (typeof value !== "object" || value === null) {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
const candidate = value;
|
|
564
|
-
return typeof candidate.name === "string" && typeof candidate.message === "string";
|
|
565
|
-
}
|
|
566
|
-
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
567
|
-
try {
|
|
568
|
-
if (!isErrorLike(value)) {
|
|
569
|
-
return { name: "UnknownError", message: String(value) };
|
|
570
|
-
}
|
|
571
|
-
seen.add(value);
|
|
572
|
-
const result = { name: value.name, message: value.message };
|
|
573
|
-
if (value.stack !== void 0) {
|
|
574
|
-
result.stack = scrubStack(value.stack);
|
|
575
|
-
}
|
|
576
|
-
const isAtDepthLimit = depth2 >= maxDepth;
|
|
577
|
-
if (value.cause !== void 0) {
|
|
578
|
-
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
|
|
579
|
-
}
|
|
580
|
-
const aggregated = readAggregatedErrors(value);
|
|
581
|
-
if (aggregated !== void 0) {
|
|
582
|
-
const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
|
|
583
|
-
result.errors = capped.map(
|
|
584
|
-
(inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
|
|
585
|
-
);
|
|
586
|
-
if (aggregated.length > MAX_AGGREGATE_ERRORS) {
|
|
587
|
-
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return result;
|
|
591
|
-
} catch {
|
|
592
|
-
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
596
|
-
if (isObject(value) && seen.has(value)) {
|
|
597
|
-
return CIRCULAR_SENTINEL;
|
|
598
|
-
}
|
|
599
|
-
return toSanitizedError(value, seen, depth2, maxDepth);
|
|
600
|
-
}
|
|
601
|
-
function readAggregatedErrors(value) {
|
|
602
|
-
const candidate = value.errors;
|
|
603
|
-
return Array.isArray(candidate) ? candidate : void 0;
|
|
604
|
-
}
|
|
605
|
-
function scrubStack(stack) {
|
|
606
|
-
return escapeControlCharacters(
|
|
607
|
-
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
function isObject(value) {
|
|
611
|
-
return typeof value === "object" && value !== null;
|
|
612
|
-
}
|
|
613
|
-
function createDepthMarker() {
|
|
614
|
-
return { _truncated: true, _reason: "cause-depth-exceeded" };
|
|
615
|
-
}
|
|
616
|
-
function createWidthMarker(omitted) {
|
|
617
|
-
return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
626
|
// src/server/filters/http-exception.filter.ts
|
|
621
627
|
var INTERNAL_ERROR_BODY = {
|
|
622
628
|
statusCode: common.HttpStatus.INTERNAL_SERVER_ERROR,
|
package/dist/server/index.mjs
CHANGED
|
@@ -225,6 +225,75 @@ function escapeControlCharacters(text) {
|
|
|
225
225
|
return text.replace(UNSAFE_IN_MULTILINE, toEscapeSequence);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// src/server/utils/sanitize-error.util.ts
|
|
229
|
+
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
230
|
+
var MAX_AGGREGATE_ERRORS = 10;
|
|
231
|
+
var CIRCULAR_SENTINEL = "[Circular]";
|
|
232
|
+
function sanitizeError(err, options) {
|
|
233
|
+
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
234
|
+
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
235
|
+
}
|
|
236
|
+
function isErrorLike(value) {
|
|
237
|
+
if (typeof value !== "object" || value === null) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const candidate = value;
|
|
241
|
+
return typeof candidate.name === "string" && typeof candidate.message === "string";
|
|
242
|
+
}
|
|
243
|
+
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
244
|
+
try {
|
|
245
|
+
if (!isErrorLike(value)) {
|
|
246
|
+
return { name: "UnknownError", message: String(value) };
|
|
247
|
+
}
|
|
248
|
+
seen.add(value);
|
|
249
|
+
const result = { name: value.name, message: value.message };
|
|
250
|
+
if (value.stack !== void 0) {
|
|
251
|
+
result.stack = scrubStack(value.stack);
|
|
252
|
+
}
|
|
253
|
+
const isAtDepthLimit = depth2 >= maxDepth;
|
|
254
|
+
if (value.cause !== void 0) {
|
|
255
|
+
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
|
|
256
|
+
}
|
|
257
|
+
const aggregated = readAggregatedErrors(value);
|
|
258
|
+
if (aggregated !== void 0) {
|
|
259
|
+
const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
|
|
260
|
+
result.errors = capped.map(
|
|
261
|
+
(inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
|
|
262
|
+
);
|
|
263
|
+
if (aggregated.length > MAX_AGGREGATE_ERRORS) {
|
|
264
|
+
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
} catch {
|
|
269
|
+
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
273
|
+
if (isObject(value) && seen.has(value)) {
|
|
274
|
+
return CIRCULAR_SENTINEL;
|
|
275
|
+
}
|
|
276
|
+
return toSanitizedError(value, seen, depth2, maxDepth);
|
|
277
|
+
}
|
|
278
|
+
function readAggregatedErrors(value) {
|
|
279
|
+
const candidate = value.errors;
|
|
280
|
+
return Array.isArray(candidate) ? candidate : void 0;
|
|
281
|
+
}
|
|
282
|
+
function scrubStack(stack) {
|
|
283
|
+
return escapeControlCharacters(
|
|
284
|
+
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
function isObject(value) {
|
|
288
|
+
return typeof value === "object" && value !== null;
|
|
289
|
+
}
|
|
290
|
+
function createDepthMarker() {
|
|
291
|
+
return { _truncated: true, _reason: "cause-depth-exceeded" };
|
|
292
|
+
}
|
|
293
|
+
function createWidthMarker(omitted) {
|
|
294
|
+
return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
|
|
295
|
+
}
|
|
296
|
+
|
|
228
297
|
// src/server/services/pino-logger.service.ts
|
|
229
298
|
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
230
299
|
function withoutOwnedKeys(metadata) {
|
|
@@ -253,6 +322,9 @@ function safeErrorMessage(error) {
|
|
|
253
322
|
return "Unreadable error message";
|
|
254
323
|
}
|
|
255
324
|
}
|
|
325
|
+
function findErrorParam(optionalParams) {
|
|
326
|
+
return optionalParams.find((param) => isErrorLike(param));
|
|
327
|
+
}
|
|
256
328
|
function assignIfDefined(payload, key, value) {
|
|
257
329
|
if (value !== void 0) {
|
|
258
330
|
Reflect.defineProperty(payload, key, { value, enumerable: true });
|
|
@@ -289,7 +361,7 @@ var PinoLoggerService = class {
|
|
|
289
361
|
* @param optionalParams - NestJS trailing params (`stack` at index 0, `context` at index 1).
|
|
290
362
|
*/
|
|
291
363
|
error(message, ...optionalParams) {
|
|
292
|
-
if (message
|
|
364
|
+
if (isErrorLike(message)) {
|
|
293
365
|
const payload2 = { err: message };
|
|
294
366
|
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
295
367
|
this.pino.error(payload2, safeErrorMessage(message));
|
|
@@ -300,6 +372,7 @@ var PinoLoggerService = class {
|
|
|
300
372
|
const payload = {};
|
|
301
373
|
assignIfDefined(payload, "context", context);
|
|
302
374
|
assignIfDefined(payload, "stack", stack);
|
|
375
|
+
assignIfDefined(payload, "err", findErrorParam(optionalParams));
|
|
303
376
|
this.pino.error(
|
|
304
377
|
payload,
|
|
305
378
|
toSingleLineMessage(typeof message === "string" ? message : String(message))
|
|
@@ -458,7 +531,9 @@ var PinoLoggerService = class {
|
|
|
458
531
|
emitNestStyle(level, message, optionalParams) {
|
|
459
532
|
const payload = {};
|
|
460
533
|
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
461
|
-
const
|
|
534
|
+
const leading = isErrorLike(message) ? message : void 0;
|
|
535
|
+
assignIfDefined(payload, "err", leading ?? findErrorParam(optionalParams));
|
|
536
|
+
const msg = leading === void 0 ? toSingleLineMessage(typeof message === "string" ? message : String(message)) : safeErrorMessage(leading);
|
|
462
537
|
switch (level) {
|
|
463
538
|
case "info":
|
|
464
539
|
this.pino.info(payload, msg);
|
|
@@ -545,75 +620,6 @@ function stripQueryString(url) {
|
|
|
545
620
|
return queryStart === -1 ? url : url.slice(0, queryStart);
|
|
546
621
|
}
|
|
547
622
|
|
|
548
|
-
// src/server/utils/sanitize-error.util.ts
|
|
549
|
-
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
550
|
-
var MAX_AGGREGATE_ERRORS = 10;
|
|
551
|
-
var CIRCULAR_SENTINEL = "[Circular]";
|
|
552
|
-
function sanitizeError(err, options) {
|
|
553
|
-
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
554
|
-
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
555
|
-
}
|
|
556
|
-
function isErrorLike(value) {
|
|
557
|
-
if (typeof value !== "object" || value === null) {
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
const candidate = value;
|
|
561
|
-
return typeof candidate.name === "string" && typeof candidate.message === "string";
|
|
562
|
-
}
|
|
563
|
-
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
564
|
-
try {
|
|
565
|
-
if (!isErrorLike(value)) {
|
|
566
|
-
return { name: "UnknownError", message: String(value) };
|
|
567
|
-
}
|
|
568
|
-
seen.add(value);
|
|
569
|
-
const result = { name: value.name, message: value.message };
|
|
570
|
-
if (value.stack !== void 0) {
|
|
571
|
-
result.stack = scrubStack(value.stack);
|
|
572
|
-
}
|
|
573
|
-
const isAtDepthLimit = depth2 >= maxDepth;
|
|
574
|
-
if (value.cause !== void 0) {
|
|
575
|
-
result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
|
|
576
|
-
}
|
|
577
|
-
const aggregated = readAggregatedErrors(value);
|
|
578
|
-
if (aggregated !== void 0) {
|
|
579
|
-
const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
|
|
580
|
-
result.errors = capped.map(
|
|
581
|
-
(inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
|
|
582
|
-
);
|
|
583
|
-
if (aggregated.length > MAX_AGGREGATE_ERRORS) {
|
|
584
|
-
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return result;
|
|
588
|
-
} catch {
|
|
589
|
-
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
593
|
-
if (isObject(value) && seen.has(value)) {
|
|
594
|
-
return CIRCULAR_SENTINEL;
|
|
595
|
-
}
|
|
596
|
-
return toSanitizedError(value, seen, depth2, maxDepth);
|
|
597
|
-
}
|
|
598
|
-
function readAggregatedErrors(value) {
|
|
599
|
-
const candidate = value.errors;
|
|
600
|
-
return Array.isArray(candidate) ? candidate : void 0;
|
|
601
|
-
}
|
|
602
|
-
function scrubStack(stack) {
|
|
603
|
-
return escapeControlCharacters(
|
|
604
|
-
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
function isObject(value) {
|
|
608
|
-
return typeof value === "object" && value !== null;
|
|
609
|
-
}
|
|
610
|
-
function createDepthMarker() {
|
|
611
|
-
return { _truncated: true, _reason: "cause-depth-exceeded" };
|
|
612
|
-
}
|
|
613
|
-
function createWidthMarker(omitted) {
|
|
614
|
-
return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
623
|
// src/server/filters/http-exception.filter.ts
|
|
618
624
|
var INTERNAL_ERROR_BODY = {
|
|
619
625
|
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
package/package.json
CHANGED