@bymax-one/nest-logger 1.0.1 → 1.0.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +263 -135
  3. package/package.json +39 -12
package/CHANGELOG.md CHANGED
@@ -11,6 +11,58 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.0.3] - 2026-07-29
15
+
16
+ ### Fixed
17
+
18
+ - **Declared `@types/express` as an optional peer dependency.** The published
19
+ declarations reference `Request` / `Response` / `NextFunction` from `express`
20
+ (the HTTP interceptor, the exception filter and the request-id middleware all
21
+ type their signatures with them), but the requirement was undeclared. A
22
+ consumer compiling with `skipLibCheck: false` and no express types installed
23
+ hit `error TS2307: Cannot find module 'express'` from inside
24
+ `dist/server/index.d.ts` with nothing in `package.json` pointing at the cause.
25
+ Present since 1.0.0 — the imports are `import type` only, so runtime was never
26
+ affected and no express code is bundled.
27
+
28
+ It is marked optional because nothing in the library needs express at runtime:
29
+ a consumer on a non-express adapter (Fastify) can ignore it, as can anyone
30
+ keeping `skipLibCheck: true` — the default the Nest CLI scaffolds.
31
+
32
+ ## [1.0.2] - 2026-07-29
33
+
34
+ ### Fixed
35
+
36
+ - **CommonJS consumers no longer receive ESM type declarations.** Both subpaths
37
+ now declare `types` per condition, so `require()` resolves to the `.d.cts`
38
+ declarations that match the `.cjs` runtime. Previously a single `types` entry
39
+ pointed at the `.d.ts` files, which — with `"type": "module"` — TypeScript
40
+ reads as ESM, so a project on `moduleResolution: node16` / `nodenext`
41
+ importing from CommonJS got declarations for the wrong module format.
42
+ - Added top-level `main`, `module` and `types` so the root entrypoint also
43
+ resolves under the legacy `moduleResolution: node` algorithm. The `./shared`
44
+ subpath remains unresolvable in that mode, which is inherent to subpath
45
+ exports and not fixable without a directory shim.
46
+ - Exposed `./package.json` through the `exports` map. Reading it previously
47
+ failed with `ERR_PACKAGE_PATH_NOT_EXPORTED`, which breaks tooling that
48
+ inspects an installed package's manifest.
49
+ - **The `./shared` subpath now resolves under `moduleResolution: node`.** A
50
+ `typesVersions` map points the subpath at its `.d.cts` declarations, which is
51
+ the only mechanism that resolution algorithm understands — it predates
52
+ `exports` and ignores it entirely. Without this, a consumer whose tsconfig
53
+ sets `module: commonjs` without an explicit `moduleResolution` (the default
54
+ the Nest CLI scaffolds, which falls back to `node`) got
55
+ `error TS2307: Cannot find module '@bymax-one/nest-logger/shared'` while the
56
+ root entrypoint resolved fine. Runtime was never affected: Node reads
57
+ `exports`, so `require('@bymax-one/nest-logger/shared')` always worked.
58
+
59
+ ### Added
60
+
61
+ - `pnpm check:exports` (`@arethetypeswrong/cli`) — packs the tarball and
62
+ resolves every entrypoint the way each module resolution mode would, in the
63
+ strict profile — every entrypoint in every resolution mode. Wired into CI and
64
+ into the release workflow ahead of the publish step.
65
+
14
66
  ## [1.0.1] - 2026-07-29
15
67
 
16
68
  Supply-chain and tooling hardening only. `src/` is unchanged since 1.0.0, so the
@@ -96,7 +148,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
96
148
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
97
149
  `release.yml`, Dependabot, and issue templates
98
150
 
99
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.1...HEAD
151
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.3...HEAD
152
+ [1.0.3]: https://github.com/bymaxone/nest-logger/compare/v1.0.2...v1.0.3
153
+ [1.0.2]: https://github.com/bymaxone/nest-logger/compare/v1.0.1...v1.0.2
100
154
  [1.0.1]: https://github.com/bymaxone/nest-logger/compare/v1.0.0...v1.0.1
101
155
  [1.0.0]: https://github.com/bymaxone/nest-logger/compare/v0.1.0...v1.0.0
102
156
  [0.1.0]: https://github.com/bymaxone/nest-logger/releases/tag/v0.1.0
package/README.md CHANGED
@@ -39,59 +39,122 @@
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
 
