@bymax-one/nest-logger 1.1.0 → 1.2.0

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.
@@ -174,8 +174,23 @@ interface BymaxLoggerModuleOptions {
174
174
  * `BymaxLoggerModule.useNestLogger(app)` in `main.ts` instead. Default: true.
175
175
  */
176
176
  shouldUseAsNestLogger?: boolean;
177
- /** Additional Pino redact paths — merged with `DEFAULT_REDACT_PATHS`. */
177
+ /**
178
+ * Additional `fast-redact` paths, applied on top of the default coverage
179
+ * whatever `redactStrategy` is in force.
180
+ */
178
181
  redactPaths?: readonly string[];
182
+ /**
183
+ * Engine backing the DEFAULT redaction set (never the consumer's own
184
+ * `redactPaths`, which are always `fast-redact` paths).
185
+ *
186
+ * - `'names'` (default) — one recursive walk censoring any value whose KEY
187
+ * NAME is in `REDACT_COMMON_FIELDS`, at ANY depth. ~943 k logs/s.
188
+ * - `'paths'` — the pre-1.2 engine: `DEFAULT_REDACT_PATHS` handed to
189
+ * `fast-redact`. Matches only wildcard depths 1–4 and measures ~9.3 k
190
+ * logs/s. Provided as an escape hatch for a consumer depending on exact
191
+ * path-matching semantics; expect it to be removed in a future major.
192
+ */
193
+ redactStrategy?: 'names' | 'paths';
179
194
  /** Censor string written in place of redacted values. Default: `[REDACTED]`. */
180
195
  redactCensor?: string;
181
196
  /** Disable default redact paths (use with caution). Default: false. */
@@ -328,6 +343,127 @@ declare class BymaxLoggerModule extends BymaxLoggerModuleBase {
328
343
  static useNestLogger(app: INestApplication): void;
329
344
  }
330
345
 
346
+ /**
347
+ * Log keys reserved by `@bymax-one/nest-logger` for its own structured events.
348
+ *
349
+ * Consumer apps SHOULD NOT use these names for application-level events
350
+ * to avoid collision in log aggregation queries (Datadog, Loki, Elastic).
351
+ *
352
+ * Every key here is emitted by some code path, EXCEPT the ones listed in
353
+ * {@link RESERVED_LOG_KEYS_NOT_EMITTED}, which are reserved-only and carry the
354
+ * reason they stay that way. `reserved-log-keys.constants.spec.ts` enforces the
355
+ * split, so a key can never again be declared, documented as a signal, and then
356
+ * silently never written.
357
+ */
358
+ declare const RESERVED_LOG_KEYS: Readonly<{
359
+ readonly LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK";
360
+ readonly LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING";
361
+ readonly LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK";
362
+ readonly HTTP_REQUEST_START: "HTTP_REQUEST_START";
363
+ readonly HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS";
364
+ readonly HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT";
365
+ readonly HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR";
366
+ readonly HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR";
367
+ readonly HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED";
368
+ readonly HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED";
369
+ readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
370
+ readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
371
+ readonly METHOD_EXECUTION: "METHOD_EXECUTION";
372
+ readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
373
+ readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
374
+ readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
375
+ readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
376
+ readonly LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED";
377
+ }>;
378
+ /**
379
+ * Keys that are reserved but intentionally never emitted, each with the reason.
380
+ *
381
+ * A name in this map is claimed so no application event can take it, but the
382
+ * library commits to not writing it. Anything NOT listed here must be reachable
383
+ * from some code path — the spec asserts both directions.
384
+ */
385
+ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
386
+ /**
387
+ * Superseded before it was ever used. The request lifecycle terminates in one
388
+ * of `HTTP_REQUEST_SUCCESS` / `_REDIRECT` / `_CLIENT_ERROR` / `_SERVER_ERROR`,
389
+ * each carrying the same `duration` a generic "completed" entry would. Emitting
390
+ * it as well would double the terminal access-log volume to say nothing new.
391
+ * It stays claimed so an application cannot repurpose a name that reads like a
392
+ * library event.
393
+ */
394
+ readonly HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys";
395
+ }>;
396
+
397
+ /**
398
+ * Name-based recursive redaction.
399
+ *
400
+ * Layer: server/utils — the engine behind the library's DEFAULT PII protection.
401
+ * It walks a log record once and censors every value whose KEY NAME is in the
402
+ * sensitive set, to a bounded depth (see {@link REDACT_MAX_TRAVERSAL_DEPTH}).
403
+ *
404
+ * Why this replaced path matching. The previous default compiled 140
405
+ * `fast-redact` paths, 108 of them multi-level wildcards (`*.password`,
406
+ * `*.*.password`, …). `fast-redact` walks every key at every listed level on
407
+ * every log call, so the cost grew with the PATH COUNT, not with the payload:
408
+ * measured at ~107 µs per entry (≈9.3 k logs/s) against ~0.9 µs with redaction
409
+ * off. A single traversal is O(nodes) — the same order Pino's serializer already
410
+ * pays. The shipped configuration measures 462 k logs/s against 9.3 k, for
411
+ * byte-identical output on every payload the old paths covered.
412
+ *
413
+ * It is also STRICTLY SAFER than the paths it replaces:
414
+ * - depth 100 instead of four, and past it the value is DROPPED rather than
415
+ * passed through, so the ceiling cannot become a leak the way the old one
416
+ * was (anything below the wildcard list's fourth level was emitted in clear);
417
+ * - a sensitive key is caught wherever it appears, so a headers bag logged as
418
+ * `{ headers: { authorization } }` is covered, not only the exact
419
+ * `req.headers.authorization` shape the absolute paths pinned.
420
+ *
421
+ * Three value kinds need special handling, and getting any of them wrong is a
422
+ * leak rather than a cosmetic issue:
423
+ * - `Error` — copied through its prototype and descriptors, so it stays an
424
+ * `Error` (Pino's `err` serializer keys off the instance, and `message` /
425
+ * `stack` are own but NON-enumerable). Skipping errors entirely left an
426
+ * error under any key WITHOUT a serializer serializing its own enumerable
427
+ * properties in clear.
428
+ * - anything carrying `toJSON` (`Date`, `Decimal`, Luxon, Prisma types) — the
429
+ * method decides the serialized form, so the walk inspects its OUTPUT.
430
+ * Skipping these let a `toJSON` SYNTHESIZE a secret straight past both hooks.
431
+ * - `ArrayBuffer` views (`Buffer`, typed arrays) — indexed byte containers,
432
+ * returned untouched.
433
+ *
434
+ * What this file cannot reach: bindings passed to `child()`. Pino pre-serializes
435
+ * them into the instance's `chindings` fragment before any formatter runs, so
436
+ * `PinoLoggerService.child()` applies this redactor itself.
437
+ *
438
+ * Contract: SNAPSHOT and NEVER-THROW. Nothing the caller passed is mutated; the
439
+ * result is a fresh structure built from values read exactly ONCE. That is a
440
+ * deliberate trade against an earlier copy-on-write design which returned a
441
+ * clean subtree by reference: doing so left every accessor in it to be evaluated
442
+ * a SECOND time by `JSON.stringify`, and a stateful getter (or `toJSON`) can
443
+ * answer clean to the walk and `{ password }` to the serializer. Inspection
444
+ * cannot close that window — only reading once and pinning the result can. It
445
+ * costs ~26 % of the entry throughput, measured, and the walk is still ~36x
446
+ * faster than the path expansion it replaced.
447
+ *
448
+ * Any internal failure — including a censor that cannot be written — degrades the
449
+ * whole record to a marked, secret-free envelope rather than crashing the request
450
+ * that produced it.
451
+ *
452
+ * @see {@link createNameRedactor}
453
+ */
454
+
455
+ /**
456
+ * A configured redactor.
457
+ *
458
+ * @param value - The value to redact.
459
+ * @param isRecordRoot - `true` when `value` is the ROOT of a log record — the
460
+ * object Pino iterates rather than serializes. A root must not honour a
461
+ * `toJSON` method, because doing so would replace the entire record with that
462
+ * method's return value; nested values and serializer outputs do reach
463
+ * `JSON.stringify` and keep it.
464
+ */
465
+ type Redactor = (value: unknown, isRecordRoot?: boolean) => unknown;
466
+
331
467
  /**
332
468
  * Pino-backed implementation of the NestJS `LoggerService` contract plus the
333
469
  * structured `MODULE_ACTION_RESULT` API.
@@ -346,8 +482,15 @@ declare class BymaxLoggerModule extends BymaxLoggerModuleBase {
346
482
  */
