@bymax-one/nest-logger 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,92 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.4.2] - 2026-10-30
15
+
16
+ No breaking changes. Dependency and build-tooling maintenance only. No source file under `src/` changes and the
17
+ runtime JavaScript in `dist/` is byte-identical to `1.4.1`.
18
+
19
+ ### Security
20
+
21
+ - Raise the pnpm override floors for transitive devDependencies that resolved to versions
22
+ with new advisories: `body-parser` `^2.4.0` (GHSA-27r3-jxmh-p828, DoS on trailing bytes after a
23
+ compressed body), `multer` `^2.5.0` (GHSA-q7qp-rjg7-q26m, DoS from a pending async
24
+ `fileFilter`), `qs` `^6.16.1` (GHSA-6pq8-jhm2-2vcw, `qs.stringify` DoS) and `zod` `^4.6.6`
25
+ (GHSA-mrhx-c7xc-m3w5, memory exhaustion on oversized collections). Dev-only: the published
26
+ package has zero runtime dependencies.
27
+ - Ignore GHSA-gcqx-w48m-w2h4 (`deepmerge` <= 4.3.1, prototype poisoning in `mergeObject`) in
28
+ `osv-scanner.toml` until 2027-01-27. No patched release exists, and `deepmerge` is reached
29
+ only through `jest-config`, a devDependency. Remove the entry when a fixed `deepmerge` ships
30
+ or jest drops it.
31
+
32
+ ### Changed
33
+
34
+ - Build with `tsup` 9 (devDependency, was 8.5). The published `.mjs` and `.cjs` bundles are
35
+ byte-identical; the `.d.ts` and `.d.cts` files re-emit type exports as a separate
36
+ `export type { … }` statement instead of inline `type` modifiers, which declares the same
37
+ public surface. `attw` reports no problems and the bundle-size budgets hold.
38
+ - Resolve `@opentelemetry/api` to 1.10.0 in the lockfile (was 1.9.1), so the test suite runs
39
+ against the current API release. The `^1.9.0` range in `package.json`, devDependency and peer
40
+ alike, is unchanged.
41
+ - Bump `github/codeql-action/upload-sarif` to 4.39.1 in the Scorecard workflow. CI only; nothing
42
+ ships.
43
+
44
+ ### Documentation
45
+
46
+ - The runtime bundles are unchanged, and the type declarations were regenerated by `tsup` 9.
47
+ - The 1.0.1 and 1.0.8 release notes are corrected: 1.0.8 now records the id-safe charset
48
+ validation of inbound `x-request-id` and tenant ids that it already shipped.
49
+
50
+ #### Apply to a consumer
51
+
52
+ No action required. There is no new public API, no peer range changed, and the runtime JavaScript
53
+ is byte-identical to `1.4.1`; only the grouping of type exports in the declaration files differs.
54
+
55
+ ## [1.4.1] - 2026-10-23
56
+
57
+ No breaking changes.
58
+
59
+ ### Added
60
+
61
+ - **`PinoUnexpectedErrorSink` — the cause of a nest-core `500`, logged.** When nest-core's exception
62
+ filter turns an unexpected error into `500 BYMAX_INTERNAL_ERROR`, the access-log line carries the
63
+ cause only when this package's HTTP interceptor saw the error. An error raised before it (in a
64
+ guard or a middleware), or any error with HTTP logging off, left a synthetic
65
+ `Error("GET /url → 500")` or no line at all. nest-core 1.6.2 adds an
66
+ optional `BYMAX_UNEXPECTED_ERROR_SINK` token. This class implements it: one `error` entry per
67
+ unexpected error, keyed `HTTP_UNEXPECTED_ERROR`, with the original error's type, message, stack
68
+ and cause chain, plus `method`, the route template (`route`, never the raw URL) and
69
+ `correlationId`. It adds nothing else from the request — no body, headers or query string — and
70
+ `record()` never throws. The error keeps its own enumerable fields, as on every `err` entry, so
71
+ request data an error object carries itself is logged under the usual key-name redaction.
72
+
73
+ This package still does not depend on nest-core. The class matches nest-core's contract by
74
+ shape, and the types it implements are exported as `IUnexpectedErrorSink` and
75
+ `UnexpectedErrorReport`.
76
+
77
+ `HTTP_UNEXPECTED_ERROR` joins `RESERVED_LOG_KEYS`. An application that already writes that key
78
+ for its own events should rename them, or its queries will mix the two.
79
+
80
+ #### Apply to a derived backend
81
+
82
+ Requires `@bymax-one/nest-core` 1.6.2 or later. Nothing changes until you bind the sink: the token
83
+ is optional and unbound by default. Bind it from a `@Global()` module imported next to
84
+ `BymaxLoggerModule`:
85
+
86
+ ```typescript
87
+ @Global()
88
+ @Module({
89
+ providers: [{ provide: BYMAX_UNEXPECTED_ERROR_SINK, useClass: PinoUnexpectedErrorSink }],
90
+ exports: [BYMAX_UNEXPECTED_ERROR_SINK]
91
+ })
92
+ export class UnexpectedErrorSinkModule {}
93
+ ```
94
+
95
+ `BYMAX_UNEXPECTED_ERROR_SINK` comes from `@bymax-one/nest-core` and `PinoUnexpectedErrorSink` from
96
+ `@bymax-one/nest-logger`. To verify, make a route throw a plain `Error`: the response stays
97
+ `500 BYMAX_INTERNAL_ERROR`, and the log gains one `HTTP_UNEXPECTED_ERROR` line next to the
98
+ access-log line.
99
+
14
100
  ## [1.4.0] - 2026-08-30
