@bymax-one/nest-logger 1.2.4 → 1.2.6

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.
@@ -864,28 +864,104 @@ declare class DefaultStdoutDestination implements ILogDestination {
864
864
  });
865
865
  /**
866
866
  * Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
867
+ *
868
+ * Routed through {@link writeStdoutSafely} rather than calling
869
+ * `process.stdout.write` directly. This is the sink a consumer gets when they
870
+ * configure nothing, so it is the path that decides whether a closed pipe
871
+ * (`node app | head`) kills the application — and unguarded, it did: EPIPE
872
+ * arrives asynchronously through the stream's `'error'` event, Node attaches no
873
+ * default handler, and the emit becomes an uncaught exception. Measured on the
874
+ * built artifact, exit code 42.
867
875
  */
868
876
  write(payload: string): void;
869
877
  }
870
878
 
879
+ /**
880
+ * How the development terminal renders an entry — a VIEW over the record, never
881
+ * a change to it. Every field maps to the `pino-pretty` option of the same name.
882
+ *
883
+ * Declared here rather than reusing `pino-pretty`'s own `PrettyOptions`, and the
884
+ * reason is load-bearing: a type import from the peer emits a hard
885
+ * `import { PrettyOptions } from 'pino-pretty'` at the top of the published
886
+ * `.d.ts`, so every consumer who type-checks WITHOUT the optional peer installed
887
+ * would hit an unresolved module. That would make an optional peer effectively
888
+ * required — a worse regression than the missing options, and the same
889
+ * contradiction this library already refused when asked to hard-fail on a missing
890
+ * peer. Measured on the built artifact, not assumed.
891
+ *
892
+ * `destination` is deliberately absent. The library owns where entries go; a
893
+ * consumer redirecting the stream would route around the multistream fan-out and
894
+ * the last-resort rescue. Omitting it from this type states the intent — applying
895
+ * the library's own value AFTER the spread in {@link PrettyDevDestination.onInit}
896
+ * is what enforces it, including against an untyped JavaScript caller.
897
+ *
898
+ * @example
899
+ * // One line per entry, with the fields a project repeats on every line hidden.
900
+ * const view: PrettyViewOptions = {
901
+ * singleLine: true,
902
+ * ignore: 'pid,hostname,service,deployment,event.name'
903
+ * }
904
+ */
905
+ interface PrettyViewOptions {
906
+ /**
907
+ * Collapse the whole entry onto one line. The single biggest change to how a
908
+ * terminal reads — `false` is what expands one entry into a block.
909
+ * Default: `false`.
910
+ */
911
+ singleLine?: boolean;
912
+ /**
913
+ * Comma-separated field names to hide from the rendered line. Display-only:
914
+ * what a real sink receives is untouched. Extend rather than replace when the
915
+ * goal is quieter output — the default already hides the noisiest fields.
916
+ * Default: `'pid,hostname,service'`.
917
+ */
918
+ ignore?: string;
919
+ /**
920
+ * Render only the message line, hiding the record entirely. In pretty mode
921
+ * there is no JSON copy behind it (`destinations` REPLACES stdout), so a field
922
+ * hidden here is not visible anywhere — including `logKey`. That is the point
923
+ * of the option rather than a caveat; use `messageFormat` to pull a specific
924
+ * field back into the line. Default: `false`.
925
+ */
926
+ hideObject?: boolean;
927
+ /**
928
+ * Template for the message line, e.g. `'[{context}] {msg}'`. The way to keep a
929
+ * chosen field visible when `hideObject` is on.
930
+ */
931
+ messageFormat?: string;
932
+ /** Timestamp format, or `false` to leave it raw. Default: `'SYS:HH:MM:ss.l'`. */
933
+ translateTime?: string | boolean;
934
+ /** ANSI colour in the rendered line. Default: `true`. */
935
+ colorize?: boolean;
936
+ }
871
937
  /**
872
938
  * Pretty-print destination backed by `pino-pretty`.
873
939
  *
874
940
  * Lifecycle contract (driven by `DestinationRegistry`):
875
- * 1. `onInit()` resolves `pino-pretty` and builds the transform (rendering to
876
- * `process.stdout`).
941
+ * 1. `onInit()` resolves `pino-pretty`, builds the transform (rendering to
942
+ * `process.stdout`), and flushes anything written before it existed.
877
943
  * 2. `write()` forwards each serialized line to the transform.
878
944
  * 3. `onShutdown()` flushes and closes the transform.
879
945
  *
880
946
  * @example
881
- * BymaxLoggerModule.forRoot({
882
- * service: { name: 'api', version: '1.0.0' },
883
- * destinations: [new PrettyDevDestination()]
947
+ * // Default view — unchanged by the options below.
948
+ * new PrettyDevDestination()
949
+ *
950
+ * // One line per entry, with this project's repeated fields hidden.
951
+ * new PrettyDevDestination({
952
+ * view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
953
+ * })
954
+ *
955
+ * // Message-only, with the context pulled back into the line.
956
+ * new PrettyDevDestination({
957
+ * view: { hideObject: true, messageFormat: '[{context}] {msg}' }
884
958
  * })
885
959
  */