347
483
  declare class PinoLoggerService implements LoggerService, OnApplicationShutdown {
348
484
  private readonly pino;
485
+ private readonly redact;
349
486
  private context?;
350
- constructor(pino: Logger);
487
+ /**
488
+ * @param pino - The wrapped Pino instance.
489
+ * @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
490
+ * Defaults to the identity so a hand-built instance (benchmarks, unit tests)
491
+ * still works; the module always injects the configured one.
492
+ */
493
+ constructor(pino: Logger, redact?: Redactor);
351
494
  /**
352
495
  * NestJS `log` (info level). Last string parameter is treated as the context.
353
496
  *
@@ -701,6 +844,21 @@ declare class HttpLoggingInterceptor implements NestInterceptor {
701
844
  * @returns The (unmodified) response stream.
702
845
  */
703
846
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
847
+ /**
848
+ * Hand the lifecycle to the access-log middleware, recording only the thrown
849
+ * error for its terminal entry.
850
+ *
851
+ * The interceptor cannot be the recorder: it never runs for a request a guard
852
+ * rejected or one that matched no route. It also cannot judge DELIVERY — at the
853
+ * moment it completes, the response has not been flushed yet, which is why an
854
+ * aborted request used to be logged as the 200 the handler intended. What it
855
+ * uniquely has is the thrown value, so that is all it contributes.
856
+ *
857
+ * @param req - The request whose lifecycle the middleware owns.
858
+ * @param next - The downstream call handler.
859
+ * @returns The response stream, unmodified and with exceptions re-thrown.
860
+ */
861
+ private recordOnly;
704
862
  /**
705
863
  * Log a non-throwing completion: SUCCESS for 2xx, REDIRECT for 3xx, nothing
706
864
  * for any other status (those are surfaced via the error path instead).
@@ -731,8 +889,12 @@ declare class HttpLoggingInterceptor implements NestInterceptor {
731
889
  *
732
890
  * Layer: server/middlewares — NestJS requires middleware to be wired in a
733
891
  * module's `configure(consumer)` hook, which the library cannot do on the
734
- * consumer's behalf. This helper encapsulates the one-liner so consumers do not
735
- * import the middleware class directly.
892
+ * consumer's behalf. This helper encapsulates the wiring so consumers do not
893
+ * import the middleware classes directly.
894
+ *
895
+ * It registers TWO middlewares, in order: the correlation scope, then the access
896
+ * log that runs inside it. Both run before any guard, which is the whole point —
897
+ * an interceptor never observes a request a guard rejected.
736
898
  */
737
899
 
738
900
  /**
@@ -791,6 +953,18 @@ interface LoggableRequest {
791
953
  readonly method: string;
792
954
  /** Request target, normalized and query-stripped before it reaches a log. */
793
955
  readonly url: string;
956
+ /**
957
+ * The request target as it arrived, before any mount point was stripped.
958
+ *
959
+ * Middleware mounted at a path sees `url` RELATIVE to that mount: measured
960
+ * under `setGlobalPrefix('api')`, a request for `/api/users/7` reaches a
961
+ * root-mounted middleware as `url = '/users/7'` and
962
+ * `originalUrl = '/api/users/7'`, and `/api` itself arrives as `url = '/'`.
963
+ * Logging `url` there would drop the prefix from every entry and make an
964
+ * `excludePaths` pattern written against the real path stop matching. Absent
965
+ * on adapters that do not provide it, in which case `url` is already whole.
966
+ */
967
+ readonly originalUrl?: string | undefined;
794
968
  /** Client address when the adapter resolves one. */
795
969
  readonly ip?: string | undefined;
796
970
  /**
@@ -811,12 +985,26 @@ interface LoggableRequest {
811
985
  interface LoggableResponse {
812
986
  /** Status code, read when logging the completed request. */
813
987
  readonly statusCode: number;
988
+ /**
989
+ * `true` once every byte has been flushed to the socket. Read in the `'close'`
990
+ * handler to tell a delivered response from one the client never received:
991
+ * `'close'` fires for both, and `'finish'` never fires for an aborted request
992
+ * at all. It is what keeps an aborted request from being logged as the success
993
+ * the handler intended.
994
+ */
995
+ readonly writableFinished: boolean;
814
996
  /** Sets a response header (used for the correlation id). */
815
997
  setHeader(name: string, value: string): void;
816
998
  /** Selects the status code and returns the chainable body writer. */
817
999
  status(code: number): {
818
1000
  json(body: unknown): unknown;
819
1001
  };
1002
+ /**
1003
+ * Registers a lifecycle listener. Only `'close'` is used: it fires exactly
1004
+ * once whether the response completed or the connection died early, where
1005
+ * `'finish'` is blind to the aborted case.
1006
+ */
1007
+ on(event: 'close', listener: () => void): unknown;
820
1008
  }
