@bymax-one/nest-logger 1.4.2 → 1.4.3

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,122 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.4.3] - 2026-10-31
15
+
16
+ ### Added
17
+
18
+ - `LOGGER_REDACTOR_TOKEN`, `REDACT_COMMON_FIELDS` and `isValidTraceId` are exported from the package
19
+ root. The token is the injection point of `PinoLoggerService`'s redactor, so a consumer composing
20
+ its own providers can now name it. `REDACT_COMMON_FIELDS` is the name list the
21
+ default redactor matches, and `DEFAULT_REDACT_PATHS` is built from it, so extending the defaults no
22
+ longer means copying the names. `isValidTraceId` is the check the trace mixin already applies.
23
+ `sanitizeError` stays internal: its output shape is not a contract this release commits to.
24
+
25
+ ### Deprecated
26
+
27
+ - `@LogContext(name)` is marked `@deprecated`. It records a label as class metadata and nothing in
28
+ the package reads it, so the label has never been applied. `@InjectLogger(MyClass.name)` is the
29
+ working equivalent. `shouldUseAsNestLogger` was already deprecated and is unchanged.
30
+
31
+ ### Fixed
32
+
33
+ - **A custom `timestamp` can no longer break the log line or add fields to it.** Pino inserts the
34
+ value into the JSON line verbatim, and the library wrapped it in quotes without encoding it, so a
35
+ quote, a backslash or a newline in the returned string produced invalid JSON for every entry, and a
36
+ crafted value such as `","injected":"x` added a field. The value is now encoded as a JSON string,
37
+ and `time` carries exactly what the function returned.
38
+
39
+ ### Breaking
40
+
41
+ - **`http.excludePaths` now excludes `/health/live` and `/health/ready` by default**, the probes of
42
+ `@bymax-one/nest-core`, so the defaults are `/health`, `/health/live`, `/health/ready` and
43
+ `/metrics`. A probe hit every few seconds drowned the record (8272 of 8274 entries on one
44
+ deployment). Each default is an exact, anchored path, so a route such as `/health/other` is still
45
+ logged.
46
+ - **Options are validated more strictly at bootstrap**, each rejection naming the option and, where
47
+ there is a value to show, what was received. Added checks: `maxEntrySizeBytes` must be a finite number (a `NaN` or `Infinity`
48
+ made the size gate never trip); `isGlobal`, `shouldDisableDefaultRedact` and the `http` and `otel`
49
+ switches must be booleans (the string `'false'` is truthy); `redactCensor` a string; `timestamp` a
50
+ function; `redactPaths` an array of non-empty strings; `serializers` an object of functions;
51
+ `destinations` an array whose entries have a `write` method, a non-empty `name` and function hooks;
52
+ `http` and `otel` objects; `http.excludePaths` an array of `RegExp` (a pattern from another realm,
53
+ such as a `vm` context, is accepted); `http.tenantIdHeader` and the
54
+ three `otel.*Field` names non-empty strings; `otel.fieldFormat` `'camelCase'` or `'snake_case'`
55
+ (an unknown value silently behaved as `camelCase`).
56
+ - **`PrettyDevDestination` refuses to initialize when `NODE_ENV` is `production`.** The 0.1.0 entry
57
+ said it was auto-disabled in production, and nothing did that: registered there it rendered ANSI
58
+ colour through a main-thread transform into the log stream. It now fails at `onInit()` with a
59
+ message naming the cause; the registry reports `LOGGER_DESTINATION_INIT_FAILED` on stderr, later
60
+ writes to it are dropped, and when it is the only destination entries go to stdout as raw NDJSON.
61
+ Only the exact value `production` refuses.
62
+
63
+ #### Apply to a consumer
64
+
65
+ - A deployment whose own `/health/live` or `/health/ready` route should stay in the log now sets
66
+ `http.excludePaths` itself. A value replaces the defaults, and `[/^\/health$/, /^\/metrics$/]`
67
+ restores the previous list.
68
+ - An application that passed a value of the wrong kind, such as `http: { isEnabled: 'false' }` or a
69
+ destination without a `name`, now fails at bootstrap with a message naming the option, where it
70
+ previously booted with that value ignored or misread. Correct the value.
71
+ - A deployment that registers `PrettyDevDestination` with `NODE_ENV=production` now gets raw JSON on
72
+ stdout and one stderr line instead of coloured output. Register it only outside production, for
73
+ example `...(process.env.NODE_ENV === 'production' ? [] : [new PrettyDevDestination()])`, and keep
74
+ `DefaultStdoutDestination` in the list.
75
+
76
+ ### Documentation
77
+
78
+ - The error catalog listed codes that nothing in the library emits (for example
79
+ `LOGGER_INVALID_OPTIONS` and `LOGGER_OTEL_API_UNAVAILABLE`), and described them as thrown. A thrown
80
+ error carries a message, not a code, so the catalog now separates the message each thrown error
81
+ starts with from the log keys the library writes. It also gains the errors it omitted and the real
82
+ surface of a missing `@opentelemetry/api`, a `LOGGER_BOOTSTRAP_WARNING` with
83
+ `reason: 'OTEL_API_UNAVAILABLE'`.
84
+ `readme-error-catalog.spec.ts` scans every throw site in `src` and fails when a row and the code
85
+ disagree in either direction.
86
+ - The README documents the injection tokens and the exported helpers, the `useClass` and
87
+ `useExisting` forms of `forRootAsync`, the rules the request-id middleware applies to an inbound
88
+ `x-request-id` and tenant header (a value that fails them is replaced, not cleaned), and that a
89
+ Fastify adapter is not supported.
90
+ - `forRootAsync` does not register the exception filter, which `forRoot` does, and the README now
91
+ says so under the async example, in the options table and in the `http` option JSDoc. Measured with
92
+ the same failing controller on both paths: with `http.isEnabled`, `forRootAsync` emits the access
93
+ log (`HTTP_REQUEST_START`, `_CLIENT_ERROR`, `_SERVER_ERROR`) and the real error reaches its terminal
94
+ entry, but not `HTTP_EXCEPTION_HANDLED` or `HTTP_EXCEPTION_UNHANDLED`; the 500 response is
95
+ identical. Registering `{ provide: APP_FILTER, useClass: HttpExceptionFilter }` yields them. The
96
+ difference is deliberate, and a spec pins the behaviour and the remedy.
97
+ - `ILogDestination` JSDoc: `write` no longer says a failing destination "may be temporarily
98
+ skipped". Nothing skips a destination that initialized: every failed entry is dropped for it and
99
+ reported, and it keeps receiving entries. Only one whose `onInit()` failed receives none.
100
+ `minLevel` says it can only narrow what the module `level` lets through.
101
+ - The README prose and tables are corrected against the code; that correction touches no source file.
102
+ - The options table gave `level` a default of `'info'`. Outside `NODE_ENV=production` the default
103
+ is `'debug'`. It now lists the options it omitted: `isGlobal`, `serializers`, `timestamp`, the
104
+ deprecated `shouldUseAsNestLogger`, and `otel.traceIdField`, `otel.spanIdField` and
105
+ `otel.traceFlagsField`.
106
+ - `DefaultStdoutDestination` was called "always active". It is the default sink only while
107
+ `destinations` is empty, as the Destinations section already said. A destination whose
108
+ `onInit()` rejects was described as staying in the write fan-out. It is removed from the fan-out
109
+ and from shutdown.
110
+ - The architecture diagram showed `info(logKey, msg, context, metadata)`; the third parameter is
111
+ `userId`. It named a `composedMixin()` that does not exist (the function is
112
+ `createTraceContextMixin`) and put `userId` in the scope `RequestIdMiddleware` opens, which holds
113
+ `requestId` and `tenantId`.
114
+ - The tech stack badges now read pnpm 11 and tsup 9. The URL normalization lists name ULID and
115
+ nanoid segments as well as UUID and numeric ones. The `LogContextService` table lists `get()`.
116
+ The last two HTTP log keys sit inside their table.
117
+ - The Pino throughput figures are attributed to Pino's own benchmarks, the `Scope.REQUEST` cost
118
+ to NestJS's estimate, and the EPIPE guard note names its one exception, `pino-pretty`'s own
119
+ stream.
120
+ - The resource identity example passed `process.env.POD_UID` to `service.instanceId`, which does
121
+ not compile under `exactOptionalPropertyTypes` because the value can be `undefined`. It now reads
122
+ `process.env.POD_UID!`.
123
+
124
+ ### Tests
125
+
126
+ - `readme-options.spec.ts` parses the three option tables in the README and checks every default
127
+ against the defaults the module applies, and checks that the tables list exactly the options the
128
+ module resolves, so a default that drifts, or an option added without a row, fails the suite.
129
+
14
130
  ## [1.4.2] - 2026-10-30