42
+ ### Why nest-logger?
43
+
44
+ - **🎯 One module, the whole pipeline** — Logger service, HTTP interceptor, exception filter, context propagation, redaction, and destinations arrive in a single `forRoot()`. No gluing together `pino-http`, a redaction layer, and a transport by hand.
45
+ - **🔌 Your sinks, your rules** — The library defines `ILogDestination`. You implement it for Loki, Postgres, a rolling file, or anything else. No vendor lock-in, no hidden transport dependencies.
46
+ - **🔒 Redacted by default** — 113 redact paths compile at bootstrap covering the standard set: passwords, tokens, PCI DSS card data, MFA secrets, and LGPD documents. Domain-specific field names are yours to add via `redactPaths`.
47
+ - **⚡ On the hot path, so it stays cheap** — Singleton providers, one composed Pino mixin, and a pre-compiled redactor. No `Scope.REQUEST`, no per-log regex matching.
48
+ - **🔭 Correlated when you need it** — When an OpenTelemetry span is active, `traceId`/`spanId`/`traceFlags` land in every entry. When the peer is absent, the mixin steps aside at zero cost.
49
+
50
+ ```
51
+ pnpm add @bymax-one/nest-logger
52
+ ```
53
+
42
54
  ---
43
55
 
44
56
  ## 🔥 Features
45
57
 
46
- - **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
47
- - **PII redaction by default** — 113 paths covering passwords, tokens, generic secrets, PCI DSS card data, MFA secrets, CPF/CNPJ/RG (LGPD), and common HTTP headers — powered by `fast-redact`
48
- - **OpenTelemetry correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
49
- - **AsyncLocalStorage context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
50
- - **HTTP logging interceptor** — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by `:id` placeholder)
51
- - **Exception filter** — captures NestJS `HttpException` and unexpected errors with structured output
52
- - **Pluggable destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
53
- - **NestJS `LoggerService` bridge** — drop-in replacement; all NestJS internal logs flow through Pino
54
- - **`MODULE_ACTION_RESULT` log key convention** — enforced by an exported regex for CI validation
55
- - **Pretty-print in dev** — auto-enabled when `NODE_ENV !== 'production'` (requires optional `pino-pretty`)
56
- - **Entry size guard** — truncates oversized entries (default 64 KB) with a structured warning instead of silently dropping them
57
- - **Zero-downtime destination lifecycle** — `onInit()` / `onShutdown()` hooks; a failing destination is removed without affecting others
58
+ ### 📝 Core Logging
59
+
60
+ - ✅ **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
61
+ - ✅ **`MODULE_ACTION_RESULT` Log Keys** — a naming convention enforced by an exported regex for CI validation
62
+ - ✅ **NestJS `LoggerService` Bridge** — drop-in replacement; all NestJS internal logs flow through Pino
63
+ - ✅ **Pretty-Print in Dev** — opt-in `PrettyDevDestination` for readable local output (requires optional `pino-pretty`)
64
+ - ✅ **Field Size Guard** — a serialized field over the ceiling (default 64 KB) is replaced by a compact truncation envelope instead of flooding the sink
65
+
66
+ ### 🛡️ Security & Privacy
67
+
68
+ - ✅ **PII Redaction by Default** — 113 paths covering passwords, tokens, and generic secrets — powered by `fast-redact`
69
+ - ✅ **PCI DSS & MFA Coverage** — card data and MFA secrets redacted out of the box, with common HTTP auth headers
70
+ - ✅ **LGPD-Aware Paths** — CPF, CNPJ, and RG redacted by default for Brazilian workloads
71
+ - ✅ **Append-Only Redact List** — `DEFAULT_REDACT_PATHS` never shrinks without a major version; extend it via `redactPaths`
72
+ - ✅ **Validated Trace IDs** — OTel identifiers pass `isValidTraceId` before injection, never raw user input
73
+
74
+ ### 🔍 Observability & Context
75
+
76
+ - ✅ **OpenTelemetry Correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
77
+ - ✅ **AsyncLocalStorage Context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
78
+ - ✅ **HTTP Logging Interceptor** — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by `:id`)
79
+ - ✅ **Exception Filter** — captures NestJS `HttpException` and unexpected errors with structured output
80
+
81
+ ### 🔌 Destinations
82
+
83
+ - ✅ **Pluggable Destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
84
+ - ✅ **Managed Lifecycle** — `onInit()` / `onShutdown()` hooks; a destination that fails `onInit()` is reported and excluded from shutdown, and boot is never aborted
85
+ - ✅ **Crash-Proof Writes** — every `write()` is wrapped in a try/catch; a failure is reported on `stderr` and swallowed, never propagated to the app
86
+
87
+ ### 🧩 Developer Experience
88
+
89
+ - ✅ **Zero Runtime Dependencies** — everything arrives as a peer dependency, so you control versions and supply-chain surface
90
+ - ✅ **2 Subpath Exports** — `.` for the NestJS server API, `./shared` for zero-dependency types and constants
91
+ - ✅ **Dynamic Module** — configure via `forRoot()` or `forRootAsync()`, sensible defaults included
92
+ - ✅ **Strict TypeScript** — `strict: true`, no `any` in production code, JSDoc on every public export
93
+ - ✅ **100% Coverage + Mutation Tested** — statements, branches, functions, and lines gated at 100%, with Stryker as the deeper gate
58
94
 
