@bymax-one/nest-logger 1.3.1 → 1.4.1

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.
@@ -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";
@@ -878,8 +879,21 @@ declare class LogContextService {
878
879
  * Run `callback` inside a fresh context scope. Every log emitted
879
880
  * synchronously or asynchronously within the callback inherits `context`.
880
881
  *
882
+ * **A nested `run()` REPLACES the enclosing context, it does not extend it.**
883
+ * The scope starts from `context` alone, so a field an outer scope had set —
884
+ * a `tenantId` resolved at the edge, say — is absent for the whole inner
885
+ * scope: nothing throws, `get()` simply returns `undefined`, and the field is
886
+ * missing from every entry the callback emits. Reach for {@link runMerged}
887
+ * when the inner scope should extend the outer one.
888
+ *
889
+ * Replacement is the deliberate default because the opposite leaks: a
890
+ * background job started inside a request scope would silently inherit the
891
+ * caller's `userId` and attribute its entries to a user who never triggered
892
+ * it. A missing field is visibly missing; an inherited one is wrong while
893
+ * looking right.
894
+ *
881
895
  * @typeParam T - Return type of the callback.
882
- * @param context - Initial context bag for the scope.
896
+ * @param context - Initial context bag for the scope. Becomes the WHOLE scope.
883
897
  * @param callback - Function executed within the scope.
884
898
  * @returns Whatever `callback` returns.
885
899
  * @example
@@ -888,6 +902,32 @@ declare class LogContextService {
888
902
  * })
889
903
  */
890
904
  run<T>(context: LogContext$1, callback: () => T): T;
905
+ /**
906
+ * Run `callback` inside a scope that EXTENDS the enclosing one: the active
907
+ * context's fields are carried in, and `context` overrides them key by key.
908
+ *
909
+ * The explicit counterpart to {@link run}'s replacement semantics, for the
910
+ * case where an inner scope is genuinely a refinement of the outer — adding a
911
+ * `userId` once authentication resolved it, without having to restate the
912
+ * `requestId` the middleware already set.
913
+ *
914
+ * Outside any scope there is nothing to merge and this behaves exactly like
915
+ * {@link run}. Writes inside the new scope never reach the enclosing one: the
916
+ * merge produces a fresh object, so the outer context is intact after the
917
+ * callback returns.
918
+ *
919
+ * @typeParam T - Return type of the callback.
920
+ * @param context - Fields to add or override on top of the active context.
921
+ * @param callback - Function executed within the merged scope.
922
+ * @returns Whatever `callback` returns.
923
+ * @example
924
+ * logContext.run({ requestId: 'r_1' }, () => {
925
+ * logContext.runMerged({ userId: 'u_1' }, () => {
926
+ * logContext.get('requestId') // 'r_1' — carried in from the outer scope
927
+ * })
928
+ * })
929
+ */
930
+ runMerged<T>(context: LogContext$1, callback: () => T): T;
891
931
  /**
892
932
  * Read the active context, or `undefined` when called outside any scope.
893
933
  *
@@ -1294,6 +1334,76 @@ declare class HttpLoggingInterceptor implements NestInterceptor {
1294
1334
  private logError;
1295
1335
  }
1296
1336
 
1337
+ /**
1338
+ * `applyAccessLog` consumer helper — the access log mounted AHEAD of the body
1339
+ * parser, from `main.ts`.
1340
+ *
1341
+ * Layer: server/middlewares — the bootstrap-time counterpart to
1342
+ * {@link applyRequestIdMiddleware}. Both mount the same two middlewares in the
1343
+ * same order; they differ only in WHERE the mount lands in the Express stack,
1344
+ * and that difference decides an entire class of requests.
1345
+ *
1346
+ * NestJS registers the body parser one line before it registers module
1347
+ * middleware (`@nestjs/core/nest-application.js`: `registerParserMiddleware()`
1348
+ * then `registerModules()`), and Express dispatches in registration order. So
1349
+ * anything wired through `configure(consumer)` sits downstream of the parser,
1350
+ * and a body the parser rejects — `next(err)` skips every remaining non-error
1351
+ * handler — reaches no module middleware, no guard, no interceptor and no
1352
+ * handler. Measured: a `POST` with truncated JSON produced no access log at all.
1353
+ *
1354
+ * `INestApplication.use()` delegates straight to the HTTP adapter, so a handler
1355
+ * mounted here lands in the Express stack at call time — the same reason `helmet`
1356
+ * and `cookie-parser` are mounted this way.
1357
+ *
1358
+ * The boundary is **`init()`**, not `listen()`. `listen()` merely triggers `init()`
1359
+ * when it has not run yet, so "before `listen()`" is true only for the usual
1360
+ * bootstrap that never calls `init()` itself. A serverless handler or a test that
1361
+ * does `await app.init()` and mounts afterwards is already behind the parser.
1362
+ *
1363
+ * {@link HttpAccessLogMiddleware} needed no new logic to work there: it emits
1364
+ * from the response's `'close'` event, so it never depends on a route matching,
1365
+ * a handler running or a filter catching anything.
1366
+ *
1367
+ * Full rationale, and the table of what each wiring emits today, is in the
1368
+ * README under "Requests rejected before routing".
1369
+ */
1370
+
1371
+ /**
1372
+ * Mount the correlation scope and the HTTP access log ahead of the body parser.
1373
+ *
1374
+ * Call AFTER `NestFactory.create()` and BEFORE the application initializes —
1375
+ * that is, before `app.init()`, or before `app.listen()` in the usual bootstrap
1376
+ * that never calls `init()` directly.
1377
+ *
1378
+ * **`init()` is the real deadline, and `listen()` is only where most bootstraps
1379
+ * meet it.** A serverless entry point or an integration test typically does
1380
+ * `await app.init()` and then wires things up; mounting there still mounts, but
1381
+ * BEHIND the parser, which forfeits the entire reason to use this helper — the
1382
+ * parser-rejected request goes unlogged exactly as before, with no error to say
1383
+ * so. The mount is not rejected when it is late because NestJS exposes no
1384
+ * supported signal for "already initialized" — `isInitialized` is private, and
1385
+ * reaching into the adapter's router to guess would break on a NestJS patch
1386
+ * release while claiming to be a safety net.
1387
+ *
1388
+ * The ordering is the one thing to get right here, so it is worth a glance at
1389
+ * the call site rather than trust: mount it immediately after `NestFactory.create()`.
1390
+ *
1391
+ * Requires `http.isEnabled` for the access log itself; with HTTP logging off the
1392
+ * correlation scope is still opened (matching `applyRequestIdMiddleware`) and no
1393
+ * access entries are written.
1394
+ *
1395
+ * @param app - The Nest application returned by `NestFactory.create()`.
1396
+ * @throws Error When `BymaxLoggerModule` was not imported.
1397
+ * @example
1398
+ * import { BymaxLoggerModule, applyAccessLog } from '@bymax-one/nest-logger'
1399
+ *
1400
+ * const app = await NestFactory.create(AppModule, { bufferLogs: true })
1401
+ * BymaxLoggerModule.useNestLogger(app)
1402
+ * applyAccessLog(app) // before init(): listen() triggers it if you have not
1403
+ * await app.listen(3000)
1404
+ */
1405
+ declare function applyAccessLog(app: INestApplication): void;
1406
+
1297
1407
  /**
1298
1408
  * `applyRequestIdMiddleware` consumer helper.
1299
1409
  *
@@ -1419,6 +1529,47 @@ interface LoggableResponse {
1419
1529
  /** Continuation passed to a middleware; invoked with no arguments here. */