886
960
  declare class PrettyDevDestination implements ILogDestination {
887
961
  readonly name = "pretty-dev";
888
962
  readonly minLevel?: LogLevel;
963
+ /** Consumer-chosen view, merged over {@link DEFAULT_VIEW} at init. */
964
+ private readonly view;
889
965
  /**
890
966
  * The `pino-pretty` transform stream. Undefined until {@link onInit} succeeds.
891
967
  */
@@ -893,16 +969,42 @@ declare class PrettyDevDestination implements ILogDestination {
893
969
  /**
894
970
  * Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
895
971
  * permanently-failed destination — whose writes are DROPPED, since the registry
896
- * dropped it from the active set — from the transient pre-init window, where
897
- * {@link write} falls back to raw stdout so a bootstrap log is not lost.
972
+ * dropped it from the active set — from the transient pre-init window.
898
973
  */
899
974
  private initFailed;
975
+ /**
976
+ * Entries written before {@link onInit} built the transform, held so they can be
977
+ * RENDERED rather than printed raw.
978
+ *
979
+ * The transform cannot exist earlier: loading the optional peer is async, and
980
+ * `onInit` is the first moment the lifecycle offers. Before this buffer those
981
+ * entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
982
+ * put dozens of JSON lines on screen before the first rendered one, and a
983
+ * developer who had just turned pretty on reasonably concluded it had not
984
+ * worked. Reported from exactly that reading.
985
+ */
986
+ private buffer;
987
+ /**
988
+ * Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
989
+ * straight to raw stdout — and the buffer is drained raw at the same moment, so
990
+ * the output stays in ORDER rather than replaying old entries after newer ones.
991
+ */
992
+ private bufferOverflowed;
993
+ /**
994
+ * UTF-8 size of everything currently held, tracked incrementally so the bound
995
+ * costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
996
+ */
997
+ private bufferedBytes;
900
998
  /**
901
999
  * @param opts.minLevel — Optional minimum level filter applied by the
902
1000
  * destination registry's multi-stream wiring. Omit to accept every level.
1001
+ * @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
1002
+ * Omitted fields keep their default, so `new PrettyDevDestination()` renders
1003
+ * exactly as it did before this option existed.
903
1004
  */
904
1005
  constructor(opts?: {
905
1006
  minLevel?: LogLevel;
1007
+ view?: PrettyViewOptions;
906
1008
  });
907
1009
  /**
908
1010
  * Lazily resolve `pino-pretty` and build the colorized transform that renders
@@ -921,12 +1023,24 @@ declare class PrettyDevDestination implements ILogDestination {
921
1023
  * fan-out already skips a failed destination before reaching this method
922
1024
  * (and rescues the entry when nothing else initialized); this branch is the
923
1025
  * belt to that, for any caller holding the destination directly;
924
- * - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
925
- * → fall back to raw stdout so the entry is not lost.
1026
+ * - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
1027
+ * emits while instantiating providers) → BUFFERED, then rendered through
1028
+ * the transform once it exists. Nothing is lost on any path: if init fails
1029
+ * the buffer is drained raw, and if it overflows it is drained raw in order.
926
1030
  *
927
1031
  * @param payload — Newline-terminated JSON entry, UTF-8 encoded.
928
1032
  */
929
1033
  write(payload: string): void;
1034
+ /**
1035
+ * Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
1036
+ *
1037
+ * Emptying unconditionally is what makes this safe to call from both the
1038
+ * success and failure paths: whichever runs first owns the entries, and the
1039
+ * other finds nothing to emit twice.
1040
+ *
1041
+ * @param emit - Receives each buffered payload, oldest first.
1042
+ */
1043
+ private flushBuffer;
930
1044
  /**
931
1045
  * Flush and close the pretty transform, resolving once the stream has ended.
932
1046
  * A no-op when {@link onInit} never ran.
@@ -1382,4 +1496,4 @@ interface LogEntry {
1382
1496
  */
1383
1497
  declare const LOG_KEYS_CONVENTION_REGEX: RegExp;
1384
1498
 
1385
- 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, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, type ReservedLogKey, type ResolvedBymaxLoggerModuleOptions, type ResolvedServiceMetadata, type ServiceMetadata, applyRequestIdMiddleware };
1499
+ 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 };
@@ -225,6 +225,75 @@ function escapeControlCharacters(text) {
225
225
  return text.replace(UNSAFE_IN_MULTILINE, toEscapeSequence);
226
226
  }
