@bymax-one/nest-logger 1.2.0 → 1.2.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 +138 -1
- package/README.md +171 -13
- package/dist/server/index.cjs +315 -40
- package/dist/server/index.d.cts +127 -13
- package/dist/server/index.d.ts +127 -13
- package/dist/server/index.mjs +327 -49
- package/dist/shared/index.d.cts +102 -3
- package/dist/shared/index.d.ts +102 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,142 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.1] - 2026-08-13
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **A log message can no longer forge log lines in re-rendered output** (CodeQL
|
|
19
|
+
`js/log-injection`, alert #61). Every message argument handed to Pino can carry caller- or
|
|
20
|
+
user-provided text — a thrown value recorded off an HTTP request, a variadic NestJS line, a
|
|
21
|
+
structured message — and while the NDJSON transport neutralizes an embedded LF or CR through
|
|
22
|
+
JSON escaping (measured; it does NOT cover U+2028/U+2029 — see the entry below),
|
|
23
|
+
`pino-pretty` — shipped here as `PrettyDevDestination` — and
|
|
24
|
+
any destination that re-renders the parsed message print real newlines, where a break forges what
|
|
25
|
+
looks like a separate entry (also measured: the forged line is indistinguishable from a genuine
|
|
26
|
+
one). Line and paragraph separators (`\r`, `\n`, U+2028, U+2029) are now replaced with the
|
|
27
|
+
literal `\n` sequence on **every** message sink: `error()` for both the `Error` and the string
|
|
28
|
+
path, the structured `info`/`warn` calls, and the NestJS variadic bridge. Structured fields are
|
|
29
|
+
untouched — `err.message` keeps the verbatim text — so no information is lost, only the
|
|
30
|
+
human-readable `msg` is pinned to one line.
|
|
31
|
+
- **Terminal control characters can no longer drive the renderer either.** Pinning the line
|
|
32
|
+
terminators is not enough on a terminal: `ESC E` is ANSI NEL (next line), `ESC [` opens a control
|
|
33
|
+
sequence, and vertical tab, form feed and C1 NEL (U+0085) all move the cursor. Measured on the
|
|
34
|
+
built bundle — the raw `ESC` byte reached the terminal through `pino-pretty` and forged a line
|
|
35
|
+
exactly like a newline did. Every C0 control except TAB, plus DEL and the C1 range, is now
|
|
36
|
+
rendered as its readable `\uXXXX` escape — one form for every character, four hex digits.
|
|
37
|
+
Escaping the ESC byte is what disarms every sequence built on it, rather than chasing sequences
|
|
38
|
+
one by one.
|
|
39
|
+
- **This also protects the raw NDJSON line, not only re-rendering destinations.** JSON escaping
|
|
40
|
+
covers C0 and nothing more: `JSON.stringify` and Pino's serializer emit DEL, the C1 range
|
|
41
|
+
(U+0085 NEL among them), U+2028 and U+2029 **verbatim** — measured on the emitted bytes. A human
|
|
42
|
+
reading raw NDJSON in a terminal was therefore exposed to the same cursor movement as a
|
|
43
|
+
`pino-pretty` reader. Both paths are now covered. The boundary is unchanged: structured values
|
|
44
|
+
are data and are not rewritten, so a control character placed in a metadata field still reaches
|
|
45
|
+
the terminal — only `msg` and the stack carry the guarantee.
|
|
46
|
+
- **The scrubbed stack is escaped too.** `pino-pretty` prints `err.stack` RAW rather than as a JSON
|
|
47
|
+
string, and a stack's first line repeats the error message — so escaping only `msg` left the
|
|
48
|
+
identical attack working through `err.stack` and `exception.stacktrace`. Control characters in
|
|
49
|
+
the stack are now escaped; its newlines are preserved, because a stack is legitimately
|
|
50
|
+
multi-line.
|
|
51
|
+
|
|
52
|
+
P1 of the observability audit ([`docs/observability_audit.md`](./docs/observability_audit.md)):
|
|
53
|
+
stable OpenTelemetry resource identity, robust trace correlation, semconv-aligned error fields and
|
|
54
|
+
machine-readable event names. Every convention adopted here was verified **Stable** against
|
|
55
|
+
Semantic Conventions **v1.44.0** on 2026-08-13; nothing Development or Experimental is emitted by
|
|
56
|
+
default, and no runtime dependency was added.
|
|
57
|
+
|
|
58
|
+
Design rationale is in [ADR 0001](./docs/adr/0001-resource-identity-and-otel-correlation.md); the
|
|
59
|
+
complete field inventory is in
|
|
60
|
+
[`docs/semantic-convention-mapping.md`](./docs/semantic-convention-mapping.md).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **The HTTP terminal entry is emitted in the live async context when one exists.** The `'close'`
|
|
65
|
+
listener used `AsyncResource.bind` alone, which captures the context at REGISTRATION time — so
|
|
66
|
+
when instrumentation opened a span downstream of the middleware, the terminal entry was
|
|
67
|
+
attributed to the wrong span, silently, because a plausible trace id was still present. Measured
|
|
68
|
+
(and confirmed independently against a real OTel ContextManager): the live read covers the
|
|
69
|
+
normal path, the bound context covers the aborted path, and neither alone covers both. The
|
|
70
|
+
listener now reads live-first with the bound context as fallback; the ALS store doubles as the
|
|
71
|
+
liveness probe.
|
|
72
|
+
- **A thrown non-object no longer spreads into the `err` object.** The serializer's own-property
|
|
73
|
+
copy ran on any value, and `Object.entries` on a thrown STRING spreads its characters as indexed
|
|
74
|
+
keys — a record carried `err: {"0":"t","1":"h",…}` beside the `UnknownError` envelope; a thrown
|
|
75
|
+
array spread its elements the same way. Own properties are now copied only off a plain
|
|
76
|
+
non-array object.
|
|
77
|
+
- **Trace correlation no longer switches off because of the working directory.** `@opentelemetry/api`
|
|
78
|
+
was resolved from `process.cwd()` alone, so a Docker `WORKDIR` that is not the app root, a
|
|
79
|
+
pnpm/Yarn workspace with hoisted `node_modules`, a monorepo launched from the repository root or a
|
|
80
|
+
serverless bundle silently disabled ALL trace correlation. Measured with `cwd = /`: the old anchor
|
|
81
|
+
fails to resolve, the new one succeeds. Resolution is now anchored at the library's own module
|
|
82
|
+
path, falling back to the working directory.
|
|
83
|
+
- **`err.type` is no longer `"Object"` for a typed exception.** `pino.stdSerializers.err` derives
|
|
84
|
+
the type from the value's CONSTRUCTOR, so an error already normalized into a plain object — which
|
|
85
|
+
is exactly what `HttpExceptionFilter` produces — was reported as `Object`. The fix is
|
|
86
|
+
architectural: nothing pre-serializes any more and the `err` serializer reads the real type. A
|
|
87
|
+
second layer of the same defect is fixed too — `sanitizeError` only accepted `instanceof Error`,
|
|
88
|
+
so the same plain object came out as `UnknownError`.
|
|
89
|
+
- **`Error.cause` chains reach the log.** `sanitizeError` computed the chain and the service
|
|
90
|
+
discarded it, so modern JavaScript error chaining was invisible. `AggregateError` members are
|
|
91
|
+
carried as `err.errors`. Both are depth- and width-bounded, circular-safe, and redacted.
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- **Stable resource identity.** `service.namespace`, `service.instanceId` and `service.environment`
|
|
96
|
+
options, emitted as `service.namespace`, `service.instance.id` and `deployment.environment.name`.
|
|
97
|
+
Resolution precedence is deterministic: explicit options → `OTEL_SERVICE_NAME` →
|
|
98
|
+
`OTEL_RESOURCE_ATTRIBUTES` → `NODE_ENV`. The order of the two OTel variables is required by the
|
|
99
|
+
specification, so the logger and the SDK reading the same environment agree by construction.
|
|
100
|
+
`service.instance.id` is **never generated** — a UUID minted here would be the logger's rather
|
|
101
|
+
than the OTel Resource's, and would change on every restart while looking authoritative.
|
|
102
|
+
- **`resourceFormat: 'nested' | 'flat'`** (default `'nested'`). `'flat'` emits the dotted attribute
|
|
103
|
+
names verbatim, which a collector maps onto resource attributes directly.
|
|
104
|
+
- **`errorFormat: 'pino' | 'semconv' | 'both'`** (default `'pino'`). Adds the Stable
|
|
105
|
+
`exception.type` / `exception.message` / `exception.stacktrace` and a low-cardinality
|
|
106
|
+
`error.type`. `'both'` is the migration path; `'semconv'` replaces the legacy `err` object and is
|
|
107
|
+
an explicit choice, never a default.
|
|
108
|
+
- **`eventNameField`** (default `'event.name'`). Derives an OTel-conforming event name from
|
|
109
|
+
`logKey` — `PAYMENT_FAILED` → `payment.failed` — following the naming rules. `logKey` is never
|
|
110
|
+
renamed or removed. `false` disables it.
|
|
111
|
+
- **`LOGGER_BOOTSTRAP_WARNING` for an unavailable OTel API.** When trace injection is enabled and
|
|
112
|
+
`@opentelemetry/api` cannot be resolved, one warning naming `OTEL_API_UNAVAILABLE` is emitted at
|
|
113
|
+
boot. A missing `traceId` used to be indistinguishable from "no active span".
|
|
114
|
+
- **`ResolvedServiceMetadata`** exported from `@bymax-one/nest-logger/shared` — the identity after
|
|
115
|
+
precedence, and the contract a future `@bymax-one/nest-observability` consumes.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- An error's own enumerable properties (`code`, `statusCode`, domain fields) are still carried onto
|
|
120
|
+
the serialized `err`, as Pino's standard serializer did — with the fields the serializer derives
|
|
121
|
+
itself excluded, so a plain error-like object no longer emits `name` beside the `type` derived
|
|
122
|
+
from it.
|
|
123
|
+
- `deployment.environment.name` is emitted from `NODE_ENV` when nothing else supplies it, so most
|
|
124
|
+
applications gain the attribute without configuration. The **deprecated** `deployment.environment`
|
|
125
|
+
spelling is never emitted.
|
|
126
|
+
|
|
127
|
+
### Performance
|
|
128
|
+
|
|
129
|
+
Measured on the same machine as the 1.2.0 figures, shipped configuration:
|
|
130
|
+
|
|
131
|
+
| Path | 1.2.0 | P1 | Delta |
|
|
132
|
+
| ------------------------------------------- | --------------: | -------------: | ----: |
|
|
133
|
+
| Shipped config throughput | ~278,000 logs/s | 263,768 logs/s | −5% |
|
|
134
|
+
| Retention vs. bare service (budget ≥ 0.20×) | 0.349× | 0.323× | −7% |
|
|
135
|
+
|
|
136
|
+
The cost is almost entirely `event.name` derivation: 949,560 vs 1,089,561 ops/sec on the info path
|
|
137
|
+
(~13% there, ~7% of the full path). Resource identity costs ~2%, because it is resolved once and
|
|
138
|
+
precomputed into Pino's `base`. `errorFormat: 'semconv'` is **faster** than the legacy shape
|
|
139
|
+
(330,289 vs 245,463 ops/sec) since it emits flat strings instead of walking a cause chain. Set
|
|
140
|
+
`eventNameField: false` to pay none of it.
|
|
141
|
+
|
|
142
|
+
### Not adopted
|
|
143
|
+
|
|
144
|
+
- **`@opentelemetry/api-logs`** — still `0.221.0`. A 0.x package will not become a dependency of a
|
|
145
|
+
library that ships `"dependencies": {}`.
|
|
146
|
+
- **`event.name` as an OTLP attribute** — the attribute is Deprecated; the value belongs in the
|
|
147
|
+
LogRecord `EventName` field, and the emitted key is the JSON carrier for that mapping.
|
|
148
|
+
- **`deployment.id` / `.name` / `.status`** — Development stability.
|
|
149
|
+
|
|
14
150
|
## [1.2.0] - 2026-08-13
|
|
15
151
|
|
|
16
152
|
Remediation of the P0 findings from the observability audit
|
|
@@ -657,7 +793,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
657
793
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
658
794
|
`release.yml`, Dependabot, and issue templates
|
|
659
795
|
|
|
660
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.
|
|
796
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.1...HEAD
|
|
797
|
+
[1.2.1]: https://github.com/bymaxone/nest-logger/compare/v1.2.0...v1.2.1
|
|
661
798
|
[1.2.0]: https://github.com/bymaxone/nest-logger/compare/v1.1.0...v1.2.0
|
|
662
799
|
[1.1.0]: https://github.com/bymaxone/nest-logger/compare/v1.0.8...v1.1.0
|
|
663
800
|
[1.0.8]: https://github.com/bymaxone/nest-logger/compare/v1.0.7...v1.0.8
|
package/README.md
CHANGED
|
@@ -73,7 +73,10 @@ pnpm add @bymax-one/nest-logger
|
|
|
73
73
|
|
|
74
74
|
### 🔍 Observability & Context
|
|
75
75
|
|
|
76
|
-
- ✅ **OpenTelemetry Correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
|
|
76
|
+
- ✅ **OpenTelemetry Correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected. Resolution is anchored at the library's own module path, so a Docker `WORKDIR`, a pnpm workspace or a monorepo launched from the repo root cannot silently switch correlation off
|
|
77
|
+
- ✅ **Stable Resource Identity** — `service.name`/`.namespace`/`.version`/`.instance.id` and `deployment.environment.name`, all **Stable** in Semantic Conventions v1.44.0, resolved from one deterministic precedence shared with the OTel SDK
|
|
78
|
+
- ✅ **Semconv Error Fields** — opt-in `exception.type`/`.message`/`.stacktrace` and low-cardinality `error.type`, additive beside the legacy `err.*`, with full `Error.cause` chains
|
|
79
|
+
- ✅ **Machine-Readable Event Names** — `event.name` derived from `logKey` following OTel naming rules
|
|
77
80
|
- ✅ **AsyncLocalStorage Context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
|
|
78
81
|
- ✅ **HTTP Access Log, before guards** — logs every request including the ones an interceptor cannot see (401/403/429 guard rejections, 404 unmatched routes), with URL normalization (UUIDs and numeric IDs replaced by `:id`) and the query string stripped
|
|
79
82
|
- ✅ **Exception Filter** — captures NestJS `HttpException` and unexpected errors with structured output
|
|
@@ -361,6 +364,153 @@ void bootstrap()
|
|
|
361
364
|
|
|
362
365
|
Once active, every log entry automatically carries `traceId`, `spanId`, and `traceFlags`. Click the `traceId` in Grafana to jump directly to the correlated span in Tempo or Honeycomb.
|
|
363
366
|
|
|
367
|
+
**Without OpenTelemetry installed, everything above simply does not happen** — no error, no warning
|
|
368
|
+
on every log, no crash. The peer is optional and the logger is fully usable on its own. The one
|
|
369
|
+
case that _is_ reported is a misconfiguration: if trace injection is enabled and
|
|
370
|
+
`@opentelemetry/api` cannot be resolved, a single `LOGGER_BOOTSTRAP_WARNING` naming
|
|
371
|
+
`OTEL_API_UNAVAILABLE` is emitted at startup. Absence of `traceId` would otherwise be
|
|
372
|
+
indistinguishable from "no active span".
|
|
373
|
+
|
|
374
|
+
The logger **observes** the trace context; it never creates spans, installs a context manager, or
|
|
375
|
+
parses `traceparent` by hand. When the API is present but no span is active, no identifiers are
|
|
376
|
+
emitted — none are invented.
|
|
377
|
+
|
|
378
|
+
> [!NOTE]
|
|
379
|
+
> Resolution of the optional peer is anchored at the **library's own module path**, falling back to
|
|
380
|
+
> `process.cwd()`. Anchoring only at the working directory — as versions before this did — silently
|
|
381
|
+
> disabled correlation whenever the process was launched from somewhere other than the application
|
|
382
|
+
> root: a Docker `WORKDIR`, a pnpm/Yarn workspace with hoisted `node_modules`, a monorepo started at
|
|
383
|
+
> the repository root, a serverless bundle.
|
|
384
|
+
|
|
385
|
+
### 6b. Resource identity
|
|
386
|
+
|
|
387
|
+
Every entry carries the service identity, using attributes that are **Stable** in Semantic
|
|
388
|
+
Conventions v1.44.0:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
BymaxLoggerModule.forRoot({
|
|
392
|
+
service: {
|
|
393
|
+
name: 'checkout-api', // service.name
|
|
394
|
+
version: '2.14.3', // service.version
|
|
395
|
+
namespace: 'payments', // service.namespace
|
|
396
|
+
instanceId: process.env.POD_UID, // service.instance.id
|
|
397
|
+
environment: 'production' // deployment.environment.name
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"level": "info",
|
|
405
|
+
"time": "2026-08-13T10:00:00.000Z",
|
|
406
|
+
"service": {
|
|
407
|
+
"name": "checkout-api",
|
|
408
|
+
"version": "2.14.3",
|
|
409
|
+
"namespace": "payments",
|
|
410
|
+
"instance": { "id": "pod-7f3a" }
|
|
411
|
+
},
|
|
412
|
+
"deployment": { "environment": { "name": "production" } },
|
|
413
|
+
"logKey": "PAYMENT_FAILED",
|
|
414
|
+
"event.name": "payment.failed",
|
|
415
|
+
"msg": "Payment failed"
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Set `resourceFormat: 'flat'` to emit the dotted attribute names verbatim
|
|
420
|
+
(`"service.instance.id": "pod-7f3a"`), which is what a collector mapping log fields onto resource
|
|
421
|
+
attributes reads directly.
|
|
422
|
+
|
|
423
|
+
#### Precedence
|
|
424
|
+
|
|
425
|
+
Resolved once at startup, in this order:
|
|
426
|
+
|
|
427
|
+
1. **explicit `service` options**
|
|
428
|
+
2. **`OTEL_SERVICE_NAME`** (name only)
|
|
429
|
+
3. **`OTEL_RESOURCE_ATTRIBUTES`** (`service.namespace=payments,service.version=2.14.3,…`)
|
|
430
|
+
4. **`NODE_ENV`** (environment only)
|
|
431
|
+
|
|
432
|
+
Explicit configuration wins because it is the most specific statement you can make. The order of
|
|
433
|
+
the two OpenTelemetry variables is required by the specification, not chosen: _"If `service.name` is
|
|
434
|
+
also provided in `OTEL_RESOURCE_ATTRIBUTES`, then `OTEL_SERVICE_NAME` takes precedence."_
|
|
435
|
+
|
|
436
|
+
Reading the same variables the SDK reads is what makes **logs and traces agree** without the logger
|
|
437
|
+
depending on the SDK. Configure the environment once and both signals describe the same service.
|
|
438
|
+
|
|
439
|
+
> [!IMPORTANT]
|
|
440
|
+
> **`service.instance.id` is never generated.** The specification requires the triplet
|
|
441
|
+
> (`service.namespace`, `service.name`, `service.instance.id`) to be globally unique and recommends
|
|
442
|
+
> a random UUID — but a UUID minted by the logger would be the _logger's_, not the OpenTelemetry
|
|
443
|
+
> Resource's, so logs and traces would claim different instances of the same process. It would also
|
|
444
|
+
> change on every restart while looking authoritative. Supply it from the platform (Kubernetes pod
|
|
445
|
+
> UID, ECS task ARN, VM instance id) or through `OTEL_RESOURCE_ATTRIBUTES`. Omitted is honest;
|
|
446
|
+
> plausible-but-wrong is not.
|
|
447
|
+
>
|
|
448
|
+
> There is **no `OTEL_SERVICE_VERSION`** variable in the specification. Version comes from
|
|
449
|
+
> configuration or from `OTEL_RESOURCE_ATTRIBUTES`.
|
|
450
|
+
|
|
451
|
+
The deprecated `deployment.environment` spelling is **never** emitted, in any mode.
|
|
452
|
+
|
|
453
|
+
### 6c. Event names
|
|
454
|
+
|
|
455
|
+
Structured entries carry a machine-readable event name derived from `logKey`, following OTel naming
|
|
456
|
+
rules — lowercase, dot-namespaced:
|
|
457
|
+
|
|
458
|
+
| `logKey` | `event.name` |
|
|
459
|
+
| ---------------------------- | ---------------------------- |
|
|
460
|
+
| `PAYMENT_FAILED` | `payment.failed` |
|
|
461
|
+
| `USER_AUTHENTICATION_FAILED` | `user.authentication.failed` |
|
|
462
|
+
|
|
463
|
+
`logKey` is **never renamed or removed** — this is additive. Calls through the NestJS variadic
|
|
464
|
+
bridge (`logger.log`, `logger.warn`, …) carry no log key and correctly get no event name: an
|
|
465
|
+
ordinary diagnostic line is not an Event.
|
|
466
|
+
|
|
467
|
+
Keep event names **low cardinality**. `payment.failed` is an event; `payment.failed.918231781` is an
|
|
468
|
+
identifier wearing an event's clothes and will multiply your series count. Identifiers belong in
|
|
469
|
+
their own fields.
|
|
470
|
+
|
|
471
|
+
> [!NOTE]
|
|
472
|
+
> The value is meant to be mapped onto the **`EventName`** field of the OpenTelemetry LogRecord,
|
|
473
|
+
> which is Stable in the Logs Data Model. The same-named `event.name` **attribute** is _Deprecated_
|
|
474
|
+
> precisely because the value belongs in that top-level field instead. A JSON log line has no way to
|
|
475
|
+
> express that distinction — every key is just a key — so the carrier key is configurable via
|
|
476
|
+
> `eventNameField`, and your collector decides where it lands. What it must not do is carry it into
|
|
477
|
+
> an OTLP _attributes_ map under the deprecated name.
|
|
478
|
+
|
|
479
|
+
Set `eventNameField: false` to emit nothing.
|
|
480
|
+
|
|
481
|
+
### 6d. Error fields
|
|
482
|
+
|
|
483
|
+
`errorFormat` controls the shape:
|
|
484
|
+
|
|
485
|
+
```json
|
|
486
|
+
// errorFormat: 'pino' (default — unchanged from 1.2.0)
|
|
487
|
+
{ "err": { "type": "PaymentDeclined", "message": "card declined", "stack": "…",
|
|
488
|
+
"cause": { "name": "Error", "message": "gateway timeout" } } }
|
|
489
|
+
|
|
490
|
+
// errorFormat: 'both' (recommended while migrating)
|
|
491
|
+
{ "err": { "type": "PaymentDeclined", … },
|
|
492
|
+
"exception.type": "PaymentDeclined",
|
|
493
|
+
"exception.message": "card declined",
|
|
494
|
+
"exception.stacktrace": "…",
|
|
495
|
+
"error.type": "PaymentDeclined" }
|
|
496
|
+
|
|
497
|
+
// errorFormat: 'semconv' (explicit migration — err is removed)
|
|
498
|
+
{ "exception.type": "PaymentDeclined", "exception.message": "card declined",
|
|
499
|
+
"exception.stacktrace": "…", "error.type": "PaymentDeclined" }
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
All four attributes are **Stable**. `error.type` carries the **class name** and is low cardinality
|
|
503
|
+
by construction — never a message, never an identifier — because the spec requires it to be
|
|
504
|
+
aggregatable.
|
|
505
|
+
|
|
506
|
+
**`Error.cause` chains are preserved**, depth- and width-bounded, circular-safe, and redacted like
|
|
507
|
+
any other field. `AggregateError` members reach the entry as `err.errors`. There is no
|
|
508
|
+
OpenTelemetry attribute for a cause chain, so these stay namespaced under `err` rather than
|
|
509
|
+
inventing an `exception.cause` the spec does not define.
|
|
510
|
+
|
|
511
|
+
An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
|
|
512
|
+
Pino's standard serializer did.
|
|
513
|
+
|
|
364
514
|
### 7. Context propagation with `LogContextService`
|
|
365
515
|
|
|
366
516
|
```typescript
|
|
@@ -396,18 +546,24 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
|
|
|
396
546
|
|
|
397
547
|
### Top-level options
|
|
398
548
|
|
|
399
|
-
| Option | Type
|
|
400
|
-
| ---------------------------- |
|
|
401
|
-
| `service.name` | `string`
|
|
402
|
-
| `service.version` | `string`
|
|
403
|
-
| `
|
|
404
|
-
| `
|
|
405
|
-
| `
|
|
406
|
-
| `
|
|
407
|
-
| `
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
549
|
+
| Option | Type | Default | Description |
|
|
550
|
+
| ---------------------------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
551
|
+
| `service.name` | `string` | **Required** | Service name emitted in every log entry |
|
|
552
|
+
| `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
|
|
553
|
+
| `service.namespace` | `string` | — | OTel `service.namespace` (**Stable**) — the group the service belongs to |
|
|
554
|
+
| `service.instanceId` | `string` | — | OTel `service.instance.id` (**Stable**). **Never generated** — supply it from the platform. See [Resource identity](#6b-resource-identity) |
|
|
555
|
+
| `service.environment` | `string` | `NODE_ENV` | OTel `deployment.environment.name` (**Stable**). The deprecated `deployment.environment` is never emitted |
|
|
556
|
+
| `resourceFormat` | `'nested' \| 'flat'` | `'nested'` | Shape of the identity fields. `'flat'` emits the dotted attribute names verbatim |
|
|
557
|
+
| `eventNameField` | `string \| false` | `'event.name'` | Field carrying the derived event name (`PAYMENT_FAILED` → `payment.failed`). `false` disables it |
|
|
558
|
+
| `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'` | `'both'` adds `exception.*` and `error.type` beside `err.*`; `'semconv'` replaces them |
|
|
559
|
+
| `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
|
|
560
|
+
| `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
|
|
561
|
+
| `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths, applied on top of the default coverage |
|
|
562
|
+
| `redactStrategy` | `'names' \| 'paths'` | `'names'` | Engine behind the DEFAULT set. `'paths'` restores the pre-1.2 `fast-redact` expansion (four-level ceiling, ~100× slower) |
|
|
563
|
+
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the default PII coverage entirely. ⚠️ Emits `LOGGER_BOOTSTRAP_WARNING` at startup — document why |
|
|
564
|
+
| `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
|
|
565
|
+
| `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
|
|
566
|
+
| `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
|
|
411
567
|
|
|
412
568
|
### `http` options
|
|
413
569
|
|
|
@@ -724,6 +880,8 @@ Before `1.2.0` the same names were expanded into 140 `fast-redact` paths at wild
|
|
|
724
880
|
|
|
725
881
|
**Not covered, by design:** a secret interpolated into the message STRING (`logger.info(key, \`token=${t}\`)`) — redaction works on structured fields, and no field-based mechanism can scrub a substring. Keep secrets out of `msg`.
|
|
726
882
|
|
|
883
|
+
**One entry, one line.** What the message string _cannot_ do is forge an entry. Every message argument passes through a line-separator normalization before it reaches Pino, so `\r`, `\n`, U+2028, U+2029 and C1 NEL (U+0085) become the literal two-character sequence `\n` — and every other control character that can drive a terminal (ESC, vertical tab, form feed, backspace, the rest of C0 except TAB, DEL and the C1 range) becomes its readable `\uXXXX` escape. This protects **two** sinks, not one. `pino-pretty` (shipped here as `PrettyDevDestination`) and any destination that re-renders the parsed message write those bytes straight to the terminal, where a raw newline **or** an `ESC E` produces something indistinguishable from a separate log entry. And the raw NDJSON line is exposed too when a human reads it in a terminal: JSON escaping covers only C0, so `JSON.stringify` and Pino's serializer emit DEL, the C1 range (U+0085 NEL included), U+2028 and U+2029 **verbatim** — measured on real bytes. Escaping at the sink neutralizes both paths. The scrubbed stack gets the same escaping (its newlines are kept, since a stack is multi-line by design), because `pino-pretty` prints it raw and its first line repeats the error message. Structured fields are untouched: `err.message` keeps the original text verbatim. That is the boundary — a control character placed in a **metadata value** still reaches a terminal, because escaping data would mean rewriting what you asked to be logged. Only `msg` and the stack carry this guarantee.
|
|
884
|
+
|
|
727
885
|
### Extending the defaults
|
|
728
886
|
|
|
729
887
|
```typescript
|