59
95
  ---
60
96
 
61
97
  ## 📦 Subpath Exports
62
98
 
63
- The package ships two entry points:
99
+ One package, two entry points — import only what your app needs:
100
+
101
+ | Subpath | Import | Purpose | Dependencies |
102
+ | ---------- | ------------------------------- | ------------------------------------------------------------------------------- | :-------------------------------------: |
103
+ | **Server** | `@bymax-one/nest-logger` | NestJS module, logger service, interceptor, filter, decorators, destinations | NestJS 11, pino, rxjs, reflect-metadata |
104
+ | **Shared** | `@bymax-one/nest-logger/shared` | Types, constants, the log-key regex — `LogLevel`, `LogEntry`, `ServiceMetadata` | None |
64
105
 
65
- | Import path | Contents | Deps |
66
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
67
- | `@bymax-one/nest-logger` | Full server API: `BymaxLoggerModule`, `PinoLoggerService`, `LogContextService`, destinations, decorators, interceptors | NestJS + Pino peer deps |
68
- | `@bymax-one/nest-logger/shared` | Zero-dependency utilities: `LOG_KEYS_CONVENTION_REGEX`, `RESERVED_LOG_KEYS`, types (`LogLevel`, `LogEntry`, `ServiceMetadata`) | None |
106
+ ```
107
+ shared (zero deps)
108
+ ↑
109
+ server
110
+ ```
69
111
 
70
112
  The `/shared` subpath is safe to import in isomorphic code, test helpers, CLI scripts, or shared packages that must not pull in NestJS.
71
113
 
72
114
  ---
73
115
 