821
1009
  /** Continuation passed to a middleware; invoked with no arguments here. */
822
1010
  type NextHandler = () => void;
@@ -928,9 +1116,12 @@ declare const LOGGER_DESTINATIONS_TOKEN: unique symbol;
928
1116
  declare const LOG_CONTEXT_TOKEN: unique symbol;
929
1117
 
930
1118
  /**
931
- * Canonical list of redact paths merged into Pino's `redact.paths` option by
932
- * default. Expected length: 27 root fields + 27 common fields × 4 depths + 5
933
- * absolute = **140**.
1119
+ * The legacy `fast-redact` path expansion of {@link REDACT_COMMON_FIELDS}:
1120
+ * every name at the record root plus wildcard depths 1–4, followed by the
1121
+ * absolute header paths. Length is derived, not fixed — `fields × 5 + absolute`.
1122
+ *
1123
+ * Fed to Pino only under the opt-in `redactStrategy: 'paths'`. Kept exported at
1124
+ * full fidelity because it is public API and append-only by contract.
934
1125
  *
935
1126
  * The bare field names come first, at depth 0 — the log record's own root.
936
1127
  * `emitStructured` spreads caller metadata there (`{ ...metadata, logKey, ... }`),
@@ -942,18 +1133,48 @@ declare const LOG_CONTEXT_TOKEN: unique symbol;
942
1133
  */
