@bymax-one/nest-logger 1.2.6 → 1.2.7
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 +104 -2
- package/README.md +36 -1
- package/dist/server/index.cjs +31 -23
- package/dist/server/index.d.cts +17 -2
- package/dist/server/index.d.ts +17 -2
- package/dist/server/index.mjs +31 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,107 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.7] - 2026-08-15
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **Serializer output is redacted by name under `redactStrategy: 'paths'` too.** Carrying an
|
|
19
|
+
error's own properties at every depth of the `cause` chain (below) opened a hole in that legacy
|
|
20
|
+
mode: the name walk is a pass-through there, so redaction fell entirely to `fast-redact`, and
|
|
21
|
+
`DEFAULT_REDACT_PATHS` reaches `*.*.*.*.password` — four wildcard levels. A serialized cause chain
|
|
22
|
+
goes deeper. Measured on the built artifact with a password on the deepest link of
|
|
23
|
+
`err.errors[0].cause.cause`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "cause": { "name": "Error", "message": "deep", "password": "LEAK-deep" } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Confirmed absent from `1.2.6`, where the field was not carried at all — so this was a regression
|
|
30
|
+
introduced inside this release rather than an inherited one, and it never reached a published
|
|
31
|
+
artifact.
|
|
32
|
+
|
|
33
|
+
`redactStrategy` is the consumer's choice about **their own payload**; what a serializer
|
|
34
|
+
synthesizes is this library's output, and the name walk — which has no depth ceiling — now covers
|
|
35
|
+
it either way. The caller's record is untouched by this: `'paths'` still means exactly what it
|
|
36
|
+
documented, four-level ceiling included, and the e2e case that pins that ceiling is what caught
|
|
37
|
+
the first attempt at this fix collapsing both hooks onto one redactor.
|
|
38
|
+
|
|
39
|
+
Reported by Copilot on the pull request that introduced the deeper copy.
|
|
40
|
+
|
|
41
|
+
- **An own `__proto__` key on an error is now stored as data instead of being applied as a
|
|
42
|
+
prototype.** The own-property copy assigned each key, and `__proto__` is an accessor inherited
|
|
43
|
+
from `Object.prototype` — so the assignment walked the chain, invoked that setter, and replaced
|
|
44
|
+
the sanitized node's prototype. An error-like object arriving from JSON (a worker boundary, a
|
|
45
|
+
queue, an HTTP body) carries `__proto__` as an ordinary own enumerable key, and `Object.entries`
|
|
46
|
+
hands it over like any other.
|
|
47
|
+
|
|
48
|
+
Measured rather than assumed. `Object.prototype` itself is never touched, so this is prototype
|
|
49
|
+
injection into a single log node rather than global pollution, and with a JSON-sourced payload the
|
|
50
|
+
emitted line was unchanged — `JSON.stringify` ignores inherited fields. The live hazard is the
|
|
51
|
+
node: given `{"__proto__": null}` it loses `hasOwnProperty` and `toString`, which then **throw**
|
|
52
|
+
for anything downstream that touches them, inside the one path whose contract is that logging
|
|
53
|
+
never crashes the application.
|
|
54
|
+
|
|
55
|
+
The copy now defines an own data property, which makes the key inert and keeps its value — an
|
|
56
|
+
error that genuinely carries a `__proto__` field gets it logged as the data it is. Reported by
|
|
57
|
+
Copilot on the pull request that introduced the deeper copy.
|
|
58
|
+
|
|
59
|
+
### Documentation
|
|
60
|
+
|
|
61
|
+
- **`ignore` needs an escaped dot for `event.name`, and every example here taught the opposite.**
|
|
62
|
+
`pino-pretty` reads a dot as a nested path, and this library emits `event.name` as a literal
|
|
63
|
+
top-level key — so `ignore: '…,event.name'` looks for an `event` object with a `name` inside,
|
|
64
|
+
finds neither, and hides nothing without a word of complaint. Measured on one entry with only the
|
|
65
|
+
option changing:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
69
|
+
ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Found by a consumer who copied the `1.2.6` example verbatim, configured it, and saw the field
|
|
73
|
+
still on screen. The escaped form is now in the `PrettyViewOptions` JSDoc — where an editor's
|
|
74
|
+
autocomplete shows it, which is where someone actually is when writing the option — in the README,
|
|
75
|
+
and in the `1.2.6` example itself, which is corrected rather than left to keep teaching it.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **An error's own properties now survive at every depth of the `cause` chain, not just at the top.**
|
|
80
|
+
`code`, `statusCode` and any domain field an application attaches were copied onto the error handed
|
|
81
|
+
to the log call and **dropped the moment that same error was wrapped as someone else's `cause`** —
|
|
82
|
+
which is what a `catch` in `main.ts` does routinely. Measured on the published `1.2.6`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"type": "Error",
|
|
87
|
+
"message": "bootstrap failed",
|
|
88
|
+
"code": "EBOOT",
|
|
89
|
+
"cause": { "name": "Error", "message": "config invalid" }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The inner error carried `code: "BYMAX_CONFIG_VALIDATION"` and an `issues` array holding one entry
|
|
94
|
+
per invalid variable. Both vanished. The reporting consumer framed the severity exactly right:
|
|
95
|
+
because `message` survives and their message is the full aggregated report, **the human stayed
|
|
96
|
+
served while the machine went blind** — the text an operator reads was intact, and the fields an
|
|
97
|
+
alert or a dashboard keys on were gone.
|
|
98
|
+
|
|
99
|
+
Nothing justified the asymmetry. The reason own properties are "part of the contract" at the top —
|
|
100
|
+
`pino.stdSerializers.err` copied them, so dropping them is a silent compatibility loss — is the
|
|
101
|
+
same reason one level down, and a wrapped error is the common case rather than the exotic one.
|
|
102
|
+
They still never shadow a field the serializer derives, and they still pass through the same
|
|
103
|
+
name-based redaction and size bound as any other serializer output.
|
|
104
|
+
|
|
105
|
+
Found by following up an asymmetry noticed while answering a consumer's question about how to pass
|
|
106
|
+
an error through the NestJS bridge — not by a bug report.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- **The own-property copy lives in one place instead of two.** The `err` serializer read the raw
|
|
111
|
+
thrown value and walked its properties itself, duplicating what the sanitizer now does at every
|
|
112
|
+
node; it copies from the sanitized node instead. Same fields, one walk — and no second copy to
|
|
113
|
+
drift away from the first.
|
|
114
|
+
|
|
14
115
|
## [1.2.6] - 2026-08-15
|
|
15
116
|
|
|
16
117
|
### Added
|
|
@@ -30,7 +131,7 @@ heading here.
|
|
|
30
131
|
|
|
31
132
|
```ts
|
|
32
133
|
new PrettyDevDestination({
|
|
33
|
-
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event
|
|
134
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
34
135
|
})
|
|
35
136
|
```
|
|
36
137
|
|
|
@@ -1130,7 +1231,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1130
1231
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1131
1232
|
`release.yml`, Dependabot, and issue templates
|
|
1132
1233
|
|
|
1133
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
1234
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...HEAD
|
|
1235
|
+
[1.2.7]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...v1.2.7
|
|
1134
1236
|
[1.2.6]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...v1.2.6
|
|
1135
1237
|
[1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
|
|
1136
1238
|
[1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
|
package/README.md
CHANGED
|
@@ -534,6 +534,32 @@ any other field. `AggregateError` members reach the entry as `err.errors`. There
|
|
|
534
534
|
OpenTelemetry attribute for a cause chain, so these stay namespaced under `err` rather than
|
|
535
535
|
inventing an `exception.cause` the spec does not define.
|
|
536
536
|
|
|
537
|
+
**An error's own enumerable properties travel with it at every depth** — `code` on a Node system
|
|
538
|
+
error, `statusCode` from an HTTP layer, whatever domain fields you attached — including on a nested
|
|
539
|
+
`cause` and on each `AggregateError` member:
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{
|
|
543
|
+
"err": {
|
|
544
|
+
"type": "Error",
|
|
545
|
+
"message": "bootstrap failed",
|
|
546
|
+
"code": "EBOOT",
|
|
547
|
+
"cause": {
|
|
548
|
+
"name": "Error",
|
|
549
|
+
"message": "config invalid",
|
|
550
|
+
"code": "BYMAX_CONFIG_VALIDATION",
|
|
551
|
+
"issues": [{ "variable": "DATABASE_URL", "code": "invalid_url" }]
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
This matters because `code` is usually the field you alert on. Until 1.2.7 those properties survived
|
|
558
|
+
only on the error handed to the log call and were dropped the moment the same error was wrapped as
|
|
559
|
+
someone else's `cause` — which is what a `catch` in `main.ts` does routinely. They never shadow a
|
|
560
|
+
field the serializer derives, and they pass through the same name-based redaction and size bound as
|
|
561
|
+
any other serializer output.
|
|
562
|
+
|
|
537
563
|
An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
|
|
538
564
|
Pino's standard serializer did.
|
|
539
565
|
|
|
@@ -760,7 +786,7 @@ So the common accident — adding `new PrettyDevDestination()` without installin
|
|
|
760
786
|
```typescript
|
|
761
787
|
// One line per entry, with the fields your project repeats on every line hidden.
|
|
762
788
|
new PrettyDevDestination({
|
|
763
|
-
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event
|
|
789
|
+
view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
764
790
|
})
|
|
765
791
|
|
|
766
792
|
// Message only, with the context pulled back into the line.
|
|
@@ -780,6 +806,15 @@ new PrettyDevDestination({
|
|
|
780
806
|
|
|
781
807
|
> **`hideObject` hides it everywhere.** In pretty mode there is no JSON copy behind the rendering, because `destinations` **replaces** stdout — so a field hidden here, `logKey` included, is not visible anywhere. That is what the option is for; `messageFormat` is how you pull a specific field back.
|
|
782
808
|
|
|
809
|
+
> **A dot in `ignore` means "nested path", so a field name that CONTAINS a dot must escape it.** This library emits `event.name` as a literal top-level key, so the obvious spelling hides nothing at all — `pino-pretty` looks for an `event` object with a `name` inside, finds neither, and says nothing. Measured on one entry with only the option changing:
|
|
810
|
+
>
|
|
811
|
+
> ```
|
|
812
|
+
> ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
813
|
+
> ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
814
|
+
> ```
|
|
815
|
+
>
|
|
816
|
+
> In a TypeScript string literal that is `'event\\.name'` — one real backslash reaching `pino-pretty`. `service` and `deployment` need no escape; they really are nested objects. Reported by a consumer who copied the example above when it still had the unescaped form.
|
|
817
|
+
|
|
783
818
|
The shape is exported as `PrettyViewOptions` when you want to build the view separately — it is this library's own interface, not a re-export of `pino-pretty`'s `PrettyOptions`, so type-checking without the optional peer installed still resolves.
|
|
784
819
|
|
|
785
820
|
> **`destination` is not exposed, by design.** The library owns where entries go — a redirected stream would route around the fan-out and the last-resort rescue above. It is absent from the type and applied after your options are merged, so it cannot be overridden from untyped JavaScript either.
|
package/dist/server/index.cjs
CHANGED
|
@@ -232,6 +232,13 @@ function escapeControlCharacters(text) {
|
|
|
232
232
|
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
233
233
|
var MAX_AGGREGATE_ERRORS = 10;
|
|
234
234
|
var CIRCULAR_SENTINEL = "[Circular]";
|
|
235
|
+
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
236
|
+
"name",
|
|
237
|
+
"message",
|
|
238
|
+
"stack",
|
|
239
|
+
"cause",
|
|
240
|
+
"errors"
|
|
241
|
+
]);
|
|
235
242
|
function sanitizeError(err, options) {
|
|
236
243
|
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
237
244
|
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
@@ -267,11 +274,23 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
|
267
274
|
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
268
275
|
}
|
|
269
276
|
}
|
|
277
|
+
assignOwnErrorFields(result, value);
|
|
270
278
|
return result;
|
|
271
279
|
} catch {
|
|
272
280
|
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
273
281
|
}
|
|
274
282
|
}
|
|
283
|
+
function assignOwnErrorFields(target, source) {
|
|
284
|
+
try {
|
|
285
|
+
const own = Array.isArray(source) ? {} : source;
|
|
286
|
+
for (const [key, value] of Object.entries(own)) {
|
|
287
|
+
if (!Object.hasOwn(target, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
288
|
+
Object.defineProperty(target, key, { value, enumerable: true });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
275
294
|
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
276
295
|
if (isObject(value) && seen.has(value)) {
|
|
277
296
|
return CIRCULAR_SENTINEL;
|
|
@@ -1401,13 +1420,6 @@ function leafNameOf(path) {
|
|
|
1401
1420
|
}
|
|
1402
1421
|
return leaf;
|
|
1403
1422
|
}
|
|
1404
|
-
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
1405
|
-
"name",
|
|
1406
|
-
"message",
|
|
1407
|
-
"stack",
|
|
1408
|
-
"cause",
|
|
1409
|
-
"errors"
|
|
1410
|
-
]);
|
|
1411
1423
|
function serializeErrorValue(value_) {
|
|
1412
1424
|
const sanitized = sanitizeError(value_);
|
|
1413
1425
|
const serialized = {
|
|
@@ -1424,18 +1436,7 @@ function serializeErrorValue(value_) {
|
|
|
1424
1436
|
if (sanitized.errors !== void 0) {
|
|
1425
1437
|
serialized["errors"] = sanitized.errors;
|
|
1426
1438
|
}
|
|
1427
|
-
|
|
1428
|
-
const source = (
|
|
1429
|
-
// Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
|
|
1430
|
-
typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
|
|
1431
|
-
);
|
|
1432
|
-
for (const [key, value] of Object.entries(source)) {
|
|
1433
|
-
if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
1434
|
-
Reflect.set(serialized, key, value);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
} catch {
|
|
1438
|
-
}
|
|
1439
|
+
assignOwnErrorFields(serialized, sanitized);
|
|
1439
1440
|
return serialized;
|
|
1440
1441
|
}
|
|
1441
1442
|
function withSemconvException(record, format) {
|
|
@@ -1485,6 +1486,9 @@ function resolveNameRedactor(options) {
|
|
|
1485
1486
|
if (options.redactStrategy !== "names") {
|
|
1486
1487
|
return PASS_THROUGH;
|
|
1487
1488
|
}
|
|
1489
|
+
return buildNameRedactor(options);
|
|
1490
|
+
}
|
|
1491
|
+
function buildNameRedactor(options) {
|
|
1488
1492
|
const names = [
|
|
1489
1493
|
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1490
1494
|
...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
|
|
@@ -1503,8 +1507,9 @@ function resolveRedactOption(options) {
|
|
|
1503
1507
|
}
|
|
1504
1508
|
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1505
1509
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1506
|
-
const
|
|
1507
|
-
const
|
|
1510
|
+
const redactRecord = resolveNameRedactor(options);
|
|
1511
|
+
const redactSerialized = buildNameRedactor(options);
|
|
1512
|
+
const bound = (serializer) => createSizeBoundedSerializer((input) => redactSerialized(serializer(input)), maxBytes);
|
|
1508
1513
|
const serializers = {
|
|
1509
1514
|
err: bound(serializeErrorValue),
|
|
1510
1515
|
...Object.fromEntries(
|
|
@@ -1541,14 +1546,17 @@ function buildPinoInstance(options, logContext, destinations, health) {
|
|
|
1541
1546
|
// consumer's extra metadata instead of silently dropping it. Runs ONCE, at
|
|
1542
1547
|
// logger construction — no per-entry cost. `true`: bindings are a record
|
|
1543
1548
|
// root, iterated rather than serialized.
|
|
1544
|
-
bindings: (bindings) =>
|
|
1549
|
+
bindings: (bindings) => redactRecord(bindings, true),
|
|
1545
1550
|
// The name-walk redactor runs here, on the merged record (mixin output +
|
|
1546
1551
|
// the caller's object), which is exactly the surface a caller controls.
|
|
1547
1552
|
// `base` and child bindings are NOT visible at this hook — they are
|
|
1548
1553
|
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1549
1554
|
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1550
1555
|
log: (record) => withSemconvException(
|
|
1551
|
-
withEventName(
|
|
1556
|
+
withEventName(
|
|
1557
|
+
redactRecord(record, true),
|
|
1558
|
+
options.eventNameField
|
|
1559
|
+
),
|
|
1552
1560
|
options.errorFormat
|
|
1553
1561
|
)
|
|
1554
1562
|
},
|
package/dist/server/index.d.cts
CHANGED
|
@@ -899,7 +899,7 @@ declare class DefaultStdoutDestination implements ILogDestination {
|
|
|
899
899
|
* // One line per entry, with the fields a project repeats on every line hidden.
|
|
900
900
|
* const view: PrettyViewOptions = {
|
|
901
901
|
* singleLine: true,
|
|
902
|
-
* ignore: 'pid,hostname,service,deployment,event
|
|
902
|
+
* ignore: 'pid,hostname,service,deployment,event\\.name'
|
|
903
903
|
* }
|
|
904
904
|
*/
|
|
905
905
|
interface PrettyViewOptions {
|
|
@@ -914,6 +914,21 @@ interface PrettyViewOptions {
|
|
|
914
914
|
* what a real sink receives is untouched. Extend rather than replace when the
|
|
915
915
|
* goal is quieter output — the default already hides the noisiest fields.
|
|
916
916
|
* Default: `'pid,hostname,service'`.
|
|
917
|
+
*
|
|
918
|
+
* **A dot means "nested path", so a field name CONTAINING a dot has to escape
|
|
919
|
+
* it.** This library emits `event.name` as a literal top-level key, so the
|
|
920
|
+
* obvious spelling silently hides nothing — `pino-pretty` looks for an `event`
|
|
921
|
+
* object with a `name` inside and finds neither. Measured on one entry, only
|
|
922
|
+
* the option changing:
|
|
923
|
+
*
|
|
924
|
+
* ```
|
|
925
|
+
* ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
926
|
+
* ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
927
|
+
* ```
|
|
928
|
+
*
|
|
929
|
+
* In a TypeScript string literal that is `'event\\.name'` — one real backslash
|
|
930
|
+
* reaching `pino-pretty`. Nested objects (`service`, `deployment`) need no
|
|
931
|
+
* escape; they really are paths.
|
|
917
932
|
*/
|
|
918
933
|
ignore?: string;
|
|
919
934
|
/**
|
|
@@ -949,7 +964,7 @@ interface PrettyViewOptions {
|
|
|
949
964
|
*
|
|
950
965
|
* // One line per entry, with this project's repeated fields hidden.
|
|
951
966
|
* new PrettyDevDestination({
|
|
952
|
-
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event
|
|
967
|
+
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
953
968
|
* })
|
|
954
969
|
*
|
|
955
970
|
* // Message-only, with the context pulled back into the line.
|
package/dist/server/index.d.ts
CHANGED
|
@@ -899,7 +899,7 @@ declare class DefaultStdoutDestination implements ILogDestination {
|
|
|
899
899
|
* // One line per entry, with the fields a project repeats on every line hidden.
|
|
900
900
|
* const view: PrettyViewOptions = {
|
|
901
901
|
* singleLine: true,
|
|
902
|
-
* ignore: 'pid,hostname,service,deployment,event
|
|
902
|
+
* ignore: 'pid,hostname,service,deployment,event\\.name'
|
|
903
903
|
* }
|
|
904
904
|
*/
|
|
905
905
|
interface PrettyViewOptions {
|
|
@@ -914,6 +914,21 @@ interface PrettyViewOptions {
|
|
|
914
914
|
* what a real sink receives is untouched. Extend rather than replace when the
|
|
915
915
|
* goal is quieter output — the default already hides the noisiest fields.
|
|
916
916
|
* Default: `'pid,hostname,service'`.
|
|
917
|
+
*
|
|
918
|
+
* **A dot means "nested path", so a field name CONTAINING a dot has to escape
|
|
919
|
+
* it.** This library emits `event.name` as a literal top-level key, so the
|
|
920
|
+
* obvious spelling silently hides nothing — `pino-pretty` looks for an `event`
|
|
921
|
+
* object with a `name` inside and finds neither. Measured on one entry, only
|
|
922
|
+
* the option changing:
|
|
923
|
+
*
|
|
924
|
+
* ```
|
|
925
|
+
* ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
|
|
926
|
+
* ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
|
|
927
|
+
* ```
|
|
928
|
+
*
|
|
929
|
+
* In a TypeScript string literal that is `'event\\.name'` — one real backslash
|
|
930
|
+
* reaching `pino-pretty`. Nested objects (`service`, `deployment`) need no
|
|
931
|
+
* escape; they really are paths.
|
|
917
932
|
*/
|
|
918
933
|
ignore?: string;
|
|
919
934
|
/**
|
|
@@ -949,7 +964,7 @@ interface PrettyViewOptions {
|
|
|
949
964
|
*
|
|
950
965
|
* // One line per entry, with this project's repeated fields hidden.
|
|
951
966
|
* new PrettyDevDestination({
|
|
952
|
-
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event
|
|
967
|
+
* view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
|
|
953
968
|
* })
|
|
954
969
|
*
|
|
955
970
|
* // Message-only, with the context pulled back into the line.
|
package/dist/server/index.mjs
CHANGED
|
@@ -229,6 +229,13 @@ function escapeControlCharacters(text) {
|
|
|
229
229
|
var DEFAULT_MAX_CAUSE_DEPTH = 3;
|
|
230
230
|
var MAX_AGGREGATE_ERRORS = 10;
|
|
231
231
|
var CIRCULAR_SENTINEL = "[Circular]";
|
|
232
|
+
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
233
|
+
"name",
|
|
234
|
+
"message",
|
|
235
|
+
"stack",
|
|
236
|
+
"cause",
|
|
237
|
+
"errors"
|
|
238
|
+
]);
|
|
232
239
|
function sanitizeError(err, options) {
|
|
233
240
|
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
234
241
|
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
@@ -264,11 +271,23 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
|
264
271
|
result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
|
|
265
272
|
}
|
|
266
273
|
}
|
|
274
|
+
assignOwnErrorFields(result, value);
|
|
267
275
|
return result;
|
|
268
276
|
} catch {
|
|
269
277
|
return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
|
|
270
278
|
}
|
|
271
279
|
}
|
|
280
|
+
function assignOwnErrorFields(target, source) {
|
|
281
|
+
try {
|
|
282
|
+
const own = Array.isArray(source) ? {} : source;
|
|
283
|
+
for (const [key, value] of Object.entries(own)) {
|
|
284
|
+
if (!Object.hasOwn(target, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
285
|
+
Object.defineProperty(target, key, { value, enumerable: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
272
291
|
function toSanitizedChild(value, seen, depth2, maxDepth) {
|
|
273
292
|
if (isObject(value) && seen.has(value)) {
|
|
274
293
|
return CIRCULAR_SENTINEL;
|
|
@@ -1398,13 +1417,6 @@ function leafNameOf(path2) {
|
|
|
1398
1417
|
}
|
|
1399
1418
|
return leaf;
|
|
1400
1419
|
}
|
|
1401
|
-
var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
1402
|
-
"name",
|
|
1403
|
-
"message",
|
|
1404
|
-
"stack",
|
|
1405
|
-
"cause",
|
|
1406
|
-
"errors"
|
|
1407
|
-
]);
|
|
1408
1420
|
function serializeErrorValue(value_) {
|
|
1409
1421
|
const sanitized = sanitizeError(value_);
|
|
1410
1422
|
const serialized = {
|
|
@@ -1421,18 +1433,7 @@ function serializeErrorValue(value_) {
|
|
|
1421
1433
|
if (sanitized.errors !== void 0) {
|
|
1422
1434
|
serialized["errors"] = sanitized.errors;
|
|
1423
1435
|
}
|
|
1424
|
-
|
|
1425
|
-
const source = (
|
|
1426
|
-
// Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
|
|
1427
|
-
typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
|
|
1428
|
-
);
|
|
1429
|
-
for (const [key, value] of Object.entries(source)) {
|
|
1430
|
-
if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
|
|
1431
|
-
Reflect.set(serialized, key, value);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
} catch {
|
|
1435
|
-
}
|
|
1436
|
+
assignOwnErrorFields(serialized, sanitized);
|
|
1436
1437
|
return serialized;
|
|
1437
1438
|
}
|
|
1438
1439
|
function withSemconvException(record, format) {
|
|
@@ -1482,6 +1483,9 @@ function resolveNameRedactor(options) {
|
|
|
1482
1483
|
if (options.redactStrategy !== "names") {
|
|
1483
1484
|
return PASS_THROUGH;
|
|
1484
1485
|
}
|
|
1486
|
+
return buildNameRedactor(options);
|
|
1487
|
+
}
|
|
1488
|
+
function buildNameRedactor(options) {
|
|
1485
1489
|
const names = [
|
|
1486
1490
|
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1487
1491
|
...options.redactPaths.flatMap((path2) => leafNameOf(path2) ?? [])
|
|
@@ -1500,8 +1504,9 @@ function resolveRedactOption(options) {
|
|
|
1500
1504
|
}
|
|
1501
1505
|
function buildPinoInstance(options, logContext, destinations, health) {
|
|
1502
1506
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1503
|
-
const
|
|
1504
|
-
const
|
|
1507
|
+
const redactRecord = resolveNameRedactor(options);
|
|
1508
|
+
const redactSerialized = buildNameRedactor(options);
|
|
1509
|
+
const bound = (serializer) => createSizeBoundedSerializer((input) => redactSerialized(serializer(input)), maxBytes);
|
|
1505
1510
|
const serializers = {
|
|
1506
1511
|
err: bound(serializeErrorValue),
|
|
1507
1512
|
...Object.fromEntries(
|
|
@@ -1538,14 +1543,17 @@ function buildPinoInstance(options, logContext, destinations, health) {
|
|
|
1538
1543
|
// consumer's extra metadata instead of silently dropping it. Runs ONCE, at
|
|
1539
1544
|
// logger construction — no per-entry cost. `true`: bindings are a record
|
|
1540
1545
|
// root, iterated rather than serialized.
|
|
1541
|
-
bindings: (bindings) =>
|
|
1546
|
+
bindings: (bindings) => redactRecord(bindings, true),
|
|
1542
1547
|
// The name-walk redactor runs here, on the merged record (mixin output +
|
|
1543
1548
|
// the caller's object), which is exactly the surface a caller controls.
|
|
1544
1549
|
// `base` and child bindings are NOT visible at this hook — they are
|
|
1545
1550
|
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1546
1551
|
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1547
1552
|
log: (record) => withSemconvException(
|
|
1548
|
-
withEventName(
|
|
1553
|
+
withEventName(
|
|
1554
|
+
redactRecord(record, true),
|
|
1555
|
+
options.eventNameField
|
|
1556
|
+
),
|
|
1549
1557
|
options.errorFormat
|
|
1550
1558
|
)
|
|
1551
1559
|
},
|
package/package.json
CHANGED