74
- ## 📥 Installation
116
+ > [!TIP]
117
+ > Prefer to learn from a working app? See the [nest-logger-example](https://github.com/bymaxone/nest-logger-example) — a full NestJS project wired with this library.
118
+
119
+ ## 🚀 Quick Start
120
+
121
+ ### 1. Install
75
122
 
76
123
  ```bash
77
- # Install the library
124
+ # Using pnpm (recommended)
78
125
  pnpm add @bymax-one/nest-logger
79
126
 
80
- # Required peer dependencies
81
- pnpm add pino reflect-metadata
127
+ # Using npm
128
+ npm install @bymax-one/nest-logger
129
+
130
+ # Using yarn
131
+ yarn add @bymax-one/nest-logger
132
+ ```
133
+
134
+ > [!IMPORTANT]
135
+ > You must also install the required **peer dependencies**. The library ships `"dependencies": {}`, so nothing arrives implicitly:
82
136
 
83
- # Optional — pretty dev output
137
+ ```bash
138
+ # Server subpath (required)
139
+ pnpm add @nestjs/common @nestjs/core pino reflect-metadata rxjs
140
+
141
+ # Optional — pretty local output via PrettyDevDestination
84
142
  pnpm add -D pino-pretty
85
143
 
86
- # Optional — OpenTelemetry correlation
144
+ # Optional — OpenTelemetry trace correlation
87
145
  pnpm add @opentelemetry/api @opentelemetry/sdk-node
146
+
147
+ # Optional — express types, only if you compile with `skipLibCheck: false`
148
+ pnpm add -D @types/express
88
149
  ```
89
150
 
90
- ---
151
+ > [!NOTE]
152
+ > `@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.
91
153
 
92
- ## 🚀 Quick Start
154
+ > [!NOTE]
155
+ > `@types/express` is an **optional type-only peer**. The HTTP interceptor, the exception filter and the request-id middleware type their signatures with express's `Request` / `Response` / `NextFunction`, so the published declarations import them. Nothing needs express at runtime — no express code is bundled. You only need the types installed if you compile with `skipLibCheck: false`; with the Nest CLI default (`skipLibCheck: true`), or on a non-express adapter, you can ignore it.
93
156
 
94
- ### Step 1 — Register the module
157
+ ### 2. Register the module
95
158
 
96
159
  ```typescript
97
160
  // app.module.ts
@@ -110,9 +173,9 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
110
173
  export class AppModule {}
111
174
  ```
112
175
 
113
- `isPretty` defaults to `true` when `NODE_ENV !== 'production'` — no extra config needed in development.
176
+ Stdout is wired automatically — `DefaultStdoutDestination` is always active, so this is all you need to be logging structured JSON.
114
177
 
115
- ### Step 2 — Async configuration with `ConfigService`
178
+ ### 3. Async configuration with `ConfigService`
116
179
 
117
180
  ```typescript
118
181
  // app.module.ts
@@ -140,7 +203,7 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
140
203
  export class AppModule {}
141
204
  ```
142
205
 
143
- ### Step 3 — Inject the logger in a service
206
+ ### 4. Inject the logger in a service
144
207
 
145
208
  ```typescript
146
209
  // payments.service.ts
@@ -209,7 +272,7 @@ Output (pretty-print, development):
209
272
  stripeRefundId: "re_abc"
210
273
  ```
211
274
 
212
- ### Step 4 — HTTP logging (automatic)
275
+ ### 5. HTTP logging (automatic)
213
276
 
214
277
  Enable `http.isEnabled: true` in the module options. The `HttpLoggingInterceptor` is registered globally and emits:
215
278
 
@@ -225,7 +288,7 @@ Enable `http.isEnabled: true` in the module options. The `HttpLoggingInterceptor
225
288
 
226
289
  URLs are automatically normalized — `/users/550e8400-e29b-41d4-a716-446655440000` becomes `/users/:id` so Loki/Grafana cardinality stays bounded.
227
290
 
228
- ### Step 5 — OpenTelemetry correlation
291
+ ### 6. OpenTelemetry correlation
229
292
 
230
293
  Initialize the OTel SDK **before** importing NestJS — this is critical:
231
294
 
@@ -271,7 +334,7 @@ void bootstrap()
271
334
 
272
335
  Once active, every log entry automatically carries `traceId`, `spanId`, and `traceFlags`. Click the `traceId` in Grafana to jump directly to the correlated span in Tempo or Honeycomb.
273
336
 
274
- ### Step 6 — Context propagation with `LogContextService`
337
+ ### 7. Context propagation with `LogContextService`
275
338
 
276
339
  ```typescript
277
340
  // request-id.middleware.ts
@@ -300,17 +363,17 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
300
363
 
301
364
  ### Top-level options
302
365
 
303
- | Option | Type | Default | Description |
304
- | ---------------------------- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
305
- | `service.name` | `string` | **Required** | Service name emitted in every log entry |
306
- | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
307
- | `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
308
- | `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
309
- | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
310
- | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
311
- | `redactCensor` | `string \| function` | `'[REDACTED]'` | Replacement value for redacted fields. A function receives the raw value |
312
- | `maxEntrySizeBytes` | `number` | `65536` | Entries larger than this are replaced by a structured `LOGGER_ENTRY_TRUNCATED` warning |
313
- | `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
366
+ | Option | Type | Default | Description |
367
+ | ---------------------------- | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
368
+ | `service.name` | `string` | **Required** | Service name emitted in every log entry |
369
+ | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
370
+ | `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
371
+ | `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
372
+ | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
373
+ | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
374
+ | `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
375
+ | `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
376
+ | `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
314
377
 
315
378
  ### `http` options
316
379
 
@@ -331,54 +394,6 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
331
394
 
332
395
  ---
333
396
 
334
- ## 🔐 PII Redaction
335
-
336
- ### Default paths
337
-
338
- The library ships **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
339
-
340
- | Category | Fields |
341
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
342
- | Passwords | `password`, `passwordHash`, `passwordConfirm`, `newPassword`, `oldPassword` |
343
- | Tokens | `token`, `accessToken`, `refreshToken`, `idToken`, `apiKey`, `apiSecret` |
344
- | MFA | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` |
345
- | Generic secrets | `secret`, `clientSecret`, `signingSecret`, `privateKey` |
346
- | Payment / PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
347
- | Personal documents (LGPD) | `cpf`, `cnpj`, `rg` |
348
- | Conservative PII | `email` |
349
- | HTTP headers (absolute) | `req.headers.authorization`, `req.headers.cookie`, `req.headers["x-api-key"]`, `req.headers["x-auth-token"]`, `res.headers["set-cookie"]` |
350
-
351
- Every field is listed at wildcard depths 1–4 (`*.field`, `*.*.field`, `*.*.*.field`, `*.*.*.*.field`) because `fast-redact`'s `*` matches a single level only — not recursive.
352
-
353
- ### Extending the defaults
354
-
355
- ```typescript
356
- BymaxLoggerModule.forRoot({
357
- service: { name: 'my-app', version: '1.0.0' },
358
- redactPaths: [
359
- '*.internalSecret', // depth-1 wildcard
360
- 'body.creditCard.*', // all fields inside a subobject
361
- 'payload.user.taxId' // exact path
362
- ]
363
- })
364
- ```
365
-
366
- The extra paths are **merged** with the defaults — never replacing them.
367
-
368
- ### Disabling defaults (not recommended)
369
-
370
- ```typescript
371
- BymaxLoggerModule.forRoot({
372
- service: { name: 'my-app', version: '1.0.0' },
373
- shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
374
- redactPaths: ['*.password'] // you own the full list
375
- })
376
- ```
377
-
378
- A `LOGGER_BOOTSTRAP_WARNING` entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.
379
-
380
- ---
381
-
382
397
  ## 🔑 Log Key Convention
383
398
 
384
399
  All structured log calls must use the `MODULE_ACTION_RESULT` format:
@@ -587,12 +602,151 @@ Application Service
587
602
  └─────────────────────────────┘
588
603
  ```
589
604
 
590
- **Key design decisions:**
605
+ ### Design Principles
591
606
 
592
- - **Singleton scope, not `Scope.REQUEST`** — `AsyncLocalStorage` delivers per-request context at zero latency overhead. NestJS request scope adds ~5% latency on the injection graph; unacceptable on a logger that runs every request.
593
- - **One Pino mixin** — ALS context and OTel trace context are composed into a single mixin. Merge order is deterministic: ALS fields first, then OTel (OTel wins on name conflicts because an active span is the authoritative trace identity).
594
- - **Zero direct dependencies** — the library ships with `"dependencies": {}`. All packages arrive as peer dependencies so consumers control exact versions.
595
- - **`fast-redact` compiled at init** — 113 paths compile once at module bootstrap into a specialized JS function. No per-log regex matching.
607
+ | Principle | Description |
608
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
609
+ | **🪶 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 |
610
+ | **🧬 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 |
611
+ | **⚡ Compiled Redaction** | 113 `fast-redact` paths compile once at module bootstrap into a specialized function; no per-log regex matching, under 3% throughput impact |
612
+ | **🔌 Interface-Driven Sinks** | `ILogDestination` is a contract — Loki, Postgres, rolling files, or anything else is a consumer implementation, never a dependency of this package |
613
+ | **🌳 Zero Runtime Deps** | `"dependencies": {}` — every package arrives as a peer dependency, so consumers pin exact versions and the supply-chain surface stays theirs |
614
+
615
+ ---
616
+
617
+ ## 🔐 Security Model
618
+
619
+ A logger sees every payload the application handles, so the security posture is about what **never** reaches the sink — and about a sink failure never reaching the application.
620
+
621
+ ### Redaction by default
622
+
623
+ The library ships **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
624
+
625
+ | Category | Fields |
626
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
627
+ | Passwords | `password`, `passwordHash`, `passwordConfirm`, `newPassword`, `oldPassword` |
628
+ | Tokens | `token`, `accessToken`, `refreshToken`, `idToken`, `apiKey`, `apiSecret` |
629
+ | MFA | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` |
630
+ | Generic secrets | `secret`, `clientSecret`, `signingSecret`, `privateKey` |
631
+ | Payment / PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
632
+ | Personal documents (LGPD) | `cpf`, `cnpj`, `rg` |
633
+ | Conservative PII | `email` |
634
+ | HTTP headers (absolute) | `req.headers.authorization`, `req.headers.cookie`, `req.headers["x-api-key"]`, `req.headers["x-auth-token"]`, `res.headers["set-cookie"]` |
635
+
636
+ Every field is listed at wildcard depths 1–4 (`*.field`, `*.*.field`, `*.*.*.field`, `*.*.*.*.field`) because `fast-redact`'s `*` matches a single level only — not recursive.
637
+
638
+ ### Extending the defaults
639
+
640
+ ```typescript
641
+ BymaxLoggerModule.forRoot({
642
+ service: { name: 'my-app', version: '1.0.0' },
643
+ redactPaths: [
644
+ '*.internalSecret', // depth-1 wildcard
645
+ 'body.creditCard.*', // all fields inside a subobject
646
+ 'payload.user.taxId' // exact path
647
+ ]
648
+ })
649
+ ```
650
+
651
+ The extra paths are **merged** with the defaults — never replacing them.
652
+
653
+ ### Disabling defaults (not recommended)
654
+
655
+ ```typescript
656
+ BymaxLoggerModule.forRoot({
657
+ service: { name: 'my-app', version: '1.0.0' },
658
+ shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
659
+ redactPaths: ['*.password'] // you own the full list
660
+ })
661
+ ```
662
+
663
+ A `LOGGER_BOOTSTRAP_WARNING` entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.
664
+
665
+ ### An append-only default list
666
+
667
+ `DEFAULT_REDACT_PATHS` is append-only by contract: a path may be added in a minor release, but removing one requires a major version bump. A field that stopped being redacted silently is a leak that no consumer would notice — the version number is what makes it visible.
668
+
669
+ ### Validated trace identifiers
670
+
671
+ Trace context is never copied verbatim into a log entry. The OTel mixin reads the active span and passes its identifiers through `isValidTraceId` / `isValidSpanId` before injecting them, so a malformed or attacker-influenced value cannot be written into the correlation fields your dashboards and alerts key on.
672
+
673
+ ### Destination failures are contained
674
+
675
+ 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.
676
+
677
+ ### Bounded field size
678
+
679
+ Every serializer — the default `err` one and any you supply — is wrapped so a field whose serialized JSON exceeds `maxEntrySizeBytes` (64 KB by default) is replaced by a compact envelope: `_truncated`, `_logKey: 'LOGGER_ENTRY_TRUNCATED'`, `_originalSize`, and a 200-character `_preview`. An accidentally logged webhook payload costs a bounded number of bytes and leaves a record that truncation happened, instead of flooding the sink.
680
+
681
+ ### Security Checklist
682
+
683
+ When integrating `@bymax-one/nest-logger` in production, verify each of the following:
684
+
685
+ - `shouldDisableDefaultRedact` stays `false` — if it is on, the `LOGGER_BOOTSTRAP_WARNING` must be justified in a security review
686
+ - Every custom field carrying a secret or personal document is added to `redactPaths` at the depths it actually appears
687
+ - `http.excludePaths` regexes are anchored and linear-time — an unbounded pattern runs on every request URL (ReDoS)
688
+ - Custom destinations never re-serialize the raw request object; they receive the already-redacted `LogEntry`
689
+ - Destination credentials (Loki, Postgres, OTLP) come from the environment, never from values written into module options in source control
690
+ - Log retention at the sink matches your data-retention policy — redaction bounds what is stored, not how long
691
+
692
+ ---
693
+
694
+ ## 🛡️ Security Table
695
+
696
+ | Layer | Implementation |
697
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
698
+ | PII Redaction | 113 `fast-redact` paths compiled once at bootstrap — no per-log regex matching |
699
+ | Credentials | `password`, `passwordHash`, `token`, `accessToken`, `refreshToken`, `apiKey`, `apiSecret`, `privateKey` |
700
+ | MFA Secrets | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` — redacted at wildcard depths 1–4 |
701
+ | PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
702
+ | LGPD Documents | `cpf`, `cnpj`, `rg`, plus `email` as a conservative default |
703
+ | HTTP Headers | `authorization`, `cookie`, `x-api-key`, `x-auth-token`, `set-cookie` — absolute paths on `req` / `res` |
704
+ | Redact List | `DEFAULT_REDACT_PATHS` is append-only; removal requires a major version |
705
+ | Trace Injection | `isValidTraceId` / `isValidSpanId` validated before any identifier is written |
706
+ | URL Cardinality | UUIDs and numeric IDs normalized to `:id` — bounds label cardinality and keeps raw identifiers out of the path |
707
+ | Destination Failure | Every `write()` try/catch-wrapped; a failure is reported on `stderr` and swallowed, never propagated to the caller |
708
+ | Field Size | Per-field UTF-8 cap at `maxEntrySizeBytes` (64 KB default); oversized values become a truncation envelope |
709
+ | Supply Chain | `"dependencies": {}` — no transitive runtime packages of the library's own choosing |
710
+
711
+ > [!IMPORTANT]
712
+ > 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.
713
+
714
+ ---
715
+
716
+ ## 🧱 Tech Stack
717
+
718
+ [![Pino](https://img.shields.io/badge/Pino-10-green?style=flat-square)](https://getpino.io)
719
+ [![NestJS](https://img.shields.io/badge/NestJS-11-E0234E?style=flat-square&logo=nestjs)](https://nestjs.com)
720
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
721
+ [![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
722
+ [![Jest](https://img.shields.io/badge/Jest-30-C21325?style=flat-square&logo=jest)](https://jestjs.io)
723
+ [![Stryker](https://img.shields.io/badge/Stryker-Mutation_Testing-red?style=flat-square)](https://stryker-mutator.io)
724
+ [![pnpm](https://img.shields.io/badge/pnpm-10-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
725
+ [![tsup](https://img.shields.io/badge/tsup-8-orange?style=flat-square)](https://tsup.egoist.dev)
726
+
727
+ ---
728
+
729
+ ## 🧪 Testing & Quality
730
+
731
+ A logger runs on every request of every service that installs it, so the suite is held to a bar beyond "it runs" — every behavior is pinned so that a regression **fails a test**.
732
+
733
+ - ✅ **100% line coverage** — statements, branches, functions, and lines, enforced as a release gate across unit + e2e
734
+ - ✅ **97.42% mutation score** — verified with [Stryker](https://stryker-mutator.io/) against a `break` threshold of 95; 97.42% is the theoretical maximum for this codebase
735
+ - ✅ **Every survivor documented** — the 10 remaining mutants are equivalents, each recorded in [docs/mutation_testing_results.md](./docs/mutation_testing_results.md) rather than silenced with an inline comment, so the score is an accounting rather than a number
736
+ - ✅ **No real I/O in unit tests** — the Pino instance and every destination are mocked; e2e tests exercise the wired module through `@nestjs/testing` and supertest
737
+
738
+ ```bash
739
+ pnpm test # unit tests (Jest)
740
+ pnpm test:cov # coverage report
741
+ pnpm test:e2e # end-to-end tests (supertest)
742
+ pnpm test:cov:all # full coverage gate (100% statements/branches/functions/lines)
743
+ pnpm mutation # Stryker mutation testing (95% break gate)
744
+ pnpm typecheck # tsc strict check (all tsconfig variants)
745
+ pnpm lint # ESLint
746
+ ```
747
+
748
+ > [!NOTE]
749
+ > Line coverage proves a line _executed_ under test; mutation testing proves a test _would fail_ if that line were wrong. The full methodology and per-area breakdown are in [docs/mutation_testing_results.md](./docs/mutation_testing_results.md).
596
750
 
597
751
  ---
598
752
 
@@ -664,16 +818,19 @@ interface ILogDestination {
664
818
 
665
819
  ## 🚨 Error Code Catalog
666
820
 
667
- | Code | Severity | When | Action |
668
- | --------------------------------- | ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------- |
669
- | `LOGGER_INVALID_OPTIONS` | Throws at init | `service.name` or `service.version` missing | Add the required fields |
670
- | `LOGGER_INVALID_LEVEL` | Throws at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
671
- | `LOGGER_PRETTY_UNAVAILABLE` | Warn on bootstrap | `isPretty: true` but `pino-pretty` not installed | Install `pino-pretty` or set `isPretty: false` |
672
- | `LOGGER_OTEL_API_UNAVAILABLE` | Info on bootstrap | OTel mixin active but `@opentelemetry/api` not installed | Install the package or set `otel.shouldAutoInjectTraceContext: false` |
673
- | `LOGGER_DESTINATION_INIT_FAILED` | Error (logged) | `destination.onInit()` rejects | Destination is removed; other destinations continue |
674
- | `LOGGER_DESTINATION_WRITE_FAILED` | Warn (logged) | `destination.write()` throws | Entry skipped for that destination; others continue |
675
- | `LOGGER_CONTEXT_OUT_OF_SCOPE` | Throws | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
676
- | `LOGGER_ENTRY_TRUNCATED` | Warn (meta-log) | Entry exceeds `maxEntrySizeBytes` | Reduce metadata or raise the limit |
821
+ | Code | Surfaces as | When | Action |
822
+ | --------------------------------- | ---------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
823
+ | `LOGGER_INVALID_OPTIONS` | Thrown `Error` at init | `service`, `service.name`, or `service.version` missing or empty | Add the required fields |
824
+ | `LOGGER_INVALID_LEVEL` | Thrown `Error` at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
825
+ | `LOGGER_PRETTY_UNAVAILABLE` | Thrown `Error` at `onInit()` | `PrettyDevDestination` registered but `pino-pretty` not installed | Install `pino-pretty` or drop the destination |
826
+ | `LOGGER_OTEL_API_UNAVAILABLE` | Nothing emitted | `@opentelemetry/api` cannot be resolved | None — the trace mixin steps aside silently |
827
+ | `LOGGER_DESTINATION_INIT_FAILED` | Structured error log | `destination.onInit()` rejects | Sink is excluded from shutdown; boot continues, writes stay fail-soft |
828
+ | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
829
+ | `LOGGER_CONTEXT_OUT_OF_SCOPE` | Thrown `Error` | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
830
+ | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | A serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
831
+
832
+ > [!NOTE]
833
+ > 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.
677
834
 
678
835
  ---
679
836
 
@@ -690,35 +847,6 @@ interface ILogDestination {
690
847
 
691
848
  ---
692
849
 
693
- ## 🧪 Testing & Quality
694
-
695
- ```bash
696
- pnpm test # unit tests (Jest)
697
- pnpm test:cov # coverage report
698
- pnpm test:e2e # end-to-end tests (supertest)
699
- pnpm test:cov:all # full coverage gate (100% statements/branches/functions/lines)
700
- pnpm mutation # Stryker mutation testing (95% break gate)
701
- pnpm typecheck # tsc strict check (all tsconfig variants)
702
- pnpm lint # ESLint
703
- ```
704
-
705
- Coverage gate: **100%** on statements, branches, functions, and lines. Mutation: **97.42%** current vs the **95%** break gate (Stryker `thresholds.break: 95`); 97.42% is the theoretical maximum — all 10 surviving mutants are documented equivalents. See [mutation_testing_results.md](docs/mutation_testing_results.md).
706
-
707
- ---
708
-
709
- ## 🧱 Tech Stack
710
-
711
- [![Pino](https://img.shields.io/badge/Pino-10-green?style=flat-square)](https://getpino.io)
712
- [![NestJS](https://img.shields.io/badge/NestJS-11-E0234E?style=flat-square&logo=nestjs)](https://nestjs.com)
713
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
714
- [![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
715
- [![Jest](https://img.shields.io/badge/Jest-30-C21325?style=flat-square&logo=jest)](https://jestjs.io)
716
- [![Stryker](https://img.shields.io/badge/Stryker-Mutation_Testing-red?style=flat-square)](https://stryker-mutator.io)
717
- [![pnpm](https://img.shields.io/badge/pnpm-10-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
718
- [![tsup](https://img.shields.io/badge/tsup-8-orange?style=flat-square)](https://tsup.egoist.dev)
719
-
720
- ---
721
-
722
850
  ## ⚡ Performance
723
851
 
724
852
  Pino official benchmarks (Node 24, 100k logs):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
@@ -22,14 +22,35 @@
22
22
  ],
23
23
  "exports": {
24
24
  ".": {
25
- "types": "./dist/server/index.d.ts",
26
- "import": "./dist/server/index.mjs",
27
- "require": "./dist/server/index.cjs"
25
+ "import": {
26
+ "types": "./dist/server/index.d.ts",
27
+ "default": "./dist/server/index.mjs"
28
+ },
29
+ "require": {
30
+ "types": "./dist/server/index.d.cts",
31
+ "default": "./dist/server/index.cjs"
32
+ }
28
33
  },
29
34
  "./shared": {
30
- "types": "./dist/shared/index.d.ts",
31
- "import": "./dist/shared/index.mjs",
32
- "require": "./dist/shared/index.cjs"
35
+ "import": {
36
+ "types": "./dist/shared/index.d.ts",
37
+ "default": "./dist/shared/index.mjs"
38
+ },
39
+ "require": {
40
+ "types": "./dist/shared/index.d.cts",
41
+ "default": "./dist/shared/index.cjs"
42
+ }
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "main": "./dist/server/index.cjs",
47
+ "module": "./dist/server/index.mjs",
48
+ "types": "./dist/server/index.d.cts",
49
+ "typesVersions": {
50
+ "*": {
51
+ "shared": [
52
+ "./dist/shared/index.d.cts"
53
+ ]
33
54
  }
34
55
  },
35
56
  "lint-staged": {
@@ -45,21 +66,26 @@
45
66
  "peerDependencies": {
46
67
  "@nestjs/common": "^11.0.0",
47
68
  "@nestjs/core": "^11.0.0",
69
+ "@opentelemetry/api": "^1.9.0",
70
+ "@types/express": "^4.17.21 || ^5.0.0",
48
71
  "pino": "^10.0.0",
49
- "reflect-metadata": "^0.2.0",
50
- "rxjs": "^7.8.0",
51
72
  "pino-pretty": "^13.0.0",
52
- "@opentelemetry/api": "^1.9.0"
73
+ "reflect-metadata": "^0.2.0",
74
+ "rxjs": "^7.8.0"
53
75
  },
54
76
  "peerDependenciesMeta": {
55
- "pino-pretty": {
77
+ "@opentelemetry/api": {
56
78
  "optional": true
57
79
  },
58
- "@opentelemetry/api": {
80
+ "@types/express": {
81
+ "optional": true
82
+ },
83
+ "pino-pretty": {
59
84
  "optional": true
60
85
  }
61
86
  },
62
87
  "devDependencies": {
88
+ "@arethetypeswrong/cli": "^0.18.5",
63
89
  "@commitlint/cli": "^21.0.2",
64
90
  "@commitlint/config-conventional": "^21.0.2",
65
91
  "@eslint/js": "^9.39.4",
@@ -135,6 +161,7 @@
135
161
  "bench": "node --expose-gc --import tsx bench/throughput.bench.ts",
136
162
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json",
137
163
  "size": "node scripts/check-size.mjs",
164
+ "check:exports": "attw --pack .",
138
165
  "clean": "rm -rf dist coverage",
139
166
  "release": "pnpm publish --provenance"
140
167
  }