943
1134
  declare const DEFAULT_REDACT_PATHS: readonly string[];
944
1135
 
1136
+ /**
1137
+ * Pino numeric level mapping. Frozen at runtime so destinations and the
1138
+ * NestJS bridge can rely on a stable wire-format contract.
1139
+ *
1140
+ * @see {@link https://github.com/pinojs/pino/blob/main/docs/api.md#level-string}
1141
+ */
1142
+ declare const PINO_LEVEL_NUMBERS: Record<LogLevel, number>;
1143
+ /**
1144
+ * Reverse lookup — numeric Pino level → string. Derived from
1145
+ * {@link PINO_LEVEL_NUMBERS} so the two cannot drift, then frozen so it is
1146
+ * read-only at runtime.
1147
+ */
1148
+ declare const PINO_LEVEL_NAMES: Record<number, LogLevel>;
1149
+
945
1150
  /**
946
1151
  * Shape of a single log entry as serialized to JSON output.
947
1152
  *
948
- * Used by destinations and downstream parsers (Datadog, Loki, Elastic).
1153
+ * Used by destinations and downstream parsers (Loki, Elastic, Datadog).
949
1154
  * The trailing index signature `[key: string]: unknown` covers arbitrary
950
1155
  * structured fields injected by application logs.
1156
+ *
1157
+ * `level` and `time` describe what the library ACTUALLY writes. Before `1.2.0`
1158
+ * they were declared `number`, which the runtime never produced: the Pino
1159
+ * instance is built with `formatters.level` returning the string label and a
1160
+ * `timestamp` function emitting an ISO 8601 string. A destination written
1161
+ * against the old declaration type-checked and then failed at runtime — the
1162
+ * README's own Loki example called `BigInt(entry.time)` on an ISO string, which
1163
+ * throws. See `log-entry-contract.e2e-spec.ts`, which asserts the declaration
1164
+ * against a real serialized record so the two can never drift again.
951
1165
  */