15
131
 
16
132
  No breaking changes. Dependency and build-tooling maintenance only. No source file under `src/` changes and the
@@ -2076,7 +2192,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
2076
2192
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
2077
2193
  `release.yml`, Dependabot, and issue templates
2078
2194
 
2079
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.2...HEAD
2195
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.3...HEAD
2196
+ [1.4.3]: https://github.com/bymaxone/nest-logger/compare/v1.4.2...v1.4.3
2080
2197
  [1.4.2]: https://github.com/bymaxone/nest-logger/compare/v1.4.1...v1.4.2
2081
2198
  [1.4.1]: https://github.com/bymaxone/nest-logger/compare/v1.4.0...v1.4.1
2082
2199
  [1.4.0]: https://github.com/bymaxone/nest-logger/compare/v1.3.1...v1.4.0
package/README.md CHANGED
@@ -35,7 +35,7 @@
35
35
 
36
36
  `@bymax-one/nest-logger` replaces ad-hoc `console.log` and legacy Winston setups with a production-grade structured logging pipeline. Every log entry is a JSON object carrying a `logKey` (`MODULE_ACTION_RESULT` convention), `requestId`, `tenantId`, and — when OpenTelemetry is active — the correlated `traceId`/`spanId`.
37
37
 
38
- **Why Pino?** At ~750,000 logs/sec (vs Winston's ~110,000), Pino consumes 3× less CPU and half the RSS under load. The difference is measurable in production billing/payments backends where the logger is on the hot path for every request.
38
+ **Why Pino?** In Pino's own benchmarks (Node 20, one instance — not this package's end-to-end rate), Pino writes ~750,000 logs/sec against Winston's ~110,000, using 3× less CPU and half the RSS under load. The difference is measurable in production billing/payments backends where the logger is on the hot path for every request.
39
39
 
40
40
  The library has **zero direct dependencies** — all packages arrive as peer dependencies, so you control exact versions and the supply chain surface stays minimal.
41
41
 
@@ -78,13 +78,13 @@ pnpm add @bymax-one/nest-logger
78
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
79
  - ✅ **Machine-Readable Event Names** — `event.name` derived from `logKey` following OTel naming rules
80
80
  - ✅ **AsyncLocalStorage Context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
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
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 (UUID, ULID, nanoid and numeric segments replaced by `:id`) and the query string stripped
82
82
  - ✅ **Exception Filter** — captures NestJS `HttpException` and unexpected errors with structured output
83
83
 
84
84
  ### 🔌 Destinations
85
85
 
86
86
  - ✅ **Pluggable Destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
87
- - ✅ **Managed Lifecycle** — `onInit()` / `onShutdown()` hooks; a destination that fails `onInit()` is reported and excluded from shutdown, and boot is never aborted
87
+ - ✅ **Managed Lifecycle** — `onInit()` / `onShutdown()` hooks; a destination that fails `onInit()` is reported and removed from the write fan-out and from shutdown, and boot is never aborted
88
88
  - ✅ **Crash-Proof Writes** — every `write()` is wrapped in a try/catch; a failure is reported on `stderr` and swallowed, never propagated to the app
89
89
 
90
90
  ### 🧩 Developer Experience
@@ -170,10 +170,13 @@ pnpm add @opentelemetry/api @opentelemetry/sdk-node
170
170
  >
171
171
  > Either way, the rule that survives both layouts: **"it is a devDependency, therefore this path