1420
1530
  type NextHandler = () => void;
1421
1531
 
1532
+ /**
1533
+ * Records the HTTP access log for every request, including the ones no
1534
+ * interceptor can observe.
1535
+ */
1536
+ declare class HttpAccessLogMiddleware implements NestMiddleware {
1537
+ private readonly logger;
1538
+ private readonly logContext;
1539
+ /** Whether HTTP logging is on at all (`http.isEnabled`). */
1540
+ private readonly isEnabled;
1541
+ /** Path patterns that bypass HTTP logging entirely (health checks, metrics). */
1542
+ private readonly excludePaths;
1543
+ /**
1544
+ * @param logger - The structured logger the access entries are written to.
1545
+ * Injected by token explicitly: the package is bundled without
1546
+ * `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
1547
+ * @param options - Resolved module options (read for `http.excludePaths`).
1548
+ */
1549
+ constructor(logger: PinoLoggerService, logContext: LogContextService, options: ResolvedBymaxLoggerModuleOptions);
1550
+ /**
1551
+ * Emit START, arm the terminal entry on `'close'`, and continue the chain.
1552
+ *
1553
+ * @param req - The incoming request (Express satisfies the contract).
1554
+ * @param res - The outgoing response (Express satisfies the contract).
1555
+ * @param next - The next handler in the chain.
1556
+ */
1557
+ use(req: LoggableRequest, res: LoggableResponse, next: NextHandler): void;
1558
+ /**
1559
+ * Emit the terminal entry for a closed request.
1560
+ *
1561
+ * `userId` is read HERE rather than at START because authentication runs in a
1562
+ * guard, downstream of this middleware — at START there is no principal yet.
1563
+ *
1564
+ * @param req - The request, carrying any error the interceptor recorded.
1565
+ * @param res - The closed response.
1566
+ * @param method - The HTTP method.
1567
+ * @param url - The normalized request URL.
1568
+ * @param duration - Elapsed milliseconds.
1569
+ */
1570
+ private logTerminal;
1571
+ }
1572
+
1422
1573
  /**
1423
1574
  * Reads (or generates) the correlation id and starts the per-request log
1424
1575
  * context scope.
@@ -1447,13 +1598,156 @@ declare class RequestIdMiddleware implements NestMiddleware {
1447
1598
  constructor(logContext: LogContextService, options: ResolvedBymaxLoggerModuleOptions);
1448
1599
  /**
1449
1600
  * Echo or mint the `x-request-id`, expose it on the response, and run the
1450
- * remainder of the request inside a fresh log-context scope.
1601
+ * remainder of the request inside its own log-context scope.
1602
+ *
1603
+ * Idempotent: mounting this middleware twice on the same request mints one id,
1604
+ * not two. The request gets its OWN store, started from the enclosing scope's
1605
+ * correlation fields — see {@link INHERITABLE_CONTEXT_KEYS} — and nothing
1606
+ * else, because no other field can be known to belong to this request.
1607
+ *
1608
+ * An enclosing scope's `requestId` is never adopted and never echoed: adoption
1609
+ * reads a value this middleware recorded on the REQUEST, which a scope opened
1610
+ * elsewhere does not have. What the two generation modes do with one, measured
1611
+ * on two requests sharing a long-lived enclosing scope:
1612
+ *
1613
+ * - `shouldGenerateRequestId: true` (default) — each request mints its own
1614
+ * id, the two requests differ, and the enclosing value reaches neither the
1615
+ * header nor the entries.
1616
+ * - `shouldGenerateRequestId: false` with no inbound header — nothing is
1617
+ * minted, so the request scope inherits the enclosing `requestId` and every
1618
+ * request inside that scope logs under it. No header is exposed. That is
1619
+ * the consumer's own scope being inherited, not correlation being reused,
1620
+ * and it is the configuration that says the gateway owns the id.
1451
1621
  *
1452
1622
  * @param req - The incoming request (Express satisfies the contract).
1453
1623
  * @param res - The outgoing response (Express satisfies the contract).
1454
1624
  * @param next - The next handler in the chain.
1455
1625
  */
