@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.
- package/CHANGELOG.md +55 -1
- package/README.md +263 -135
- 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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
- **
|
|
49
|
-
-
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
124
|
+
# Using pnpm (recommended)
|
|
78
125
|
pnpm add @bymax-one/nest-logger
|
|
79
126
|
|
|
80
|
-
#
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
176
|
+
Stdout is wired automatically — `DefaultStdoutDestination` is always active, so this is all you need to be logging structured JSON.
|
|
114
177
|
|
|
115
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
304
|
-
| ---------------------------- |
|
|
305
|
-
| `service.name` | `string`
|
|
306
|
-
| `service.version` | `string`
|
|
307
|
-
| `level` | `LogLevel`
|
|
308
|
-
| `isPretty` | `boolean`
|
|
309
|
-
| `redactPaths` | `string[]`
|
|
310
|
-
| `shouldDisableDefaultRedact` | `boolean`
|
|
311
|
-
| `redactCensor` | `string
|
|
312
|
-
| `maxEntrySizeBytes` | `number`
|
|
313
|
-
| `destinations` | `ILogDestination[]`
|
|
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
|
-
|
|
605
|
+
### Design Principles
|
|
591
606
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
+
[](https://getpino.io)
|
|
719
|
+
[](https://nestjs.com)
|
|
720
|
+
[](https://www.typescriptlang.org)
|
|
721
|
+
[](https://nodejs.org)
|
|
722
|
+
[](https://jestjs.io)
|
|
723
|
+
[](https://stryker-mutator.io)
|
|
724
|
+
[](https://pnpm.io)
|
|
725
|
+
[](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 |
|
|
668
|
-
| --------------------------------- |
|
|
669
|
-
| `LOGGER_INVALID_OPTIONS` |
|
|
670
|
-
| `LOGGER_INVALID_LEVEL` |
|
|
671
|
-
| `LOGGER_PRETTY_UNAVAILABLE` |
|
|
672
|
-
| `LOGGER_OTEL_API_UNAVAILABLE` |
|
|
673
|
-
| `LOGGER_DESTINATION_INIT_FAILED` |
|
|
674
|
-
| `LOGGER_DESTINATION_WRITE_FAILED` |
|
|
675
|
-
| `LOGGER_CONTEXT_OUT_OF_SCOPE` |
|
|
676
|
-
| `LOGGER_ENTRY_TRUNCATED` |
|
|
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
|
-
[](https://getpino.io)
|
|
712
|
-
[](https://nestjs.com)
|
|
713
|
-
[](https://www.typescriptlang.org)
|
|
714
|
-
[](https://nodejs.org)
|
|
715
|
-
[](https://jestjs.io)
|
|
716
|
-
[](https://stryker-mutator.io)
|
|
717
|
-
[](https://pnpm.io)
|
|
718
|
-
[](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.
|
|
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
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
"
|
|
73
|
+
"reflect-metadata": "^0.2.0",
|
|
74
|
+
"rxjs": "^7.8.0"
|
|
53
75
|
},
|
|
54
76
|
"peerDependenciesMeta": {
|
|
55
|
-
"
|
|
77
|
+
"@opentelemetry/api": {
|
|
56
78
|
"optional": true
|
|
57
79
|
},
|
|
58
|
-
"@
|
|
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
|
}
|