172
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.
173
+ > install command.** `PrettyDevDestination` refuses to initialize when `NODE_ENV` is exactly
174
+ > `production`: it reports `LOGGER_DESTINATION_INIT_FAILED` on stderr and, when it is the only
175
+ > destination, entries go to stdout as raw NDJSON. In any other environment that ships to a log
176
+ > pipeline (a staging stack with `NODE_ENV=staging`, an unset `NODE_ENV`) it still renders ANSI
177
+ > colour into a pipeline that will fail to parse every line, which is indistinguishable from a
178
+ > service that went quiet. Gate it on an explicit configuration value you can see, not on the
179
+ > packaging.
177
180
 
178
181
  > [!NOTE]
179
182
  > `@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.
@@ -200,7 +203,7 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
200
203
  export class AppModule {}
201
204
  ```
202
205
 
203
- Stdout is wired automatically — `DefaultStdoutDestination` is always active, so this is all you need to be logging structured JSON.
206
+ Stdout is wired automatically — with no `destinations` configured, `DefaultStdoutDestination` is the default sink, so this is all you need to be logging structured JSON. A non-empty `destinations` list replaces it (see [Destinations](#destinations-replace-stdout)).
204
207
 
205
208
  ### 3. Async configuration with `ConfigService`
206
209
 
@@ -230,6 +233,58 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
230
233
  export class AppModule {}
231
234
  ```
232
235
 
236
+ > [!IMPORTANT]
237
+ > **`forRootAsync` does not register the exception filter.** With `http.isEnabled` it registers the
238
+ > access log (`HTTP_REQUEST_START`, `_SUCCESS`, `_CLIENT_ERROR`, `_SERVER_ERROR`, `_ABORTED`), and the
239
+ > failing request's error still reaches its terminal entry. The two entries the filter writes,
240
+ > `HTTP_EXCEPTION_HANDLED` and `HTTP_EXCEPTION_UNHANDLED`, are not emitted, and
241
+ > `http.shouldCaptureExceptions` has no effect on this path. `forRoot` registers the filter itself.
242
+ > To get the same entries from `forRootAsync`, register it:
243
+ >
244
+ > ```typescript
245
+ > import { Module } from '@nestjs/common'
246
+ > import { APP_FILTER } from '@nestjs/core'
247
+ > import { HttpExceptionFilter } from '@bymax-one/nest-logger'
248
+ >
249
+ > @Module({ providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }] })
250
+ > export class LoggingFilterModule {}
251
+ > ```
252
+ >
253
+ > It is left to you on purpose: the filter is a global catch-all that writes the response itself, and
254
+ > a library adding one from asynchronously resolved options would put it in the pipeline of every
255
+ > consumer, including those with HTTP logging off or with their own filters.
256
+
257
+ `useFactory` is one of three forms. When the options depend on a provider already in the DI graph, a class can produce them instead. It implements `BymaxLoggerModuleOptionsFactory`; pass it as `useClass` to have the module instantiate it, or as `useExisting` to reuse an instance another module already provides:
258
+
259
+ ```typescript
260
+ // logger-options.factory.ts
261
+ import { Injectable } from '@nestjs/common'
262
+ import { ConfigService } from '@nestjs/config'
263
+ import type {
264
+ BymaxLoggerModuleOptions,
265
+ BymaxLoggerModuleOptionsFactory
266
+ } from '@bymax-one/nest-logger'
267
+
268
+ @Injectable()
269
+ export class LoggerOptionsFactory implements BymaxLoggerModuleOptionsFactory {
270
+ constructor(private readonly cfg: ConfigService) {}
271
+
272
+ createLoggerOptions(): BymaxLoggerModuleOptions {
273
+ return {
274
+ service: {
275
+ name: this.cfg.getOrThrow<string>('OTEL_SERVICE_NAME'),
276
+ version: this.cfg.getOrThrow<string>('RELEASE_SHA')
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // app.module.ts: imports: [BymaxLoggerModule.forRootAsync({ imports: [ConfigModule], useClass: LoggerOptionsFactory })]
283
+ // or, to reuse an instance another module exports: { imports: [OptionsModule], useExisting: LoggerOptionsFactory }
284
+ ```
285
+
286
+ `createLoggerOptions()` may return the options or a promise of them. An error thrown from it, or from `useFactory`, fails module initialisation, and the options it returns go through the same validation as `forRoot`.
287
+
233
288
  ### 4. Inject the logger in a service
234
289
 
235
290
  ```typescript
@@ -301,7 +356,7 @@ Output (pretty-print, development):
301
356
 
302
357
  ### 5. HTTP logging (automatic)
303
358
 
304
- Enable `http.isEnabled: true` in the module options and wire the access log — `applyAccessLog(app)` in `main.ts` (recommended, see [Requests rejected before routing](#requests-rejected-before-routing)) or `applyRequestIdMiddleware(consumer)` (see [Context propagation](#7-context-propagation-with-logcontextservice)). The access log is recorded from **middleware**, which emits:
359
+ Enable `http.isEnabled: true` in the module options and wire the access log — `applyAccessLog(app)` in `main.ts` (recommended, see [Requests rejected before routing](#requests-rejected-before-routing)) or `applyRequestIdMiddleware(consumer)` (see [Context propagation](#7-context-propagation-with-logcontextservice)). The access log is recorded from **middleware**, which emits the six `HTTP_REQUEST_*` keys below; the exception filter adds the last two:
305
360
 
306
361
  | Log key | When |
307
362
  | --------------------------- | --------------------------------------------------- |
@@ -311,13 +366,13 @@ Enable `http.isEnabled: true` in the module options and wire the access log —
311
366
  | `HTTP_REQUEST_CLIENT_ERROR` | 4xx response |
312
367
  | `HTTP_REQUEST_SERVER_ERROR` | 5xx response |
313
368
  | `HTTP_REQUEST_ABORTED` | Connection closed before the response was delivered |
369
+ | `HTTP_EXCEPTION_HANDLED` | `HttpException` caught by the filter |
370
+ | `HTTP_EXCEPTION_UNHANDLED` | Unexpected error caught by the filter |
314
371
 
315
372
  `HTTP_REQUEST_START` is emitted **before guards**, so it carries no `userId` — authentication runs
316
373
  in a guard, and at that point there is no principal yet. The acting user is on the **terminal**
317
374
  entry, where the guard has populated it. Both entries carry the same `requestId`, so joining on
318
375
  that gives the whole request.
319
- | `HTTP_EXCEPTION_HANDLED` | `HttpException` caught by the filter |
320
- | `HTTP_EXCEPTION_UNHANDLED` | Unexpected error caught by the filter |
321
376
 
322
377
  URLs are automatically normalized — `/users/550e8400-e29b-41d4-a716-446655440000` becomes `/users/:id` so Loki/Grafana cardinality stays bounded. The query string is stripped from every logged URL, because a magic-link token or reset code in a query parameter is a secret no key-name redaction can scrub out of a string value.
323
378
 
@@ -528,7 +583,7 @@ BymaxLoggerModule.forRoot({
528
583
  name: 'checkout-api', // service.name
529
584
  version: '2.14.3', // service.version
530
585
  namespace: 'payments', // service.namespace
531
- instanceId: process.env.POD_UID, // service.instance.id
586
+ instanceId: process.env.POD_UID!, // service.instance.id
532
587
  environment: 'production' // deployment.environment.name
533
588
  }
534
589
  })
@@ -700,6 +755,17 @@ export class RequestIdMiddleware implements NestMiddleware {
700
755
 
701
756
  Any log emitted inside the `run()` scope — regardless of nesting depth — automatically includes `requestId` with no prop drilling.
702
757
 
758
+ #### Inbound `x-request-id` and tenant header
759
+
760
+ The bundled `RequestIdMiddleware` does not trust the headers it reads. A request id or tenant id is accepted only when it is a string of **at most 256 characters** drawn from `A-Z a-z 0-9 . _ : / + = -`. Anything else is rejected and replaced, not truncated or cleaned: an oversized value, an empty one, one carrying a space, a control character or any of `< > " '`,, and a value that is not a string.
761
+
762
+ | Header | Rejected value becomes |
763
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
764
+ | `x-request-id` | a fresh UUID, or none when `http.shouldGenerateRequestId` is `false` (an enclosing scope's `requestId`, if any, is then inherited) |
765
+ | the tenant header (`http.tenantIdHeader`) | no `tenantId` from the header; a tenant is never minted |
766
+
767
+ An accepted id is echoed back on the response `x-request-id` header and written to every entry the request produces, which is why the set is narrow. The example above reads the header raw to stay short; use `RequestIdMiddleware` through `applyAccessLog(app)` or `applyRequestIdMiddleware(consumer)` unless you apply the same rule yourself.
768
+
703
769
  ---
704
770
 
705
771
  ## ⚙️ Configuration
@@ -708,40 +774,47 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
708
774
 
709
775
  ### Top-level options
710
776
 
711
- | Option | Type | Default | Description |
712
- | ---------------------------- | ------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
713
- | `service.name` | `string` | **Required** | Service name emitted in every log entry |
714
- | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
715
- | `service.namespace` | `string` | — | OTel `service.namespace` (**Stable**) — the group the service belongs to |
716
- | `service.instanceId` | `string` | — | OTel `service.instance.id` (**Stable**). **Never generated** — supply it from the platform. See [Resource identity](#6b-resource-identity) |
717
- | `service.environment` | `string` | `NODE_ENV` | OTel `deployment.environment.name` (**Stable**). The deprecated `deployment.environment` is never emitted |
718
- | `resourceFormat` | `'nested' \| 'flat'` | `'nested'` | Shape of the identity fields. `'flat'` emits the dotted attribute names verbatim |
719
- | `eventNameField` | `string \| false` | `'event.name'` | Field carrying the derived event name (`PAYMENT_FAILED` → `payment.failed`). `false` disables it |
720
- | `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'` | `'both'` adds `exception.*` and `error.type` beside `err.*`; `'semconv'` replaces them |
721
- | `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
722
- | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths, applied on top of the default coverage |
723
- | `redactStrategy` | `'names' \| 'paths'` | `'names'` | Engine behind the DEFAULT set. `'paths'` restores the pre-1.2 `fast-redact` expansion (four-level ceiling, ~100× slower) |
724
- | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the default PII coverage entirely. ⚠️ Emits `LOGGER_BOOTSTRAP_WARNING` at startup — document why |
725
- | `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
726
- | `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
727
- | `destinations` | `ILogDestination[]` | `[new DefaultStdoutDestination()]` | The sinks entries are written to. ⚠️ A non-empty list **replaces** that default — see [Destinations](#destinations-replace-stdout) |
777
+ | Option | Type | Default | Description |
778
+ | ---------------------------- | --------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
779
+ | `service.name` | `string` | **Required** | Service name emitted in every log entry |
780
+ | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
781
+ | `service.namespace` | `string` | — | OTel `service.namespace` (**Stable**) — the group the service belongs to |
782
+ | `service.instanceId` | `string` | — | OTel `service.instance.id` (**Stable**). **Never generated** — supply it from the platform. See [Resource identity](#6b-resource-identity) |
783
+ | `service.environment` | `string` | `NODE_ENV` | OTel `deployment.environment.name` (**Stable**). The deprecated `deployment.environment` is never emitted |
784
+ | `resourceFormat` | `'nested' \| 'flat'` | `'nested'` | Shape of the identity fields. `'flat'` emits the dotted attribute names verbatim |
785
+ | `eventNameField` | `string \| false` | `'event.name'` | Field carrying the derived event name (`PAYMENT_FAILED` → `payment.failed`). `false` disables it |
786
+ | `errorFormat` | `'pino' \| 'semconv' \| 'both'` | `'pino'` | `'both'` adds `exception.*` and `error.type` beside `err.*`; `'semconv'` replaces them |
787
+ | `level` | `LogLevel` | `'info'` when `NODE_ENV=production`, else `'debug'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
788
+ | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths, applied on top of the default coverage |
789
+ | `redactStrategy` | `'names' \| 'paths'` | `'names'` | Engine behind the DEFAULT set. `'paths'` restores the pre-1.2 `fast-redact` expansion (four-level ceiling, ~100× slower) |
790
+ | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the default PII coverage entirely. ⚠️ Emits `LOGGER_BOOTSTRAP_WARNING` at startup — document why |
791
+ | `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
792
+ | `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
793
+ | `isGlobal` | `boolean` | `true` | Register the module as `@Global()` |
794
+ | `shouldUseAsNestLogger` | `boolean` | `true` | **Deprecated, not read.** Call `BymaxLoggerModule.useNestLogger(app)` in `main.ts` instead |
795
+ | `serializers` | `Record<string, (input: unknown) => unknown>` | `{}` | Custom Pino serializers, merged with the defaults |
796
+ | `timestamp` | `() => string` | `() => new Date().toISOString()` | Custom timestamp function |
797
+ | `destinations` | `ILogDestination[]` | `[new DefaultStdoutDestination()]` | The sinks entries are written to. ⚠️ A non-empty list **replaces** that default — see [Destinations](#destinations-replace-stdout) |
728
798
 
729
799
  ### `http` options
730
800
 
731
- | Option | Type | Default | Description |
732
- | ------------------------------ | ---------- | ------------------------------- | ------------------------------------------------------------------------------------- |
733
- | `http.isEnabled` | `boolean` | `false` | Register `HttpLoggingInterceptor` (and, on `forRoot`, `HttpExceptionFilter`) globally |
734
- | `http.shouldCaptureExceptions` | `boolean` | `true` | Capture unhandled HTTP exceptions and emit `HTTP_EXCEPTION_UNHANDLED` |
735
- | `http.shouldGenerateRequestId` | `boolean` | `true` | Generate a `requestId` when the inbound request header is absent |
736
- | `http.excludePaths` | `RegExp[]` | `[/^\/health$/, /^\/metrics$/]` | Paths that bypass HTTP logging. Use anchored, linear-time regexes (ReDoS-safe) |
737
- | `http.tenantIdHeader` | `string` | `'x-tenant-id'` | Request header carrying the tenant identifier |
801
+ | Option | Type | Default | Description |
802
+ | ------------------------------ | ---------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
803
+ | `http.isEnabled` | `boolean` | `false` | Register `HttpLoggingInterceptor` (and, on `forRoot`, `HttpExceptionFilter`) globally |
804
+ | `http.shouldCaptureExceptions` | `boolean` | `true` | Capture unhandled HTTP exceptions and emit `HTTP_EXCEPTION_UNHANDLED`. `forRoot` only: see the note under [Async configuration](#3-async-configuration-with-configservice) |
805
+ | `http.shouldGenerateRequestId` | `boolean` | `true` | Generate a `requestId` when the inbound request header is absent |
806
+ | `http.excludePaths` | `RegExp[]` | `[/^\/health$/, /^\/health\/live$/, /^\/health\/ready$/, /^\/metrics$/]` | Paths that bypass HTTP logging. Use anchored, linear-time regexes (ReDoS-safe) |
807
+ | `http.tenantIdHeader` | `string` | `'x-tenant-id'` | Request header carrying the tenant identifier |
738
808
 
739
809
  ### `otel` options
740
810
 
741
- | Option | Type | Default | Description |
742
- | ----------------------------------- | ----------------------------- | ------------- | ------------------------------------------------------------------------ |
743
- | `otel.shouldAutoInjectTraceContext` | `boolean` | `true` | Detect `@opentelemetry/api` and inject `traceId`/`spanId` via Pino mixin |
744
- | `otel.fieldFormat` | `'camelCase' \| 'snake_case'` | `'camelCase'` | Field names in log entries: `traceId`/`spanId` vs `trace_id`/`span_id` |
811
+ | Option | Type | Default | Description |
812
+ | ----------------------------------- | ----------------------------- | -------------- | ---------------------------------------------------------------------------------------------------- |
813
+ | `otel.shouldAutoInjectTraceContext` | `boolean` | `true` | Detect `@opentelemetry/api` and inject `traceId`/`spanId` via Pino mixin |
814
+ | `otel.fieldFormat` | `'camelCase' \| 'snake_case'` | `'camelCase'` | Field names in log entries: `traceId`/`spanId` vs `trace_id`/`span_id` |
815
+ | `otel.traceIdField` | `string` | `'traceId'` | Field name for the trace ID; `fieldFormat: 'snake_case'` derives `trace_id` unless this is set |
816
+ | `otel.spanIdField` | `string` | `'spanId'` | Field name for the span ID; `fieldFormat: 'snake_case'` derives `span_id` unless this is set |
817
+ | `otel.traceFlagsField` | `string` | `'traceFlags'` | Field name for the trace flags; `fieldFormat: 'snake_case'` derives `trace_flags` unless this is set |
745
818
 
746
819
  ---
747
820
 
@@ -1015,7 +1088,7 @@ HTTP Request
1015
1088
  RequestIdMiddleware ← runs FIRST (NestJS middleware precedes
1016
1089
  │ guards AND interceptors) and opens the
1017
1090
  │ AsyncLocalStorage scope
1018
- │ { requestId, tenantId, userId }
1091
+ │ { requestId, tenantId }
1019
1092
  ▼
1020
1093
  HttpAccessLogMiddleware ← emits HTTP_REQUEST_START inside that scope, and
1021
1094
  │ arms the terminal entry on the response's
@@ -1030,12 +1103,12 @@ HttpLoggingInterceptor ← records the thrown error for the terminal en
1030
1103
  ▼
1031
1104
  Application Service
1032
1105
  │
1033
- └── PinoLoggerService.info(logKey, msg, context, metadata)
1106
+ └── PinoLoggerService.info(logKey, message, userId?, metadata?)
1034
1107
  │
1035
1108
  ▼
1036
1109
  Pino logger
1037
1110
  │
1038
- composedMixin() ← runs per-log (O(1))
1111
+ createTraceContextMixin() ← runs per log entry
1039
1112
  ├── ALS store → { requestId, tenantId, userId }
1040
1113
  └── OTel span → { traceId, spanId, traceFlags }
1041
1114
  │
@@ -1044,7 +1117,7 @@ Application Service
1044
1117
  │
1045
1118
  ▼
1046
1119
  ┌─────────────────────────────┐
1047
- │ DefaultStdoutDestination │ ← always active
1120
+ │ DefaultStdoutDestination │ ← default; replaced by `destinations`
1048
1121
  │ LokiDestination │ ← optional
1049
1122
  │ PrismaLogDestination │ ← optional
1050
1123
  │ RollingFileDestination │ ← optional
@@ -1055,7 +1128,7 @@ Application Service
1055
1128
 
1056
1129
  | Principle | Description |
1057
1130
  | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1058
- | **🪶 Singleton Scope** | `AsyncLocalStorage` delivers per-request context at zero latency overhead — NestJS `Scope.REQUEST` adds ~5% on the injection graph, unacceptable on a logger that runs for every request |
1131
+ | **🪶 Singleton Scope** | `AsyncLocalStorage` delivers per-request context at zero latency overhead — NestJS's own docs put the cost of `Scope.REQUEST` at up to ~5% latency, unacceptable on a logger that runs for every request |
1059
1132
  | **🧬 One Composed Mixin** | ALS context and OTel trace context merge into a single Pino mixin with a deterministic order: ALS first, then OTel — an active span is the authoritative trace identity, so it wins on conflicts |
1060
1133
  | **⚡ Single-pass Redaction** | One recursive walk censors any value whose key name is in the sensitive set, to a 100-level ceiling past which nested objects are dropped rather than emitted — O(nodes), the same order the serializer already pays. Replaced 140 `fast-redact` wildcard paths that cost ~107 µs per entry |
1061
1134
  | **🔌 Interface-Driven Sinks** | `ILogDestination` is a contract — Loki, Postgres, rolling files, or anything else is a consumer implementation, never a dependency of this package |
@@ -1162,7 +1235,7 @@ Trace context is never copied verbatim into a log entry. The OTel mixin reads th
1162
1235
 
1163
1236
  ### Destination failures are contained
1164
1237
 
1165
- Every `write()` runs inside a try/catch. A destination that throws or rejects produces a `LOGGER_DESTINATION_WRITE_FAILED` line on `stderr` — never back through the logger, which would turn a broken sink into a write → log → write feedback loop — and the entry is dropped for that sink only. A destination whose `onInit()` rejects is reported as `LOGGER_DESTINATION_INIT_FAILED` and excluded from the shutdown sequence without blocking boot; it stays in the write fan-out, where the same fail-soft wrapper contains it. A logging backend going down degrades logging — it never takes the application with it.
1238
+ Every `write()` runs inside a try/catch. A destination that throws or rejects produces a `LOGGER_DESTINATION_WRITE_FAILED` line on `stderr` — never back through the logger, which would turn a broken sink into a write → log → write feedback loop — and the entry is dropped for that sink only. A destination whose `onInit()` rejects is reported as `LOGGER_DESTINATION_INIT_FAILED` and removed from the write fan-out and the shutdown sequence, so it receives no entries and never blocks boot. A logging backend going down degrades logging — it never takes the application with it.
1166
1239
 
1167
1240
  ### What redaction covers, and what it cannot
1168
1241
 
@@ -1205,7 +1278,7 @@ The rule that follows is short: **a value that matters is a field, not text.** A
1205
1278
 
1206
1279
  `node app | head` closes the read end of stdout while the application is still writing. The stream reports that as `EPIPE` **asynchronously**, through its `'error'` event, after `write()` has already returned — so a `try/catch` around the write catches nothing, and because Node attaches no default handler to `process.stdout`, the emit becomes an uncaught exception and the process dies. Measured, not assumed: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42.
1207
1280
 
1208
- Every write this library makes to a process stream — the default stdout sink, the last-resort rescue, the pre-init drain, the `stderr` failure reports — installs a swallowing `'error'` listener on first use. **The side effect is worth knowing:** that listener is on the shared `process.stdout`, so after the first log line an EPIPE raised by _your_ writes stops being an uncaught exception too. The alternative is the crash above. Your own `'error'` listener is never removed or replaced — this only ever adds one.
1281
+ Every write this library makes to a process stream — the default stdout sink, the last-resort rescue, the pre-init drain, the `stderr` failure reports — installs a swallowing `'error'` listener on first use. The one exception is the stream `pino-pretty` writes through inside `PrettyDevDestination`, which attaches `'error'` listeners of its own. **The side effect is worth knowing:** that listener is on the shared `process.stdout`, so after the first log line an EPIPE raised by _your_ writes stops being an uncaught exception too. The alternative is the crash above. Your own `'error'` listener is never removed or replaced — this only ever adds one.
1209
1282
 
1210
1283
  If you write to `process.stdout` from a custom `ILogDestination`, do the same in your own sink; the [destinations guide](./docs/guidelines/DESTINATIONS-IMPLEMENTATION-GUIDELINES.md) has the exact pattern.
1211
1284
 
@@ -1228,20 +1301,20 @@ When integrating `@bymax-one/nest-logger` in production, verify each of the foll
1228
1301
 
1229
1302
  ## 🛡️ Security Table
1230
1303
 
1231
- | Layer | Implementation |
1232
- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
1233
- | PII Redaction | 32 field names censored at ANY depth in one snapshotting walk — ~3.6 µs/entry |
1234
- | Credentials | `password`, `passwordHash`, `token`, `accessToken`, `refreshToken`, `apiKey`, `apiSecret`, `privateKey` |
1235
- | MFA Secrets | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` — redacted to the 100-level ceiling |
1236
- | PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
1237
- | LGPD Documents | `cpf`, `cnpj`, `rg`, plus `email` as a conservative default |
1238
- | HTTP Headers | `authorization`, `cookie`, `x-api-key`, `x-auth-token`, `set-cookie` — absolute paths on `req` / `res` |
1239
- | Redact List | `DEFAULT_REDACT_PATHS` is append-only; removal requires a major version |
1240
- | Trace Injection | `isValidTraceId` / `isValidSpanId` validated before any identifier is written |
1241
- | URL Cardinality | UUIDs and numeric IDs normalized to `:id` — bounds label cardinality and keeps raw identifiers out of the path |
1242
- | Destination Failure | Every `write()` try/catch-wrapped; a failure is reported on `stderr` and swallowed, never propagated to the caller |
1243
- | Field Size | Per-field UTF-8 cap at `maxEntrySizeBytes` (64 KB default); oversized values become a truncation envelope |
1244
- | Supply Chain | `"dependencies": {}` — no transitive runtime packages of the library's own choosing |
1304
+ | Layer | Implementation |
1305
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
1306
+ | PII Redaction | 32 field names censored at ANY depth in one snapshotting walk — ~3.6 µs/entry |
1307
+ | Credentials | `password`, `passwordHash`, `token`, `accessToken`, `refreshToken`, `apiKey`, `apiSecret`, `privateKey` |
1308
+ | MFA Secrets | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` — redacted to the 100-level ceiling |
1309
+ | PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
1310
+ | LGPD Documents | `cpf`, `cnpj`, `rg`, plus `email` as a conservative default |
1311
+ | HTTP Headers | `authorization`, `cookie`, `x-api-key`, `x-auth-token`, `set-cookie` — absolute paths on `req` / `res` |
1312
+ | Redact List | `DEFAULT_REDACT_PATHS` is append-only; removal requires a major version |
1313
+ | Trace Injection | `isValidTraceId` / `isValidSpanId` validated before any identifier is written |
1314
+ | URL Cardinality | UUIDs, ULIDs, nanoids and numeric IDs normalized to `:id` — bounds label cardinality and keeps raw identifiers out of the path |
1315
+ | Destination Failure | Every `write()` try/catch-wrapped; a failure is reported on `stderr` and swallowed, never propagated to the caller |
1316
+ | Field Size | Per-field UTF-8 cap at `maxEntrySizeBytes` (64 KB default); oversized values become a truncation envelope |
1317
+ | Supply Chain | `"dependencies": {}` — no transitive runtime packages of the library's own choosing |
1245
1318
 
1246
1319
  > [!IMPORTANT]
1247
1320
  > Redaction protects the fields it knows about. Any new field carrying a secret or a personal document must be added to `redactPaths` — the default list covers the standard set, not your domain's.
@@ -1256,8 +1329,8 @@ When integrating `@bymax-one/nest-logger` in production, verify each of the foll
1256
1329
  [![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
1257
1330
  [![Jest](https://img.shields.io/badge/Jest-30-C21325?style=flat-square&logo=jest)](https://jestjs.io)
1258
1331
  [![Stryker](https://img.shields.io/badge/Stryker-Mutation_Testing-red?style=flat-square)](https://stryker-mutator.io)
1259
- [![pnpm](https://img.shields.io/badge/pnpm-10-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
1260
- [![tsup](https://img.shields.io/badge/tsup-8-orange?style=flat-square)](https://tsup.egoist.dev)
1332
+ [![pnpm](https://img.shields.io/badge/pnpm-11-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
1333
+ [![tsup](https://img.shields.io/badge/tsup-9-orange?style=flat-square)](https://tsup.egoist.dev)
1261
1334
 
1262
1335
  ---
1263
1336
 
@@ -1341,6 +1414,7 @@ There are **two** APIs on this class, and they do not share a signature shape.
1341
1414
  | `run` | `(store, callback)` | Opens an ALS scope. Any log inside `callback` carries `store` fields. **A nested `run()` REPLACES the enclosing context** — see below |
1342
1415
  | `runMerged` | `(store, callback)` | Opens a scope that EXTENDS the enclosing one: outer fields are carried in, `store` overrides them key by key |
1343
1416
  | `set` | `(key, value)` | Adds/updates a field in the current scope. Throws if called outside `run()`. Fields set here reach every subsequent log in the scope |
1417
+ | `get` | `(key)` | Reads one field from the current scope, or `undefined` when unset or outside a `run()` call |
1344
1418
  | `getStore` | `()` | Returns current scope or `undefined` outside a `run()` call |
1345
1419
 
1346
1420
  > [!IMPORTANT]
@@ -1357,11 +1431,26 @@ There are **two** APIs on this class, and they do not share a signature shape.
1357
1431
 
1358
1432
  ### Decorators
1359
1433
 
1360
- | Decorator | Target | Description |
1361
- | ------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1362
- | `@InjectLogger(context?)` | Constructor param | Injects `PinoLoggerService` pre-bound to the given context string |
1363
- | `@LogContext(name)` | Class | Records a context label as class metadata. ⚠️ Metadata only — it does NOT open a `logContext.run()` scope; use `@InjectLogger(MyClass.name)` for a per-class label |
1364
- | `@LogPerformance(thresholdMs?)` | Method | Logs `METHOD_EXECUTION` on completion; `METHOD_SLOW_EXECUTION` if duration exceeds threshold |
1434
+ | Decorator | Target | Description |
1435
+ | ------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1436
+ | `@InjectLogger(context?)` | Constructor param | Injects `PinoLoggerService` pre-bound to the given context string |
1437
+ | `@LogContext(name)` | Class | **Deprecated.** Records a context label as class metadata that nothing in this package reads, so the label is never applied, and it does NOT open a `logContext.run()` scope. Use `@InjectLogger(MyClass.name)` for a per-class label |
1438
+ | `@LogPerformance(thresholdMs?)` | Method | Logs `METHOD_EXECUTION` on completion; `METHOD_SLOW_EXECUTION` if duration exceeds threshold |
1439
+
1440
+ ### Injection tokens and helpers
1441
+
1442
+ Every token is a `Symbol`, never a string, so it cannot collide with one of yours. Inject with an explicit `@Inject(TOKEN)`: the package is built without decorator metadata, and so is any consumer built the same way.
1443
+
1444
+ | Export | Provides | Use it to |
1445
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1446
+ | `LOGGER_OPTIONS_TOKEN` | the resolved `ResolvedBymaxLoggerModuleOptions` | read the effective configuration, defaults applied |
1447
+ | `LOGGER_PINO_INSTANCE_TOKEN` | the underlying Pino instance | hand it to a library that wants a Pino logger |
1448
+ | `LOGGER_DESTINATIONS_TOKEN` | the destinations array, `readonly ILogDestination[]` | inspect what is registered; do not mutate it |
1449
+ | `LOGGER_REDACTOR_TOKEN` | the by-name redaction function, `(value: unknown) => unknown`. With `redactStrategy: 'paths'` it returns its input unchanged, because Pino's path redaction then runs at write time instead | apply the module's by-name redaction to a value of your own under the default `'names'` strategy; do not treat it as a sanitization boundary under `'paths'`. |
1450
+ | `LOG_CONTEXT_TOKEN` | the `LogContextService` instance (an alias of the class) | inject the context by token |
1451
+ | `REDACT_COMMON_FIELDS` | the default sensitive field **names**, matched at any depth | extend the defaults by reference instead of copying the list |
1452
+ | `DEFAULT_REDACT_PATHS` | the legacy path expansion of those names | only with `redactStrategy: 'paths'` |
1453
+ | `isValidTraceId(id)` | `true` for 32 lowercase hex characters that are not all zeros | apply the rule the trace mixin applies before it injects an id |
1365
1454
 
1366
1455
  ### Level maps (from `@bymax-one/nest-logger`)
1367
1456
 
@@ -1482,19 +1571,31 @@ interface ILogDestination {
1482
1571
 
1483
1572
  ## 🚨 Error Code Catalog
1484
1573
 
1485
- | Code | Surfaces as | When | Action |
1486
- | --------------------------------- | ---------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
1487
- | `LOGGER_INVALID_OPTIONS` | Thrown `Error` at init | `service`, `service.name`, or `service.version` missing or empty | Add the required fields |
1488
- | `LOGGER_INVALID_LEVEL` | Thrown `Error` at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
1489
- | `LOGGER_PRETTY_UNAVAILABLE` | Thrown `Error` at `onInit()` | `PrettyDevDestination` registered but `pino-pretty` not installed | Install `pino-pretty` or drop the destination |
1490
- | `LOGGER_OTEL_API_UNAVAILABLE` | Nothing emitted | `@opentelemetry/api` cannot be resolved | None — the trace mixin steps aside silently |
1491
- | `LOGGER_DESTINATION_INIT_FAILED` | Structured error log | `destination.onInit()` rejects | Sink is excluded from shutdown; boot continues, writes stay fail-soft |
1492
- | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
1493
- | `LOGGER_CONTEXT_OUT_OF_SCOPE` | Thrown `Error` | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
1494
- | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | A serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
1574
+ Two kinds of failure, and they surface differently. A **thrown `Error`** carries no code string: it carries a message prefixed with the emitting class, which is what you match on. A **log key** is a code you can filter on: it is written to `stderr` as a structured line, or into the envelope as `_logKey`.
1495
1575
 
1496
- > [!NOTE]
1497
- > The codes above name the condition, not a machine-readable field. Thrown errors carry a human-readable message prefixed with the emitting class (e.g. `[BymaxLoggerModule] options.service is required`), not the code string.
1576
+ ### Thrown errors
1577
+
1578
+ | Message starts with | Thrown from | When | Action |
1579
+ | -------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
1580
+ | `[BymaxLoggerModule] options.` | `forRoot` / `forRootAsync`, at module setup | any option fails validation: a missing `service`, a `level` outside the set, a bad type | Fix the option the message names; it prints the path and what it received |
1581
+ | `[BymaxLoggerModule] useNestLogger(app) called but` | `BymaxLoggerModule.useNestLogger` | the module was never imported into the application | Import `BymaxLoggerModule` before calling it |
1582
+ | `[BymaxLoggerModule] applyAccessLog(app) called but` | `applyAccessLog` | the module was never imported into the application | Import `BymaxLoggerModule` before calling it |
1583
+ | `[PrettyDevDestination] pino-pretty is not installed` | `PrettyDevDestination.onInit` | the destination is registered and the optional peer is missing | Install `pino-pretty` or drop the destination |
1584
+ | `[PrettyDevDestination] refused in production` | `PrettyDevDestination.onInit` | `NODE_ENV` is exactly `production` | Use `DefaultStdoutDestination` in production |
1585
+ | `[LogContextService] set() called outside run() scope` | `LogContextService.set` | called with no active `run()` | Wrap the work in `logContext.run({ ... }, () => ...)` |
1586
+ | `[LogContextService] Refusing to set unsafe context key` | `LogContextService.set` | the key is `__proto__`, `constructor` or `prototype` | Use an ordinary key |
1587
+
1588
+ The two `PrettyDevDestination` errors are thrown inside `onInit`, which the registry contains: the boot continues and the failure is reported as `LOGGER_DESTINATION_INIT_FAILED` below.
1589
+
1590
+ ### Log keys
1591
+
1592
+ | Code | Surfaces as | When | Action |
1593
+ | --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
1594
+ | `LOGGER_DESTINATION_INIT_FAILED` | NDJSON line on `stderr` | `destination.onInit()` throws or rejects | Sink is removed from the write fan-out and shutdown; boot continues |
1595
+ | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
1596
+ | `LOGGER_BOOTSTRAP_WARNING` | log entry with `reason: 'OTEL_API_UNAVAILABLE'` | `otel.shouldAutoInjectTraceContext` is on and `@opentelemetry/api` cannot be resolved | Install `@opentelemetry/api`, or turn the option off; until then `traceId` and `spanId` are absent |
1597
+ | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | a serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
1598
+ | `LOGGER_REDACTION_FAILED` | `_logKey` in the envelope, with `_redactionFailed: true` | a record cannot be walked by the redactor (a throwing getter, a hostile proxy) | The record is dropped whole, never partly emitted; the message and ambient context stay. Find the value that broke the walk |
1498
1599
 
1499
1600
  ---
1500
1601
 
@@ -1536,7 +1637,8 @@ By design, the following are out of scope:
1536
1637
  - ❌ **Log aggregation UI** — Grafana, Datadog, Honeycomb
1537
1638
  - ❌ **Sentry SDK** — install `@sentry/node` in the consumer if desired
1538
1639
  - ❌ **Immutable audit logs** — future `@bymax-one/nest-audit`
1539
- - ❌ **File rotation built-in** — use `RollingFileDestination` with `pino-roll`
1640
+ - ❌ **File rotation built-in** — use the `RollingFileDestination` example above, built on `pino-roll`
1641
+ - ❌ **Fastify** — the HTTP integration targets the Express adapter (`@nestjs/platform-express`); a Fastify adapter is not supported
1540
1642
  - ❌ **`pino-http`** — the `HttpLoggingInterceptor` is a NestJS-native replacement; install `pino-http` independently if you prefer it (produces duplicate logs if both are active)
1541
1643
 
1542
1644
  ---