952
1166
  interface LogEntry {
953
- /** Pino numeric level (30 = info, 50 = error, etc.). */
954
- level: number;
955
- /** ISO 8601 UTC timestamp string OR epoch milliseconds (numeric). */
956
- time: string | number;
1167
+ /**
1168
+ * Severity as the Pino string label (`'info'`, `'error'`, …) — NOT the numeric
1169
+ * code. Convert with `PINO_LEVEL_NUMBERS` when a numeric column is needed.
1170
+ */
1171
+ level: LogLevel;
1172
+ /**
1173
+ * ISO 8601 UTC timestamp string, e.g. `'2026-08-12T09:25:46.520Z'`. Parse with
1174
+ * `Date.parse(entry.time)` for epoch milliseconds. A consumer-supplied
1175
+ * `timestamp` option may change the format, but never the type.
1176
+ */
1177
+ time: string;
957
1178
  /** Human-readable message. */
958
1179
  msg: string;
959
1180
  /** Convention key — `MODULE_ACTION_RESULT` format. */
@@ -1002,29 +1223,4 @@ interface LogEntry {
1002
1223
  */
1003
1224
  declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
1004
1225
 
1005
- /**
1006
- * Log keys reserved by `@bymax-one/nest-logger` for its own structured events.
1007
- *
1008
- * Consumer apps SHOULD NOT use these names for application-level events
1009
- * to avoid collision in log aggregation queries (Datadog, Loki, Elastic).
1010
- */
1011
- declare const RESERVED_LOG_KEYS: Readonly<{
1012
- readonly LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK";
1013
- readonly LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING";
1014
- readonly LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK";
1015
- readonly HTTP_REQUEST_START: "HTTP_REQUEST_START";
1016
- readonly HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS";
1017
- readonly HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT";
1018
- readonly HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR";
1019
- readonly HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR";
1020
- readonly HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED";
1021
- readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
1022
- readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
1023
- readonly METHOD_EXECUTION: "METHOD_EXECUTION";
1024
- readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
1025
- readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
1026
- readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
1027
- readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
1028
- }>;
1029
-
1030
- export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RequestIdMiddleware, type ResolvedBymaxLoggerModuleOptions, type ServiceMetadata, applyRequestIdMiddleware };
1226
+ export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IncomingHeaders, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, type LogContext$1 as LogContextBag, LogContextService, type LogEntry, type LogLevel, LogPerformance, type LoggableRequest, type LoggableResponse, type NextHandler, type OtelOptions, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ResolvedBymaxLoggerModuleOptions, type ServiceMetadata, applyRequestIdMiddleware };