1456
1626
  use(req: LoggableRequest, res: LoggableResponse, next: NextHandler): void;
1627
+ /**
1628
+ * The enclosing scope's fields that a request scope may start from.
1629
+ *
1630
+ * Plain `run()` would discard everything the caller had set and a plain merge
1631
+ * would carry identity across requests; {@link INHERITABLE_CONTEXT_KEYS} is
1632
+ * the middle a request scope actually needs.
1633
+ *
1634
+ * @returns A fresh bag, never the enclosing store itself, so writes inside the
1635
+ * request never reach the scope that opened it.
1636
+ */
1637
+ private inheritableContext;
1638
+ /**
1639
+ * Second mount on a request that already carries a correlation id: echo the id
1640
+ * onto the response and fill in a tenant the scope does not have yet, without
1641
+ * opening another scope.
1642
+ *
1643
+ * Nothing is minted and nothing already set is overwritten, so a third mount —
1644
+ * or a thirtieth — changes nothing. The response header is written on every
1645
+ * pass because the enclosing scope may have been opened by consumer code that
1646
+ * never exposed it.
1647
+ *
1648
+ * @param requestId - The id already in scope, which wins.
1649
+ * @param req - The incoming request, read for the tenant header.
1650
+ * @param res - The outgoing response, where the effective id is exposed.
1651
+ */
1652
+ private adoptExistingScope;
1653
+ /**
1654
+ * The correlation id for this request: the inbound header when acceptable,
1655
+ * otherwise a fresh UUID — or nothing at all when `shouldGenerateRequestId` is
1656
+ * off and the caller sent none, which leaves correlation to the upstream
1657
+ * gateway.
1658
+ *
1659
+ * @param req - The incoming request.
1660
+ * @returns The id, or `undefined` when none is available and none is minted.
1661
+ */
1662
+ private resolveRequestId;
1663
+ /**
1664
+ * The tenant id carried by the configured header, when it is acceptable.
1665
+ *
1666
+ * Never minted: a tenant is an assertion about who the request belongs to, and
1667
+ * inventing one would be a fabricated claim rather than a correlation aid.
1668
+ *
1669
+ * @param req - The incoming request.
1670
+ * @returns The tenant id, or `undefined`.
1671
+ */
1672
+ private resolveTenantId;
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;
1457
1751
  }
1458
1752
 
1459
1753
  /**
@@ -1650,4 +1944,4 @@ interface LogEntry {
1650
1944
  */
1651
1945
  declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
1652
1946
 
1653
- export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, 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, applyRequestIdMiddleware };
1947
+ export { BymaxLoggerModule, type BymaxLoggerModuleAsyncOptions, type BymaxLoggerModuleOptions, type BymaxLoggerModuleOptionsFactory, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, type EmittedDeploymentResource, type EmittedServiceResource, HttpAccessLogMiddleware, HttpExceptionFilter, HttpLoggingInterceptor, type HttpOptions, type ILogDestination, type IUnexpectedErrorSink, 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, PinoUnexpectedErrorSink, PrettyDevDestination, type PrettyViewOptions, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, type UnexpectedErrorReport, applyAccessLog, applyRequestIdMiddleware };