15
101
 
16
102
  No breaking changes. Nothing you have wired stops working, and no export was renamed.
@@ -1654,8 +1740,9 @@ identical to `1.0.8`, and nothing changed but the version number.
1654
1740
 
1655
1741
  ## [1.0.8] - 2026-08-10
1656
1742
 
1657
- Remediation of a local audit's redaction, attribution and correlation-id findings. No API
1658
- changed; the fixes are in the default redact set, the acting-user resolution and the docs.
1743
+ Remediation of a local audit's redaction, attribution and correlation-id findings. The public
1744
+ API is unchanged; the fixes are in the default redact set, the acting-user resolution, the
1745
+ correlation-id validation and the docs.
1659
1746
 
1660
1747
  ### Fixed
1661
1748
 
@@ -1667,6 +1754,14 @@ changed; the fixes are in the default redact set, the acting-user resolution and
1667
1754
  exception filter now resolve the acting user as `sub ?? id`, so a JWT principal (every
1668
1755
  `@bymax-one/nest-auth` token) is attributed to its subject rather than an ORM `id` when a
1669
1756
  principal carries both.
1757
+ - **Inbound correlation and tenant ids are confined to an id-safe charset.** The inbound
1758
+ `x-request-id` and tenant-id header values are now accepted only when they are drawn from an
1759
+ id-safe character set and fit the length bound; anything else (control bytes, whitespace,
1760
+ markup characters) is rejected. A rejected request id is replaced by a generated one (when
1761
+ `shouldGenerateRequestId` is on) and a rejected tenant id is omitted. The request id is echoed
1762
+ on the `x-request-id` response header and both ids are stored in the log context, so this keeps
1763
+ a client from planting a control byte or markup fragment in the logs and the header of every
1764
+ request it makes.
1670
1765
 
1671
1766
  ### Documentation
1672
1767
 
@@ -1911,11 +2006,11 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
1911
2006
  Dev-only: the published package has zero runtime dependencies.
1912
2007
  - Force `qs` to `^6.15.3` (via `pnpm.overrides`) to clear GHSA-q8mj-m7cp-5q26
1913
2008
  (`qs.stringify` DoS). `typed-rest-client`, pulled in by Stryker, pins `qs` at
1914
- the affected `6.15.1` exactly, so no dependency refresh could resolve it
2009
+ the affected `6.15.1` exactly, so no dependency refresh could resolve it.
1915
2010
  - Pin every third-party and GitHub-owned action to a full commit SHA, with the
1916
2011
  release tag kept as a trailing comment so Dependabot can still bump them. The
1917
2012
  org's own `bymaxone/.github` reusable workflows and composite action stay on
1918
- the `@v1` moving tag by design, so shared-pipeline fixes keep propagating
2013
+ the `@v1` moving tag by design, so shared-pipeline fixes keep propagating.
1919
2014
 
1920
2015
  ### Changed
1921
2016
 
@@ -1981,7 +2076,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
1981
2076
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
1982
2077
  `release.yml`, Dependabot, and issue templates
1983
2078
 
1984
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.0...HEAD
2079
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.2...HEAD
2080
+ [1.4.2]: https://github.com/bymaxone/nest-logger/compare/v1.4.1...v1.4.2
2081
+ [1.4.1]: https://github.com/bymaxone/nest-logger/compare/v1.4.0...v1.4.1
1985
2082
  [1.4.0]: https://github.com/bymaxone/nest-logger/compare/v1.3.1...v1.4.0
1986
2083
  [1.3.1]: https://github.com/bymaxone/nest-logger/compare/v1.3.0...v1.3.1