227
227
 
228
+ // src/server/utils/sanitize-error.util.ts
229
+ var DEFAULT_MAX_CAUSE_DEPTH = 3;
230
+ var MAX_AGGREGATE_ERRORS = 10;
231
+ var CIRCULAR_SENTINEL = "[Circular]";
232
+ function sanitizeError(err, options) {
233
+ const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
234
+ return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
235
+ }
236
+ function isErrorLike(value) {
237
+ if (typeof value !== "object" || value === null) {
238
+ return false;
239
+ }
240
+ const candidate = value;
241
+ return typeof candidate.name === "string" && typeof candidate.message === "string";
242
+ }
243
+ function toSanitizedError(value, seen, depth2, maxDepth) {
244
+ try {
245
+ if (!isErrorLike(value)) {
246
+ return { name: "UnknownError", message: String(value) };
247
+ }
248
+ seen.add(value);
249
+ const result = { name: value.name, message: value.message };
250
+ if (value.stack !== void 0) {
251
+ result.stack = scrubStack(value.stack);
252
+ }
253
+ const isAtDepthLimit = depth2 >= maxDepth;
254
+ if (value.cause !== void 0) {
255
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
256
+ }
257
+ const aggregated = readAggregatedErrors(value);
258
+ if (aggregated !== void 0) {
259
+ const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
260
+ result.errors = capped.map(
261
+ (inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
262
+ );
263
+ if (aggregated.length > MAX_AGGREGATE_ERRORS) {
264
+ result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
265
+ }
266
+ }
267
+ return result;
268
+ } catch {
269
+ return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
270
+ }
271
+ }
272
+ function toSanitizedChild(value, seen, depth2, maxDepth) {
273
+ if (isObject(value) && seen.has(value)) {
274
+ return CIRCULAR_SENTINEL;
275
+ }
276
+ return toSanitizedError(value, seen, depth2, maxDepth);
277
+ }
278
+ function readAggregatedErrors(value) {
279
+ const candidate = value.errors;
280
+ return Array.isArray(candidate) ? candidate : void 0;
281
+ }
282
+ function scrubStack(stack) {
283
+ return escapeControlCharacters(
284
+ stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
285
+ );
286
+ }
287
+ function isObject(value) {
288
+ return typeof value === "object" && value !== null;
289
+ }
290
+ function createDepthMarker() {
291
+ return { _truncated: true, _reason: "cause-depth-exceeded" };
292
+ }
293
+ function createWidthMarker(omitted) {
294
+ return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
295
+ }
296
+
228
297
  // src/server/services/pino-logger.service.ts
229
298
  var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
230
299
  function withoutOwnedKeys(metadata) {
@@ -253,6 +322,9 @@ function safeErrorMessage(error) {
253
322
  return "Unreadable error message";
254
323
  }
255
324
  }
325
+ function findErrorParam(optionalParams) {
326
+ return optionalParams.find((param) => isErrorLike(param));
327
+ }
256
328
  function assignIfDefined(payload, key, value) {
257
329
  if (value !== void 0) {
258
330
  Reflect.defineProperty(payload, key, { value, enumerable: true });
@@ -289,7 +361,7 @@ var PinoLoggerService = class {
289
361
  * @param optionalParams - NestJS trailing params (`stack` at index 0, `context` at index 1).
290
362
  */
291
363
  error(message, ...optionalParams) {
292
- if (message instanceof Error) {
364
+ if (isErrorLike(message)) {
293
365
  const payload2 = { err: message };
294
366
  assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
295
367
  this.pino.error(payload2, safeErrorMessage(message));
@@ -300,6 +372,7 @@ var PinoLoggerService = class {
300
372
  const payload = {};
301
373
  assignIfDefined(payload, "context", context);
302
374
  assignIfDefined(payload, "stack", stack);
375
+ assignIfDefined(payload, "err", findErrorParam(optionalParams));
303
376
  this.pino.error(
304
377
  payload,
305
378
  toSingleLineMessage(typeof message === "string" ? message : String(message))
@@ -458,7 +531,9 @@ var PinoLoggerService = class {
458
531
  emitNestStyle(level, message, optionalParams) {
459
532
  const payload = {};
460
533
  assignIfDefined(payload, "context", this.resolveContext(optionalParams));
461
- const msg = toSingleLineMessage(typeof message === "string" ? message : String(message));
534
+ const leading = isErrorLike(message) ? message : void 0;
535
+ assignIfDefined(payload, "err", leading ?? findErrorParam(optionalParams));
536
+ const msg = leading === void 0 ? toSingleLineMessage(typeof message === "string" ? message : String(message)) : safeErrorMessage(leading);
462
537
  switch (level) {
463
538
  case "info":
464
539
  this.pino.info(payload, msg);
@@ -508,6 +583,30 @@ function collectContextLoggerProviders() {
508
583
  function collectContextLoggerTokens() {
509
584
  return Array.from(contextTokens.values());
510
585
  }
586
+
587
+ // src/server/utils/safe-stdio.util.ts
588
+ var swallowStreamError = () => void 0;
589
+ function ensureGuarded(stream) {
590
+ if (stream.listeners("error").includes(swallowStreamError)) {
591
+ return;
592
+ }
593
+ stream.on("error", swallowStreamError);
594
+ }
595
+ function writeSafely(stream, payload) {
596
+ try {
597
+ ensureGuarded(stream);
598
+ stream.write(payload);
599
+ } catch {
600
+ }
601
+ }
602
+ function writeStdoutSafely(payload) {
603
+ writeSafely(process.stdout, payload);
604
+ }
605
+ function writeStderrSafely(line) {
606
+ writeSafely(process.stderr, line);
607
+ }
608
+
609
+ // src/server/destinations/default-stdout.destination.ts
511
610
  var DefaultStdoutDestination = class {
512
611
  /**
513
612
  * @param opts.minLevel — Optional minimum level filter. Entries below this
@@ -522,9 +621,17 @@ var DefaultStdoutDestination = class {
522
621
  }
523
622
  /**
524
623
  * Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
624
+ *
625
+ * Routed through {@link writeStdoutSafely} rather than calling
626
+ * `process.stdout.write` directly. This is the sink a consumer gets when they
627
+ * configure nothing, so it is the path that decides whether a closed pipe
628
+ * (`node app | head`) kills the application — and unguarded, it did: EPIPE
629
+ * arrives asynchronously through the stream's `'error'` event, Node attaches no
630
+ * default handler, and the emit becomes an uncaught exception. Measured on the
631
+ * built artifact, exit code 42.
525
632
  */
526
633
  write(payload) {
527
- process.stdout.write(payload);
634
+ writeStdoutSafely(payload);
528
635
  }
529
636
  };
530
637
  DefaultStdoutDestination = __decorateClass([
@@ -545,75 +652,6 @@ function stripQueryString(url) {
545
652
  return queryStart === -1 ? url : url.slice(0, queryStart);
546
653
  }
547
654
 
548
- // src/server/utils/sanitize-error.util.ts
549
- var DEFAULT_MAX_CAUSE_DEPTH = 3;
550
- var MAX_AGGREGATE_ERRORS = 10;
551
- var CIRCULAR_SENTINEL = "[Circular]";
552
- function sanitizeError(err, options) {
553
- const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
554
- return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
555
- }
556
- function isErrorLike(value) {
557
- if (typeof value !== "object" || value === null) {
558
- return false;
559
- }
560
- const candidate = value;
561
- return typeof candidate.name === "string" && typeof candidate.message === "string";
562
- }
563
- function toSanitizedError(value, seen, depth2, maxDepth) {
564
- try {
565
- if (!isErrorLike(value)) {
566
- return { name: "UnknownError", message: String(value) };
567
- }
568
- seen.add(value);
569
- const result = { name: value.name, message: value.message };
570
- if (value.stack !== void 0) {
571
- result.stack = scrubStack(value.stack);
572
- }
573
- const isAtDepthLimit = depth2 >= maxDepth;
574
- if (value.cause !== void 0) {
575
- result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
576
- }
577
- const aggregated = readAggregatedErrors(value);
578
- if (aggregated !== void 0) {
579
- const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
580
- result.errors = capped.map(
581
- (inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
582
- );
583
- if (aggregated.length > MAX_AGGREGATE_ERRORS) {
584
- result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
585
- }
586
- }
587
- return result;
588
- } catch {
589
- return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
590
- }
591
- }
592
- function toSanitizedChild(value, seen, depth2, maxDepth) {
593
- if (isObject(value) && seen.has(value)) {
594
- return CIRCULAR_SENTINEL;
595
- }
596
- return toSanitizedError(value, seen, depth2, maxDepth);
597
- }
598
- function readAggregatedErrors(value) {
599
- const candidate = value.errors;
600
- return Array.isArray(candidate) ? candidate : void 0;
601
- }
602
- function scrubStack(stack) {
603
- return escapeControlCharacters(
604
- stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
605
- );
606
- }
607
- function isObject(value) {
608
- return typeof value === "object" && value !== null;
609
- }
610
- function createDepthMarker() {
611
- return { _truncated: true, _reason: "cause-depth-exceeded" };
612
- }
613
- function createWidthMarker(omitted) {
614
- return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
615
- }
616
-
617
655
  // src/server/filters/http-exception.filter.ts
618
656
  var INTERNAL_ERROR_BODY = {
619
657
  statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
@@ -1025,7 +1063,7 @@ function reportDestinationFailure(logKey, name, cause, msg) {
1025
1063
  type: toSingleLineMessage(String(cause.name)),
1026
1064
  message: toSingleLineMessage(String(cause.message))
1027
1065
  } : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
1028
- process.stderr.write(
1066
+ writeStderrSafely(
1029
1067
  JSON.stringify({
1030
1068
  level: "error",
1031
1069
  logKey,
@@ -1048,10 +1086,7 @@ function reportWriteFailure(name, cause) {
1048
1086
  );
1049
1087
  }
1050
1088
  function rescueToStdout(payload) {
1051
- try {
1052
- process.stdout.write(payload);
1053
- } catch {
1054
- }
1089
+ writeStdoutSafely(payload);
1055
1090
  }
1056
1091
  function destinationToStream(destination, health) {
1057
1092
  return new Writable({
@@ -1748,7 +1783,7 @@ var DestinationRegistry = class {
1748
1783
  await destination.onShutdown?.();
1749
1784
  } catch (cause) {
1750
1785
  const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
1751
- process.stderr.write(
1786
+ writeStderrSafely(
1752
1787
  `[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
1753
1788
  `
1754
1789
  );
@@ -2028,23 +2063,60 @@ BymaxLoggerModule = __decorateClass([
2028
2063
  ], BymaxLoggerModule);
2029
2064
 
2030
2065
  // src/server/destinations/pretty-dev.destination.ts
2066
+ var DEFAULT_VIEW = {
2067
+ colorize: true,
2068
+ translateTime: "SYS:HH:MM:ss.l",
2069
+ ignore: "pid,hostname,service",
2070
+ singleLine: false
2071
+ };
2072
+ var MAX_BUFFERED_ENTRIES = 1e3;
2073
+ var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
2074
+ function writeRawToStdout(payload) {
2075
+ writeStdoutSafely(payload);
2076
+ }
2031
2077
  var PrettyDevDestination = class {
2032
2078
  /**
2033
2079
  * @param opts.minLevel — Optional minimum level filter applied by the
2034
2080
  * destination registry's multi-stream wiring. Omit to accept every level.
2081
+ * @param opts.view — Optional rendering overrides; see {@link PrettyViewOptions}.
2082
+ * Omitted fields keep their default, so `new PrettyDevDestination()` renders
2083
+ * exactly as it did before this option existed.
2035
2084
  */
2036
2085
  constructor(opts = {}) {
2037
2086
  this.name = "pretty-dev";
2038
2087
  /**
2039
2088
  * Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
2040
2089
  * permanently-failed destination — whose writes are DROPPED, since the registry
2041
- * dropped it from the active set — from the transient pre-init window, where
2042
- * {@link write} falls back to raw stdout so a bootstrap log is not lost.
2090
+ * dropped it from the active set — from the transient pre-init window.
2043
2091
  */
2044
2092
  this.initFailed = false;
2093
+ /**
2094
+ * Entries written before {@link onInit} built the transform, held so they can be
2095
+ * RENDERED rather than printed raw.
2096
+ *
2097
+ * The transform cannot exist earlier: loading the optional peer is async, and
2098
+ * `onInit` is the first moment the lifecycle offers. Before this buffer those
2099
+ * entries went to stdout as raw NDJSON — correct, nothing lost, but a real boot
2100
+ * put dozens of JSON lines on screen before the first rendered one, and a
2101
+ * developer who had just turned pretty on reasonably concluded it had not
2102
+ * worked. Reported from exactly that reading.
2103
+ */
2104
+ this.buffer = [];
2105
+ /**
2106
+ * Set once {@link MAX_BUFFERED_ENTRIES} is reached. From then on entries go
2107
+ * straight to raw stdout — and the buffer is drained raw at the same moment, so
2108
+ * the output stays in ORDER rather than replaying old entries after newer ones.
2109
+ */
2110
+ this.bufferOverflowed = false;
2111
+ /**
2112
+ * UTF-8 size of everything currently held, tracked incrementally so the bound
2113
+ * costs one `Buffer.byteLength` per entry rather than a walk of the buffer.
2114
+ */
2115
+ this.bufferedBytes = 0;
2045
2116
  if (opts.minLevel !== void 0) {
2046
2117
  this.minLevel = opts.minLevel;
2047
2118
  }
2119
+ this.view = opts.view ?? {};
2048
2120
  }
2049
2121
  /**
2050
2122
  * Lazily resolve `pino-pretty` and build the colorized transform that renders
@@ -2057,18 +2129,31 @@ var PrettyDevDestination = class {
2057
2129
  try {
2058
2130
  const { build } = await import('pino-pretty');
2059
2131
  this.stream = build({
2060
- colorize: true,
2061
- translateTime: "SYS:HH:MM:ss.l",
2062
- ignore: "pid,hostname,service",
2063
- singleLine: false,
2132
+ ...DEFAULT_VIEW,
2133
+ ...this.view,
2134
+ // AFTER the consumer's view, deliberately. Omitting `destination` from
2135
+ // `PrettyViewOptions` states that the library owns the sink; applying it
2136
+ // last is what ENFORCES it, including against an untyped caller reaching
2137
+ // in from JavaScript. A redirected stream would route around the fan-out
2138
+ // and the last-resort rescue.
2064
2139
  destination: process.stdout
2065
2140
  });
2066
2141
  } catch {
2067
2142
  this.initFailed = true;
2143
+ this.flushBuffer((payload) => {
2144
+ writeRawToStdout(payload);
2145
+ });
2068
2146
  throw new Error(
2069
2147
  "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
2070
2148
  );
2071
2149
  }
2150
+ this.flushBuffer((payload) => {
2151
+ try {
2152
+ this.stream?.write(payload);
2153
+ } catch {
2154
+ writeRawToStdout(payload);
2155
+ }
2156
+ });
2072
2157
  }
2073
2158
  /**
2074
2159
  * Write a serialized NDJSON entry to the pretty transform.
@@ -2079,8 +2164,10 @@ var PrettyDevDestination = class {
2079
2164
  * fan-out already skips a failed destination before reaching this method
2080
2165
  * (and rescues the entry when nothing else initialized); this branch is the
2081
2166
  * belt to that, for any caller holding the destination directly;
2082
- * - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
2083
- * → fall back to raw stdout so the entry is not lost.
2167
+ * - pre-init (transient window before {@link onInit}, e.g. every entry NestJS
2168
+ * emits while instantiating providers) → BUFFERED, then rendered through
2169
+ * the transform once it exists. Nothing is lost on any path: if init fails
2170
+ * the buffer is drained raw, and if it overflows it is drained raw in order.
2084
2171
  *
2085
2172
  * @param payload — Newline-terminated JSON entry, UTF-8 encoded.
2086
2173
  */
@@ -2092,7 +2179,36 @@ var PrettyDevDestination = class {
2092
2179
  if (this.initFailed) {
2093
2180
  return;
2094
2181
  }
2095
- process.stdout.write(payload);
2182
+ if (this.bufferOverflowed) {
2183
+ writeRawToStdout(payload);
2184
+ return;
2185
+ }
2186
+ const payloadBytes = Buffer.byteLength(payload, "utf8");
2187
+ if (this.buffer.length >= MAX_BUFFERED_ENTRIES || this.bufferedBytes + payloadBytes > MAX_BUFFERED_BYTES) {
2188
+ this.bufferOverflowed = true;
2189
+ this.flushBuffer(writeRawToStdout);
2190
+ writeRawToStdout(payload);
2191
+ return;
2192
+ }
2193
+ this.buffer.push(payload);
2194
+ this.bufferedBytes += payloadBytes;
2195
+ }
2196
+ /**
2197
+ * Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
2198
+ *
2199
+ * Emptying unconditionally is what makes this safe to call from both the
2200
+ * success and failure paths: whichever runs first owns the entries, and the
2201
+ * other finds nothing to emit twice.
2202
+ *
2203
+ * @param emit - Receives each buffered payload, oldest first.
2204
+ */
2205
+ flushBuffer(emit) {
2206
+ const held = this.buffer;
2207
+ this.buffer = [];
2208
+ this.bufferedBytes = 0;
2209
+ for (const payload of held) {
2210
+ emit(payload);
2211
+ }
2096
2212
  }
2097
2213
  /**
2098
2214
  * Flush and close the pretty transform, resolving once the stream has ended.
@@ -2101,6 +2217,7 @@ var PrettyDevDestination = class {
2101
2217
  async onShutdown() {
2102
2218
  const stream = this.stream;
2103
2219
  if (stream === void 0) {
2220
+ this.flushBuffer(writeRawToStdout);
2104
2221
  return;
2105
2222
  }
2106
2223
  await new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
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",