@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 +118 -1
- package/README.md +183 -81
- package/dist/server/index.cjs +159 -6
- package/dist/server/index.d.cts +74 -22
- package/dist/server/index.d.ts +74 -22
- package/dist/server/index.mjs +157 -7
- package/package.json +1 -1
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.
|
|
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?**
|
|
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 (
|
|
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
|
|
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.**
|
|
174
|
-
>
|
|
175
|
-
>
|
|
176
|
-
>
|
|
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
|
|
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
|
|
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
|
|
712
|
-
| ---------------------------- |
|
|
713
|
-
| `service.name` | `string`
|
|
714
|
-
| `service.version` | `string`
|
|
715
|
-
| `service.namespace` | `string`
|
|
716
|
-
| `service.instanceId` | `string`
|
|
717
|
-
| `service.environment` | `string`
|
|
718
|
-
| `resourceFormat` | `'nested' \| 'flat'`
|
|
719
|
-
| `eventNameField` | `string \| false`
|
|
720
|
-
| `errorFormat` | `'pino' \| 'semconv' \| 'both'`
|
|
721
|
-
| `level` | `LogLevel`
|
|
722
|
-
| `redactPaths` | `string[]`
|
|
723
|
-
| `redactStrategy` | `'names' \| 'paths'`
|
|
724
|
-
| `shouldDisableDefaultRedact` | `boolean`
|
|
725
|
-
| `redactCensor` | `string`
|
|
726
|
-
| `maxEntrySizeBytes` | `number`
|
|
727
|
-
| `
|
|
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
|
|
732
|
-
| ------------------------------ | ---------- |
|
|
733
|
-
| `http.isEnabled` | `boolean` | `false`
|
|
734
|
-
| `http.shouldCaptureExceptions` | `boolean` | `true`
|
|
735
|
-
| `http.shouldGenerateRequestId` | `boolean` | `true`
|
|
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'`
|
|
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
|
|
742
|
-
| ----------------------------------- | ----------------------------- |
|
|
743
|
-
| `otel.shouldAutoInjectTraceContext` | `boolean` | `true`
|
|
744
|
-
| `otel.fieldFormat` | `'camelCase' \| 'snake_case'` | `'camelCase'`
|
|
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
|
|
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,
|
|
1106
|
+
└── PinoLoggerService.info(logKey, message, userId?, metadata?)
|
|
1034
1107
|
│
|
|
1035
1108
|
▼
|
|
1036
1109
|
Pino logger
|
|
1037
1110
|
│
|
|
1038
|
-
|
|
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 │ ←
|
|
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`
|
|
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
|
|
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
|
[](https://nodejs.org)
|
|
1257
1330
|
[](https://jestjs.io)
|
|
1258
1331
|
[](https://stryker-mutator.io)
|
|
1259
|
-
[](https://pnpm.io)
|
|
1333
|
+
[](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
|
|
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
|
-
|
|
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
|
-
|
|
1497
|
-
|
|
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`
|
|
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
|
---
|