1987
2084
  [1.3.0]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...v1.3.0
package/README.md CHANGED
@@ -411,6 +411,48 @@ flushed** is reported as the success it was — the server completed and flushed
411
411
  peer read it is not knowable from the server side. What this catches is the case that matters
412
412
  operationally: the slow upstream, the load-balancer timeout, the cancelled request.
413
413
 
414
+ #### Unexpected errors behind `@bymax-one/nest-core`
415
+
416
+ When nest-core's exception filter answers, an unexpected error becomes a fixed
417
+ `500 BYMAX_INTERNAL_ERROR`. The access-log line for that response carries the cause only when this
418
+ package's HTTP interceptor saw the error, which it does not for an error raised before it (in a
419
+ guard or a middleware) or with HTTP logging off. nest-core 1.6.2 adds an optional sink token, `BYMAX_UNEXPECTED_ERROR_SINK`, and
420
+ `PinoUnexpectedErrorSink` is a ready implementation of it. Bind it from a global module:
421
+
422
+ ```typescript
423
+ import { Global, Module } from '@nestjs/common'
424
+ import { BYMAX_UNEXPECTED_ERROR_SINK } from '@bymax-one/nest-core'
425
+ import { PinoUnexpectedErrorSink } from '@bymax-one/nest-logger'
426
+
427
+ @Global()
428
+ @Module({
429
+ providers: [{ provide: BYMAX_UNEXPECTED_ERROR_SINK, useClass: PinoUnexpectedErrorSink }],
430
+ exports: [BYMAX_UNEXPECTED_ERROR_SINK]
431
+ })
432
+ export class UnexpectedErrorSinkModule {}
433
+ ```
434
+
435
+ Import it next to `BymaxLoggerModule`. The binding resolves `PinoLoggerService` because
436
+ `BymaxLoggerModule` is global by default; with `isGlobal: false`, add the same `forRoot`/`forRootAsync` result the
437
+ application imports to the sink module's `imports`. Keep it in a constant: calling `forRoot` a
438
+ second time builds a second logger. Each unexpected error then produces one `error` entry with the key
439
+ `HTTP_UNEXPECTED_ERROR`:
440
+
441
+ | Field | Content |
442
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
443
+ | `err` | The original error through the `err` serializer: type, message, scrubbed stack, cause chain, own fields |
444
+ | `method` | HTTP method |
445
+ | `route` | Matched route template (`/invoices/:id`), or `<unmatched>` before routing — never the raw URL |
446
+ | `correlationId` | nest-core's correlation id, when one resolved |
447
+
448
+ The sink adds nothing from the request beyond `method`, `route` and `correlationId`: no body,
449
+ headers or query string. The error is serialized as on every `err` entry, own enumerable fields
450
+ included, so an error object that itself carries request data (a `url` with a query string, a copy
451
+ of the headers) brings that data along, under the same key-name redaction. The trace id comes from the trace mixin, as on every other entry. `record()`
452
+ never throws, so a failing destination cannot change the response. This package does not import
453
+ nest-core: the class matches nest-core's contract by shape, and `IUnexpectedErrorSink` /
454
+ `UnexpectedErrorReport` are exported for reference.
455
+
414
456
  ### 6. OpenTelemetry correlation
415
457
 
416
458
  Initialize the OTel SDK **before** importing NestJS — this is critical:
@@ -729,7 +771,7 @@ function assertValidLogKey(key: string) {
729
771
 
730
772
  The following keys are used internally by the library — do not reuse them in application code:
731
773
 
732
- `LOGGER_BOOTSTRAP_OK` · `LOGGER_BOOTSTRAP_WARNING` · `LOGGER_SHUTDOWN_OK` · `HTTP_REQUEST_START` · `HTTP_REQUEST_SUCCESS` · `HTTP_REQUEST_REDIRECT` · `HTTP_REQUEST_CLIENT_ERROR` · `HTTP_REQUEST_SERVER_ERROR` · `HTTP_REQUEST_ABORTED` · `HTTP_REQUEST_COMPLETED` · `HTTP_EXCEPTION_HANDLED` · `HTTP_EXCEPTION_UNHANDLED` · `METHOD_EXECUTION` · `METHOD_SLOW_EXECUTION` · `LOGGER_DESTINATION_INIT_FAILED` · `LOGGER_DESTINATION_WRITE_FAILED` · `LOGGER_ENTRY_TRUNCATED` · `LOGGER_REDACTION_FAILED`
774
+ `LOGGER_BOOTSTRAP_OK` · `LOGGER_BOOTSTRAP_WARNING` · `LOGGER_SHUTDOWN_OK` · `HTTP_REQUEST_START` · `HTTP_REQUEST_SUCCESS` · `HTTP_REQUEST_REDIRECT` · `HTTP_REQUEST_CLIENT_ERROR` · `HTTP_REQUEST_SERVER_ERROR` · `HTTP_REQUEST_ABORTED` · `HTTP_REQUEST_COMPLETED` · `HTTP_EXCEPTION_HANDLED` · `HTTP_EXCEPTION_UNHANDLED` · `HTTP_UNEXPECTED_ERROR` · `METHOD_EXECUTION` · `METHOD_SLOW_EXECUTION` · `LOGGER_DESTINATION_INIT_FAILED` · `LOGGER_DESTINATION_WRITE_FAILED` · `LOGGER_ENTRY_TRUNCATED` · `LOGGER_REDACTION_FAILED`
733
775
 
734
776
  All reserved keys are exported as the `RESERVED_LOG_KEYS` constant from `@bymax-one/nest-logger/shared`.
735
777
 
@@ -185,6 +185,7 @@ var RESERVED_LOG_KEYS = Object.freeze({
185
185
  HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
186
186
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
187
187
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
188
+ HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
188
189
  METHOD_EXECUTION: "METHOD_EXECUTION",
189
190
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
190
191
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
@@ -2909,6 +2910,63 @@ var ALL_ROUTES = "/";
2909
2910
  function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
2910
2911
  consumer.apply(exports.RequestIdMiddleware, exports.HttpAccessLogMiddleware).forRoutes(routes);
2911
2912
  }
2913
+ exports.PinoUnexpectedErrorSink = class PinoUnexpectedErrorSink {
2914
+ /**
2915
+ * @param logger - The structured logger the entry is written to. Injected by
2916
+ * token explicitly: the package is bundled without `emitDecoratorMetadata`,
2917
+ * so implicit class-type injection would not resolve.
2918
+ */
2919
+ constructor(logger) {
2920
+ this.logger = logger;
2921
+ }
2922
+ /**
2923
+ * Write one `error` entry for an unexpected error. Never throws.
2924
+ *
2925
+ * The entry carries `err` (name, message, scrubbed stack, cause chain and own
2926
+ * fields, through the configured `err` serializer), `method`, `route` and,
2927
+ * when present, `correlationId`. It deliberately carries nothing else from
2928
+ * the request: the report has no body, headers or query to leak, and this
2929
+ * method adds none.
2930
+ *
2931
+ * `report.traceId` is not written. The trace mixin already stamps the active
2932
+ * span's trace id under the configured `otel.traceIdField` — the same span
2933
+ * nest-core read it from — and writing it here would override that field and
2934
+ * ignore `otel.shouldAutoInjectTraceContext: false`.
2935
+ *
2936
+ * @param report - The error and its request coordinates, from nest-core.
2937
+ * @example
2938
+ * sink.record({ error: new TypeError('x is undefined'), method: 'GET', route: '/invoices/:id', correlationId: 'req-1' })
2939
+ * // → { level: 'error', logKey: 'HTTP_UNEXPECTED_ERROR', msg: 'x is undefined',
2940
+ * // err: { type: 'TypeError', message: 'x is undefined', stack: '…' },
2941
+ * // method: 'GET', route: '/invoices/:id', correlationId: 'req-1' }
2942
+ */
2943
+ record(report) {
2944
+ try {
2945
+ const metadata = { method: report.method, route: report.route };
2946
+ if (report.correlationId !== void 0) {
2947
+ metadata["correlationId"] = report.correlationId;
2948
+ }
2949
+ this.logger.errorStructured(
2950
+ RESERVED_LOG_KEYS.HTTP_UNEXPECTED_ERROR,
2951
+ toLoggableError(report.error),
2952
+ void 0,
2953
+ metadata
2954
+ );
2955
+ } catch {
2956
+ }
2957
+ }
2958
+ };
2959
+ exports.PinoUnexpectedErrorSink = __decorateClass([
2960
+ common.Injectable(),
2961
+ __decorateParam(0, common.Inject(exports.PinoLoggerService))
2962
+ ], exports.PinoUnexpectedErrorSink);
2963
+ function toLoggableError(value) {
2964
+ try {
2965
+ return isErrorLike(value) ? value : sanitizeError(value);
2966
+ } catch {
2967
+ return sanitizeError(value);
2968
+ }
2969
+ }
2912
2970
  var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
2913
2971
  function InjectLogger(context) {
2914
2972
  return (target, propertyKey, parameterIndex) => {
@@ -572,6 +572,7 @@ declare const RESERVED_LOG_KEYS: Readonly<{
572
572
  readonly HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED";
573
573
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
574
574
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
575
+ readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
575
576
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
576
577
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
577
578
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
@@ -1671,6 +1672,84 @@ declare class RequestIdMiddleware implements NestMiddleware {
1671
1672
  private resolveTenantId;
1672
1673
  }
1673
1674
 
1675
+ /**
1676
+ * Structural mirror of `@bymax-one/nest-core`'s unexpected-error sink contract.
1677
+ *
1678
+ * Layer: server/interfaces — the shapes `PinoUnexpectedErrorSink` implements.
1679
+ * Declared here, not imported: this package has no dependency on nest-core and
1680
+ * does not take one. TypeScript compares the two contracts by shape, so
1681
+ * `PinoUnexpectedErrorSink` binds to nest-core's `BYMAX_UNEXPECTED_ERROR_SINK`
1682
+ * token as long as these shapes stay compatible with nest-core's.
1683
+ */
1684
+ /**
1685
+ * What nest-core hands the sink for one unexpected (non-`HttpException`) error.
1686
+ *
1687
+ * It never carries the request body, headers, query string or raw URL.
1688
+ */
1689
+ interface UnexpectedErrorReport {
1690
+ /** The original thrown value, by identity — not necessarily an `Error`. */
1691
+ readonly error: unknown;
1692
+ /** HTTP method, e.g. `'GET'`. */
1693
+ readonly method: string;
1694
+ /**
1695
+ * The matched route template, e.g. `'/invoices/:id'`, or `'<unmatched>'` when
1696
+ * the error was raised before routing. Never the raw URL.
1697
+ */
1698
+ readonly route: string;
1699
+ /** The request's correlation id. The key is absent when none resolved. */
1700
+ readonly correlationId?: string;
1701
+ /** The active OpenTelemetry trace id. The key is absent when nothing is recording. */
1702
+ readonly traceId?: string;
1703
+ }
1704
+ /**
1705
+ * A receiver for unexpected errors, called synchronously by nest-core's
1706
+ * exception filter before the response is written.
1707
+ */
1708
+ interface IUnexpectedErrorSink {
1709
+ /**
1710
+ * Record one unexpected error. Must be cheap: it runs on the response path.
1711
+ *
1712
+ * @param report - The error and its request coordinates.
1713
+ */
1714
+ record(report: UnexpectedErrorReport): void;
1715
+ }
1716
+
1717
+ /**
1718
+ * Writes each unexpected error nest-core reports as one `HTTP_UNEXPECTED_ERROR`
1719
+ * entry at `error` level.
1720
+ */
1721
+ declare class PinoUnexpectedErrorSink implements IUnexpectedErrorSink {
1722
+ private readonly logger;
1723
+ /**
1724
+ * @param logger - The structured logger the entry is written to. Injected by
1725
+ * token explicitly: the package is bundled without `emitDecoratorMetadata`,
1726
+ * so implicit class-type injection would not resolve.
1727
+ */
1728
+ constructor(logger: PinoLoggerService);
1729
+ /**
1730
+ * Write one `error` entry for an unexpected error. Never throws.
1731
+ *
1732
+ * The entry carries `err` (name, message, scrubbed stack, cause chain and own
1733
+ * fields, through the configured `err` serializer), `method`, `route` and,
1734
+ * when present, `correlationId`. It deliberately carries nothing else from
1735
+ * the request: the report has no body, headers or query to leak, and this
1736
+ * method adds none.
1737
+ *
1738
+ * `report.traceId` is not written. The trace mixin already stamps the active
1739
+ * span's trace id under the configured `otel.traceIdField` — the same span
1740
+ * nest-core read it from — and writing it here would override that field and
1741
+ * ignore `otel.shouldAutoInjectTraceContext: false`.
1742
+ *
1743
+ * @param report - The error and its request coordinates, from nest-core.
1744
+ * @example
1745
+ * sink.record({ error: new TypeError('x is undefined'), method: 'GET', route: '/invoices/:id', correlationId: 'req-1' })
1746
+ * // → { level: 'error', logKey: 'HTTP_UNEXPECTED_ERROR', msg: 'x is undefined',
1747
+ * // err: { type: 'TypeError', message: 'x is undefined', stack: '…' },
1748
+ * // method: 'GET', route: '/invoices/:id', correlationId: 'req-1' }
1749
+ */
1750
+ record(report: UnexpectedErrorReport): void;
1751
+ }
1752
+
1674
1753
  /**
1675
1754
  * Inject a logger into a constructor parameter.
1676
1755
  *
@@ -1865,4 +1944,5 @@ interface LogEntry {
1865
1944
  */
1866
1945
  declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
1867
1946
 
1868
- export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpAccessLogMiddleware, 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, type PrettyViewOptions, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyAccessLog, applyRequestIdMiddleware };
1947
+ export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PinoUnexpectedErrorSink, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware };
1948
+ export type { BymaxLoggerModuleAsyncOptions, BymaxLoggerModuleOptions, BymaxLoggerModuleOptionsFactory, EmittedDeploymentResource, EmittedServiceResource, HttpOptions, ILogDestination, IUnexpectedErrorSink, IncomingHeaders, LogContext$1 as LogContextBag, LogEntry, LogLevel, LoggableRequest, LoggableResponse, NextHandler, OtelOptions, PrettyViewOptions, ReservedLogKey, ResolvedBymaxLoggerModuleOptions, ResolvedServiceMetadata, ServiceMetadata, UnexpectedErrorReport };
@@ -572,6 +572,7 @@ declare const RESERVED_LOG_KEYS: Readonly<{
572
572
  readonly HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED";
573
573
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
574
574
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
575
+ readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
575
576
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
576
577
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
577
578
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
@@ -1671,6 +1672,84 @@ declare class RequestIdMiddleware implements NestMiddleware {
1671
1672
  private resolveTenantId;
1672
1673
  }
1673
1674
 
1675
+ /**
1676
+ * Structural mirror of `@bymax-one/nest-core`'s unexpected-error sink contract.
1677
+ *
1678
+ * Layer: server/interfaces — the shapes `PinoUnexpectedErrorSink` implements.
1679
+ * Declared here, not imported: this package has no dependency on nest-core and
1680
+ * does not take one. TypeScript compares the two contracts by shape, so
1681
+ * `PinoUnexpectedErrorSink` binds to nest-core's `BYMAX_UNEXPECTED_ERROR_SINK`
1682
+ * token as long as these shapes stay compatible with nest-core's.
1683
+ */
1684
+ /**
1685
+ * What nest-core hands the sink for one unexpected (non-`HttpException`) error.
1686
+ *
1687
+ * It never carries the request body, headers, query string or raw URL.
1688
+ */
1689
+ interface UnexpectedErrorReport {
1690
+ /** The original thrown value, by identity — not necessarily an `Error`. */
1691
+ readonly error: unknown;
1692
+ /** HTTP method, e.g. `'GET'`. */
1693
+ readonly method: string;
1694
+ /**
1695
+ * The matched route template, e.g. `'/invoices/:id'`, or `'<unmatched>'` when
1696
+ * the error was raised before routing. Never the raw URL.
1697
+ */
1698
+ readonly route: string;
1699
+ /** The request's correlation id. The key is absent when none resolved. */
1700
+ readonly correlationId?: string;
1701
+ /** The active OpenTelemetry trace id. The key is absent when nothing is recording. */
1702
+ readonly traceId?: string;
1703
+ }
1704
+ /**
1705
+ * A receiver for unexpected errors, called synchronously by nest-core's
1706
+ * exception filter before the response is written.
1707
+ */
1708
+ interface IUnexpectedErrorSink {
1709
+ /**
1710
+ * Record one unexpected error. Must be cheap: it runs on the response path.
1711
+ *
1712
+ * @param report - The error and its request coordinates.
1713
+ */
1714
+ record(report: UnexpectedErrorReport): void;
1715
+ }
1716
+
1717
+ /**
1718
+ * Writes each unexpected error nest-core reports as one `HTTP_UNEXPECTED_ERROR`
1719
+ * entry at `error` level.
1720
+ */
1721
+ declare class PinoUnexpectedErrorSink implements IUnexpectedErrorSink {
1722
+ private readonly logger;
1723
+ /**
1724
+ * @param logger - The structured logger the entry is written to. Injected by
1725
+ * token explicitly: the package is bundled without `emitDecoratorMetadata`,
1726
+ * so implicit class-type injection would not resolve.
1727
+ */
1728
+ constructor(logger: PinoLoggerService);
1729
+ /**
1730
+ * Write one `error` entry for an unexpected error. Never throws.
1731
+ *
1732
+ * The entry carries `err` (name, message, scrubbed stack, cause chain and own
1733
+ * fields, through the configured `err` serializer), `method`, `route` and,
1734
+ * when present, `correlationId`. It deliberately carries nothing else from
1735
+ * the request: the report has no body, headers or query to leak, and this
1736
+ * method adds none.
1737
+ *
1738
+ * `report.traceId` is not written. The trace mixin already stamps the active
1739
+ * span's trace id under the configured `otel.traceIdField` — the same span
1740
+ * nest-core read it from — and writing it here would override that field and
1741
+ * ignore `otel.shouldAutoInjectTraceContext: false`.
1742
+ *
1743
+ * @param report - The error and its request coordinates, from nest-core.
1744
+ * @example
1745
+ * sink.record({ error: new TypeError('x is undefined'), method: 'GET', route: '/invoices/:id', correlationId: 'req-1' })
1746
+ * // → { level: 'error', logKey: 'HTTP_UNEXPECTED_ERROR', msg: 'x is undefined',
1747
+ * // err: { type: 'TypeError', message: 'x is undefined', stack: '…' },
1748
+ * // method: 'GET', route: '/invoices/:id', correlationId: 'req-1' }
1749
+ */
1750
+ record(report: UnexpectedErrorReport): void;
1751
+ }
1752
+
1674
1753
  /**
1675
1754
  * Inject a logger into a constructor parameter.
1676
1755
  *
@@ -1865,4 +1944,5 @@ interface LogEntry {
1865
1944
  */
1866
1945
  declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
1867
1946
 
1868
- export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpAccessLogMiddleware, 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, type PrettyViewOptions, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyAccessLog, applyRequestIdMiddleware };
1947
+ export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PinoUnexpectedErrorSink, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware };
1948
+ export type { BymaxLoggerModuleAsyncOptions, BymaxLoggerModuleOptions, BymaxLoggerModuleOptionsFactory, EmittedDeploymentResource, EmittedServiceResource, HttpOptions, ILogDestination, IUnexpectedErrorSink, IncomingHeaders, LogContext$1 as LogContextBag, LogEntry, LogLevel, LoggableRequest, LoggableResponse, NextHandler, OtelOptions, PrettyViewOptions, ReservedLogKey, ResolvedBymaxLoggerModuleOptions, ResolvedServiceMetadata, ServiceMetadata, UnexpectedErrorReport };
@@ -182,6 +182,7 @@ var RESERVED_LOG_KEYS = Object.freeze({
182
182
  HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
183
183
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
184
184
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
185
+ HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
185
186
  METHOD_EXECUTION: "METHOD_EXECUTION",
186
187
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
187
188
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
@@ -2906,6 +2907,63 @@ var ALL_ROUTES = "/";
2906
2907
  function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
2907
2908
  consumer.apply(RequestIdMiddleware, HttpAccessLogMiddleware).forRoutes(routes);
2908
2909
  }
2910
+ var PinoUnexpectedErrorSink = class {
2911
+ /**
2912
+ * @param logger - The structured logger the entry is written to. Injected by
2913
+ * token explicitly: the package is bundled without `emitDecoratorMetadata`,
2914
+ * so implicit class-type injection would not resolve.
2915
+ */
2916
+ constructor(logger) {
2917
+ this.logger = logger;
2918
+ }
2919
+ /**
2920
+ * Write one `error` entry for an unexpected error. Never throws.
2921
+ *
2922
+ * The entry carries `err` (name, message, scrubbed stack, cause chain and own
2923
+ * fields, through the configured `err` serializer), `method`, `route` and,
2924
+ * when present, `correlationId`. It deliberately carries nothing else from
2925
+ * the request: the report has no body, headers or query to leak, and this
2926
+ * method adds none.
2927
+ *
2928
+ * `report.traceId` is not written. The trace mixin already stamps the active
2929
+ * span's trace id under the configured `otel.traceIdField` — the same span
2930
+ * nest-core read it from — and writing it here would override that field and
2931
+ * ignore `otel.shouldAutoInjectTraceContext: false`.
2932
+ *
2933
+ * @param report - The error and its request coordinates, from nest-core.
2934
+ * @example
2935
+ * sink.record({ error: new TypeError('x is undefined'), method: 'GET', route: '/invoices/:id', correlationId: 'req-1' })
2936
+ * // → { level: 'error', logKey: 'HTTP_UNEXPECTED_ERROR', msg: 'x is undefined',
2937
+ * // err: { type: 'TypeError', message: 'x is undefined', stack: '…' },
2938
+ * // method: 'GET', route: '/invoices/:id', correlationId: 'req-1' }
2939
+ */
2940
+ record(report) {
2941
+ try {
2942
+ const metadata = { method: report.method, route: report.route };
2943
+ if (report.correlationId !== void 0) {
2944
+ metadata["correlationId"] = report.correlationId;
2945
+ }
2946
+ this.logger.errorStructured(
2947
+ RESERVED_LOG_KEYS.HTTP_UNEXPECTED_ERROR,
2948
+ toLoggableError(report.error),
2949
+ void 0,
2950
+ metadata
2951
+ );
2952
+ } catch {
2953
+ }
2954
+ }
2955
+ };
2956
+ PinoUnexpectedErrorSink = __decorateClass([
2957
+ Injectable(),
2958
+ __decorateParam(0, Inject(PinoLoggerService))
2959
+ ], PinoUnexpectedErrorSink);
2960
+ function toLoggableError(value) {
2961
+ try {
2962
+ return isErrorLike(value) ? value : sanitizeError(value);
2963
+ } catch {
2964
+ return sanitizeError(value);
2965
+ }
2966
+ }
2909
2967
  var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
2910
2968
  function InjectLogger(context) {
2911
2969
  return (target, propertyKey, parameterIndex) => {
@@ -2967,4 +3025,4 @@ function LogPerformance(thresholdMs = DEFAULT_THRESHOLD_MS) {
2967
3025
  // src/shared/constants/log-keys-convention.constants.ts
2968
3026
  var LOG_KEYS_CONVENTION_REGEX = /^[A-Z][A-Z0-9_]+_[A-Z][A-Z0-9_]+(_[A-Z][A-Z0-9_]+)?$/;
2969
3027
 
2970
- export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware };
3028
+ export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PinoUnexpectedErrorSink, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyAccessLog, applyRequestIdMiddleware };
@@ -17,6 +17,7 @@ var RESERVED_LOG_KEYS = Object.freeze({
17
17
  HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
18
18
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
19
19
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
20
+ HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
20
21
  METHOD_EXECUTION: "METHOD_EXECUTION",
21
22
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
22
23
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
@@ -238,6 +238,7 @@ declare const RESERVED_LOG_KEYS: Readonly<{
238
238
  readonly HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED";
239
239
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
240
240
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
241
+ readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
241
242
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
242
243
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
243
244
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
@@ -269,4 +270,5 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
269
270
  */
270
271
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
271
272
 
272
- export { type EmittedDeploymentResource, type EmittedServiceResource, LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
273
+ export { LOG_KEYS_CONVENTION_REGEX, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED };
274
+ export type { EmittedDeploymentResource, EmittedServiceResource, LogEntry, LogLevel, ReservedLogKey, ResolvedServiceMetadata, ServiceMetadata };
@@ -238,6 +238,7 @@ declare const RESERVED_LOG_KEYS: Readonly<{
238
238
  readonly HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED";
239
239
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
240
240
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
241
+ readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
241
242
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
242
243
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
243
244
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
@@ -269,4 +270,5 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
269
270
  */
270
271
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
271
272
 
272
- export { type EmittedDeploymentResource, type EmittedServiceResource, LOG_KEYS_CONVENTION_REGEX, type LogEntry, type LogLevel, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, type ReservedLogKey, type ResolvedServiceMetadata, type ServiceMetadata };
273
+ export { LOG_KEYS_CONVENTION_REGEX, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED };
274
+ export type { EmittedDeploymentResource, EmittedServiceResource, LogEntry, LogLevel, ReservedLogKey, ResolvedServiceMetadata, ServiceMetadata };
@@ -15,6 +15,7 @@ var RESERVED_LOG_KEYS = Object.freeze({
15
15
  HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
16
16
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
17
17
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
18
+ HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
18
19
  METHOD_EXECUTION: "METHOD_EXECUTION",
19
20
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
20
21
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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",
@@ -61,8 +61,8 @@
61
61
  "check:mutants": "node scripts/check-mutation-directives.mjs",
62
62
  "check:published": "node scripts/check-published-surface.mjs",
63
63
  "clean": "rm -rf dist coverage",
64
- "lint": "eslint src",
65
- "lint:fix": "eslint src --fix",
64
+ "lint": "eslint src scripts test bench '*.config.ts' '*.config.mjs' '*.config.cjs'",
65
+ "lint:fix": "eslint src scripts test bench '*.config.ts' '*.config.mjs' '*.config.cjs' --fix",
66
66
  "mutation": "stryker run",
67
67
  "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
68
68
  "mutation:dry-run": "stryker run --dryRunOnly",
@@ -142,7 +142,7 @@
142
142
  "tinybench": "^6.0.2",
143
143
  "ts-jest": "^29.4.9",
144
144
  "ts-node": "^10.9.2",
145
- "tsup": "^8.5.1",
145
+ "tsup": "^9.0.0",
146
146
  "tsx": "^4.19.2",
147
147
  "typescript": "^5.9.3"
148
148
  },