@bymax-one/nest-logger 1.1.0 → 1.2.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.
@@ -64,6 +64,15 @@ function applyDefaults(options) {
64
64
  isGlobal: options.isGlobal ?? true,
65
65
  shouldUseAsNestLogger: options.shouldUseAsNestLogger ?? true,
66
66
  redactPaths: Object.freeze([...options.redactPaths ?? []]),
67
+ redactStrategy: options.redactStrategy ?? "names",
68
+ // `'nested'` keeps the shape every existing consumer already queries.
69
+ resourceFormat: options.resourceFormat ?? "nested",
70
+ // `??` rather than `||`: `false` is a meaningful value here (emit nothing)
71
+ // and `||` would silently replace it with the default.
72
+ eventNameField: options.eventNameField ?? "event.name",
73
+ // `'pino'` by default: the semconv attributes are additive value, but the
74
+ // legacy shape is what existing dashboards and alerts already query.
75
+ errorFormat: options.errorFormat ?? "pino",
67
76
  redactCensor: options.redactCensor ?? "[REDACTED]",
68
77
  shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
69
78
  destinations: Object.freeze([...options.destinations ?? []]),
@@ -89,7 +98,7 @@ var PINO_LEVEL_NUMBERS = Object.freeze({
89
98
  debug: 20,
90
99
  trace: 10
91
100
  });
92
- Object.freeze(
101
+ var PINO_LEVEL_NAMES = Object.freeze(
93
102
  Object.fromEntries(
94
103
  Object.entries(PINO_LEVEL_NUMBERS).map(([name, num]) => [num, name])
95
104
  // keys of PINO_LEVEL_NUMBERS are always LogLevel
@@ -126,16 +135,143 @@ function validateOptions(options) {
126
135
  if (options.maxEntrySizeBytes !== void 0 && options.maxEntrySizeBytes <= 0) {
127
136
  throw new Error("[BymaxLoggerModule] options.maxEntrySizeBytes must be > 0");
128
137
  }
138
+ if (options.redactStrategy !== void 0 && options.redactStrategy !== "names" && options.redactStrategy !== "paths") {
139
+ throw new Error(
140
+ `[BymaxLoggerModule] options.redactStrategy must be 'names' or 'paths'. Got: ${String(
141
+ options.redactStrategy
142
+ )}`
143
+ );
144
+ }
145
+ if (options.resourceFormat !== void 0 && options.resourceFormat !== "nested" && options.resourceFormat !== "flat") {
146
+ throw new Error(
147
+ `[BymaxLoggerModule] options.resourceFormat must be 'nested' or 'flat'. Got: ${String(
148
+ options.resourceFormat
149
+ )}`
150
+ );
151
+ }
152
+ if (options.errorFormat !== void 0 && options.errorFormat !== "pino" && options.errorFormat !== "semconv" && options.errorFormat !== "both") {
153
+ throw new Error(
154
+ `[BymaxLoggerModule] options.errorFormat must be 'pino', 'semconv' or 'both'. Got: ${String(
155
+ options.errorFormat
156
+ )}`
157
+ );
158
+ }
159
+ if (options.eventNameField !== void 0 && options.eventNameField !== false && (typeof options.eventNameField !== "string" || options.eventNameField.length === 0)) {
160
+ throw new Error(
161
+ `[BymaxLoggerModule] options.eventNameField must be a non-empty string or false. Got: ${String(
162
+ options.eventNameField
163
+ )}`
164
+ );
165
+ }
129
166
  }
130
167
 
131
168
  // src/server/constants/injection-tokens.constants.ts
132
169
  var LOGGER_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_OPTIONS");
133
170
  var LOGGER_PINO_INSTANCE_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_PINO_INSTANCE");
134
171
  var LOGGER_DESTINATIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_DESTINATIONS");
172
+ var LOGGER_REDACTOR_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_REDACTOR");
135
173
  var LOG_CONTEXT_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_LOG_CONTEXT");
174
+
175
+ // src/shared/constants/reserved-log-keys.constants.ts
176
+ var RESERVED_LOG_KEYS = Object.freeze({
177
+ LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK",
178
+ LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING",
179
+ LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK",
180
+ HTTP_REQUEST_START: "HTTP_REQUEST_START",
181
+ HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS",
182
+ HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT",
183
+ HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR",
184
+ HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR",
185
+ HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED",
186
+ HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
187
+ HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
188
+ HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
189
+ METHOD_EXECUTION: "METHOD_EXECUTION",
190
+ METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
191
+ LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
192
+ LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
193
+ LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
194
+ LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
195
+ });
196
+ var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
197
+ /**
198
+ * Superseded before it was ever used. The request lifecycle terminates in one
199
+ * of `HTTP_REQUEST_SUCCESS` / `_REDIRECT` / `_CLIENT_ERROR` / `_SERVER_ERROR`,
200
+ * each carrying the same `duration` a generic "completed" entry would. Emitting
201
+ * it as well would double the terminal access-log volume to say nothing new.
202
+ * It stays claimed so an application cannot repurpose a name that reads like a
203
+ * library event.
204
+ */
205
+ HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys"
206
+ });
207
+
208
+ // src/server/constants/prototype-polluting-keys.constants.ts
209
+ var PROTOTYPE_POLLUTING_KEYS = /* @__PURE__ */ new Set([
210
+ "__proto__",
211
+ "constructor",
212
+ "prototype"
213
+ ]);
214
+
215
+ // src/server/utils/escape-log-text.util.ts
216
+ var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\r", "\n", "\x85", "\u2028", "\u2029"]);
217
+ var UNSAFE_IN_SINGLE_LINE = /[\u0000-\u0008\u000A-\u001F\u007F-\u009F\u2028\u2029]/g;
218
+ var UNSAFE_IN_MULTILINE = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u2028\u2029]/g;
219
+ function toEscapeSequence(char) {
220
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
221
+ }
222
+ function toSingleLineMessage(text) {
223
+ return text.replace(
224
+ UNSAFE_IN_SINGLE_LINE,
225
+ (char) => LINE_TERMINATORS.has(char) ? "\\n" : toEscapeSequence(char)
226
+ );
227
+ }
228
+ function escapeControlCharacters(text) {
229
+ return text.replace(UNSAFE_IN_MULTILINE, toEscapeSequence);
230
+ }
231
+
232
+ // src/server/services/pino-logger.service.ts
233
+ var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
234
+ function withoutOwnedKeys(metadata) {
235
+ if (metadata === void 0) {
236
+ return {};
237
+ }
238
+ try {
239
+ const safe = {};
240
+ for (const key of Object.keys(metadata)) {
241
+ if (!OWNED_PAYLOAD_KEYS.includes(key) && !PROTOTYPE_POLLUTING_KEYS.has(key)) {
242
+ Reflect.set(safe, key, Reflect.get(metadata, key));
243
+ }
244
+ }
245
+ return safe;
246
+ } catch {
247
+ return {
248
+ _redactionFailed: true,
249
+ _logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
250
+ };
251
+ }
252
+ }
253
+ function safeErrorMessage(error) {
254
+ try {
255
+ return toSingleLineMessage(String(error.message));
256
+ } catch {
257
+ return "Unreadable error message";
258
+ }
259
+ }
260
+ function assignIfDefined(payload, key, value) {
261
+ if (value !== void 0) {
262
+ Reflect.defineProperty(payload, key, { value, enumerable: true });
263
+ }
264
+ }
136
265
  exports.PinoLoggerService = class PinoLoggerService {
137
- constructor(pino2) {
266
+ /**
267
+ * @param pino - The wrapped Pino instance.
268
+ * @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
269
+ * Defaults to the identity so a hand-built instance (benchmarks, unit tests)
270
+ * still works; the module always injects the configured one.
271
+ */
272
+ constructor(pino2, redact = (value) => value) {
138
273
  this.pino = pino2;
274
+ this.redact = redact;
139
275
  }
140
276
  // ─── NestJS LoggerService variadic interface ──────────────────────────────
141
277
  /**
@@ -158,19 +294,20 @@ exports.PinoLoggerService = class PinoLoggerService {
158
294
  */
159
295
  error(message, ...optionalParams) {
160
296
  if (message instanceof Error) {
161
- this.pino.error(
162
- { context: this.resolveContext(optionalParams), err: this.serializeError(message) },
163
- message.message
164
- );
297
+ const payload2 = { err: message };
298
+ assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
299
+ this.pino.error(payload2, safeErrorMessage(message));
165
300
  return;
166
301
  }
167
302
  const stack = typeof optionalParams[0] === "string" ? optionalParams[0] : void 0;
168
303
  const context = typeof optionalParams[1] === "string" ? optionalParams[1] : this.context;
169
- const payload = { context };
170
- if (stack !== void 0) {
171
- payload["stack"] = stack;
172
- }
173
- this.pino.error(payload, typeof message === "string" ? message : String(message));
304
+ const payload = {};
305
+ assignIfDefined(payload, "context", context);
306
+ assignIfDefined(payload, "stack", stack);
307
+ this.pino.error(
308
+ payload,
309
+ toSingleLineMessage(typeof message === "string" ? message : String(message))
310
+ );
174
311
  }
175
312
  /**
176
313
  * NestJS `warn` (warn level).
@@ -244,10 +381,19 @@ exports.PinoLoggerService = class PinoLoggerService {
244
381
  * @param metadata - Optional extra structured fields.
245
382
  */
246
383
  errorStructured(logKey, error, userId, metadata) {
247
- this.pino.error(
248
- { ...metadata, logKey, userId, context: this.context, err: this.serializeError(error) },
249
- error.message
250
- );
384
+ const payload = {
385
+ ...withoutOwnedKeys(metadata),
386
+ logKey,
387
+ // The RAW value, not a pre-serialized copy. Pre-serializing was what let
388
+ // Pino's standard `err` serializer re-derive `type` from the resulting
389
+ // plain object's constructor and emit `"Object"` for every typed
390
+ // exception — and it discarded the cause chain on the way. The configured
391
+ // serializer owns the shape now.
392
+ err: error
393
+ };
394
+ assignIfDefined(payload, "userId", userId);
395
+ assignIfDefined(payload, "context", this.context);
396
+ this.pino.error(payload, safeErrorMessage(error));
251
397
  }
252
398
  // ─── Helpers / escape hatches ─────────────────────────────────────────────
253
399
  /**
@@ -280,7 +426,8 @@ exports.PinoLoggerService = class PinoLoggerService {
280
426
  * @returns A new `PinoLoggerService` wrapping the Pino child.
281
427
  */
282
428
  child(bindings) {
283
- const childService = new exports.PinoLoggerService(this.pino.child(bindings));
429
+ const safeBindings = this.redact(bindings, true);
430
+ const childService = new exports.PinoLoggerService(this.pino.child(safeBindings), this.redact);
284
431
  if (this.context !== void 0) {
285
432
  childService.setContext(this.context);
286
433
  }
@@ -294,19 +441,6 @@ exports.PinoLoggerService = class PinoLoggerService {
294
441
  onApplicationShutdown() {
295
442
  }
296
443
  // ─── Private ──────────────────────────────────────────────────────────────
297
- /**
298
- * Serialize an Error into a plain, log-safe object matching Pino's built-in
299
- * `err` serializer shape (`type`, `message`, `stack`).
300
- *
301
- * Reads `error.name` (NOT `error.constructor.name`) for `type`: native
302
- * subclasses set `name` to their class name, and a sanitized plain-object error
303
- * (from `sanitizeError`) carries the real class name on `name` — whereas its
304
- * `constructor.name` would be the unhelpful `'Object'`, breaking `err.type`
305
- * dashboards for every exception logged through `HttpExceptionFilter`.
306
- */
307
- serializeError(error) {
308
- return { type: error.name, message: error.message, stack: error.stack };
309
- }
310
444
  /** Resolve the context: last string param wins, else the instance context. */
311
445
  resolveContext(optionalParams) {
312
446
  const last = optionalParams[optionalParams.length - 1];
@@ -314,22 +448,21 @@ exports.PinoLoggerService = class PinoLoggerService {
314
448
  }
315
449
  /** Emit a structured payload at info or warn level. */
316
450
  emitStructured(level, logKey, message, userId, metadata) {
317
- const payload = {
318
- ...metadata,
319
- logKey,
320
- userId,
321
- context: this.context
322
- };
451
+ const payload = { ...withoutOwnedKeys(metadata), logKey };
452
+ assignIfDefined(payload, "userId", userId);
453
+ assignIfDefined(payload, "context", this.context);
454
+ const line = toSingleLineMessage(message);
323
455
  if (level === "info") {
324
- this.pino.info(payload, message);
456
+ this.pino.info(payload, line);
325
457
  } else {
326
- this.pino.warn(payload, message);
458
+ this.pino.warn(payload, line);
327
459
  }
328
460
  }
329
461
  /** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
330
462
  emitNestStyle(level, message, optionalParams) {
331
- const payload = { context: this.resolveContext(optionalParams) };
332
- const msg = typeof message === "string" ? message : String(message);
463
+ const payload = {};
464
+ assignIfDefined(payload, "context", this.resolveContext(optionalParams));
465
+ const msg = toSingleLineMessage(typeof message === "string" ? message : String(message));
333
466
  switch (level) {
334
467
  case "info":
335
468
  this.pino.info(payload, msg);
@@ -351,7 +484,8 @@ exports.PinoLoggerService = class PinoLoggerService {
351
484
  };
352
485
  exports.PinoLoggerService = __decorateClass([
353
486
  common.Injectable(),
354
- __decorateParam(0, common.Inject(LOGGER_PINO_INSTANCE_TOKEN))
487
+ __decorateParam(0, common.Inject(LOGGER_PINO_INSTANCE_TOKEN)),
488
+ __decorateParam(1, common.Inject(LOGGER_REDACTOR_TOKEN))
355
489
  ], exports.PinoLoggerService);
356
490
 
357
491
  // src/server/decorators/inject-logger.provider.ts
@@ -401,26 +535,6 @@ exports.DefaultStdoutDestination = __decorateClass([
401
535
  common.Injectable()
402
536
  ], exports.DefaultStdoutDestination);
403
537
 
404
- // src/shared/constants/reserved-log-keys.constants.ts
405
- var RESERVED_LOG_KEYS = Object.freeze({
406
- LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK",
407
- LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING",
408
- LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK",
409
- HTTP_REQUEST_START: "HTTP_REQUEST_START",
410
- HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS",
411
- HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT",
412
- HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR",
413
- HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR",
414
- HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED",
415
- HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
416
- HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
417
- METHOD_EXECUTION: "METHOD_EXECUTION",
418
- METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
419
- LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
420
- LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
421
- LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED"
422
- });
423
-
424
538
  // src/server/utils/normalize-url.util.ts
425
539
  var UUID_REGEX = /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?![0-9a-f])/gi;
426
540
  var ULID_REGEX = /\/[0-9A-HJKMNP-TV-Z]{26}(?![0-9A-HJKMNP-TV-Z])/g;
@@ -443,9 +557,16 @@ function sanitizeError(err, options) {
443
557
  const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
444
558
  return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
445
559
  }
560
+ function isErrorLike(value) {
561
+ if (typeof value !== "object" || value === null) {
562
+ return false;
563
+ }
564
+ const candidate = value;
565
+ return typeof candidate.name === "string" && typeof candidate.message === "string";
566
+ }
446
567
  function toSanitizedError(value, seen, depth2, maxDepth) {
447
568
  try {
448
- if (!(value instanceof Error)) {
569
+ if (!isErrorLike(value)) {
449
570
  return { name: "UnknownError", message: String(value) };
450
571
  }
451
572
  seen.add(value);
@@ -483,7 +604,9 @@ function readAggregatedErrors(value) {
483
604
  return Array.isArray(candidate) ? candidate : void 0;
484
605
  }
485
606
  function scrubStack(stack) {
486
- return stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n");
607
+ return escapeControlCharacters(
608
+ stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
609
+ );
487
610
  }
488
611
  function isObject(value) {
489
612
  return typeof value === "object" && value !== null;
@@ -561,11 +684,39 @@ exports.HttpExceptionFilter = __decorateClass([
561
684
  common.Catch(),
562
685
  __decorateParam(0, common.Inject(exports.PinoLoggerService))
563
686
  ], exports.HttpExceptionFilter);
564
- var HTTP_SUCCESS_MIN = 200;
687
+
688
+ // src/server/utils/http-log-state.util.ts
565
689
  var HTTP_REDIRECT_MIN = 300;
566
690
  var HTTP_CLIENT_ERROR_MIN = 400;
567
691
  var HTTP_SERVER_ERROR_MIN = 500;
568
692
  var UNKNOWN_USER_AGENT = "unknown";
693
+ var RECORDER_ACTIVE = /* @__PURE__ */ Symbol("bymax-one/nest-logger:recorder");
694
+ var RECORDED_ERROR = /* @__PURE__ */ Symbol("bymax-one/nest-logger:error");
695
+ function markRecorderActive(req) {
696
+ Reflect.set(req, RECORDER_ACTIVE, true);
697
+ }
698
+ function isRecorderActive(req) {
699
+ return Reflect.get(req, RECORDER_ACTIVE) === true;
700
+ }
701
+ function recordError(req, error) {
702
+ Reflect.set(req, RECORDED_ERROR, { error });
703
+ }
704
+ function readRecordedError(req) {
705
+ return Reflect.get(req, RECORDED_ERROR);
706
+ }
707
+ function toError(thrown) {
708
+ return thrown instanceof Error ? thrown : new Error(String(thrown));
709
+ }
710
+ function readUserId(req) {
711
+ return req.user?.sub ?? req.user?.id;
712
+ }
713
+ function readUserAgent(req) {
714
+ const raw = req.headers["user-agent"];
715
+ return typeof raw === "string" ? raw : UNKNOWN_USER_AGENT;
716
+ }
717
+
718
+ // src/server/interceptors/http-logging.interceptor.ts
719
+ var HTTP_SUCCESS_MIN = 200;
569
720
  exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
570
721
  /**
571
722
  * @param logger - The structured logger the lifecycle entries are written to.
@@ -599,13 +750,15 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
599
750
  const http = context.switchToHttp();
600
751
  const req = http.getRequest();
601
752
  const res = http.getResponse();
753
+ if (isRecorderActive(req)) {
754
+ return this.recordOnly(req, next);
755
+ }
602
756
  const { method, url, ip } = req;
603
757
  if (this.isExcluded(stripQueryString(url))) {
604
758
  return next.handle();
605
759
  }
606
- const rawUserAgent = req.headers["user-agent"];
607
- const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : UNKNOWN_USER_AGENT;
608
- const userId = req.user?.sub ?? req.user?.id;
760
+ const userAgent = readUserAgent(req);
761
+ const userId = readUserId(req);
609
762
  const normalizedUrl = normalizeUrl(url);
610
763
  const start = Date.now();
611
764
  this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
@@ -628,6 +781,28 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
628
781
  })
629
782
  );
630
783
  }
784
+ /**
785
+ * Hand the lifecycle to the access-log middleware, recording only the thrown
786
+ * error for its terminal entry.
787
+ *
788
+ * The interceptor cannot be the recorder: it never runs for a request a guard
789
+ * rejected or one that matched no route. It also cannot judge DELIVERY — at the
790
+ * moment it completes, the response has not been flushed yet, which is why an
791
+ * aborted request used to be logged as the 200 the handler intended. What it
792
+ * uniquely has is the thrown value, so that is all it contributes.
793
+ *
794
+ * @param req - The request whose lifecycle the middleware owns.
795
+ * @param next - The downstream call handler.
796
+ * @returns The response stream, unmodified and with exceptions re-thrown.
797
+ */
798
+ recordOnly(req, next) {
799
+ return next.handle().pipe(
800
+ rxjs.catchError((err) => {
801
+ recordError(req, toError(err));
802
+ return rxjs.throwError(() => err);
803
+ })
804
+ );
805
+ }
631
806
  /**
632
807
  * Log a non-throwing completion: SUCCESS for 2xx, REDIRECT for 3xx, nothing
633
808
  * for any other status (those are surfaced via the error path instead).
@@ -686,12 +861,12 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
686
861
  }
687
862
  return;
688
863
  }
689
- this.logger.errorStructured(
690
- RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR,
691
- err instanceof Error ? err : new Error(String(err)),
692
- userId,
693
- { method, url, statusCode: common.HttpStatus.INTERNAL_SERVER_ERROR, duration }
694
- );
864
+ this.logger.errorStructured(RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR, toError(err), userId, {
865
+ method,
866
+ url,
867
+ statusCode: common.HttpStatus.INTERNAL_SERVER_ERROR,
868
+ duration
869
+ });
695
870
  }
696
871
  };
697
872
  exports.HttpLoggingInterceptor = __decorateClass([
@@ -726,48 +901,6 @@ var {
726
901
  // disabled" without a dead nullish branch.
727
902
  global: extras.isGlobal !== false
728
903
  })).build();
729
- function detectOtelTraceApi() {
730
- try {
731
- const requireFromCwd = module$1.createRequire(path.join(process.cwd(), "noop.cjs"));
732
- const mod = requireFromCwd("@opentelemetry/api");
733
- return mod.trace;
734
- } catch {
735
- return void 0;
736
- }
737
- }
738
- function isValidTraceId(traceId) {
739
- return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
740
- }
741
- function isValidSpanId(spanId) {
742
- return /^[0-9a-f]{16}$/.test(spanId) && !/^0+$/.test(spanId);
743
- }
744
-
745
- // src/server/mixins/trace-context.mixin.ts
746
- function createTraceContextMixin(logContext, opts) {
747
- const traceApi = opts.shouldAutoInjectTraceContext ? detectOtelTraceApi() : void 0;
748
- return function mixin(_mergeObject, _level, _logger) {
749
- const merged = {};
750
- const store = logContext.getStore();
751
- if (store) {
752
- Object.assign(merged, store);
753
- }
754
- if (traceApi) {
755
- const span = traceApi.getActiveSpan();
756
- if (span) {
757
- const ctx = span.spanContext();
758
- if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
759
- Reflect.set(merged, opts.traceIdField, ctx.traceId);
760
- Reflect.set(merged, opts.spanIdField, ctx.spanId);
761
- if (typeof ctx.traceFlags === "number") {
762
- const flagsByte = ctx.traceFlags & 255;
763
- Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
764
- }
765
- }
766
- }
767
- }
768
- return merged;
769
- };
770
- }
771
904
 
772
905
  // src/server/constants/default-redact-paths.constants.ts
773
906
  var REDACT_MAX_DEPTH = 4;
@@ -810,7 +943,20 @@ var REDACT_COMMON_FIELDS = [
810
943
  "signingSecret",
811
944
  "privateKey",
812
945
  // Conservative PII (1) — consumer can disable if logging email is justified
813
- "email"
946
+ "email",
947
+ // Credential-bearing HTTP header names (5). These also appear in
948
+ // {@link REDACT_ABSOLUTE_PATHS}, pinned to the `req.headers` / `res.headers`
949
+ // shapes — which covered ONLY those two shapes. A headers bag logged under any
950
+ // other key (`logger.info(k, m, u, { headers: req.headers })`, one of the most
951
+ // common things written while debugging an integration) wrote the bearer token
952
+ // in clear. Listed here as bare names, they are caught wherever they appear.
953
+ // Node lower-cases inbound header names, so the lower-case spelling is the one
954
+ // that can actually reach a log.
955
+ "authorization",
956
+ "cookie",
957
+ "set-cookie",
958
+ "x-api-key",
959
+ "x-auth-token"
814
960
  ];
815
961
  var REDACT_ABSOLUTE_PATHS = [
816
962
  "req.headers.authorization",
@@ -824,6 +970,51 @@ var DEFAULT_REDACT_PATHS = [
824
970
  ...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
825
971
  ...REDACT_ABSOLUTE_PATHS
826
972
  ];
973
+ function loadTraceApiFrom(anchor) {
974
+ try {
975
+ const requireFrom = module$1.createRequire(anchor());
976
+ const mod = requireFrom("@opentelemetry/api");
977
+ return mod.trace;
978
+ } catch {
979
+ return void 0;
980
+ }
981
+ }
982
+ function detectOtelTraceApi() {
983
+ return loadTraceApiFrom(() => __filename) ?? loadTraceApiFrom(() => path.join(process.cwd(), "noop.cjs"));
984
+ }
985
+ function isValidTraceId(traceId) {
986
+ return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
987
+ }
988
+ function isValidSpanId(spanId) {
989
+ return /^[0-9a-f]{16}$/.test(spanId) && !/^0+$/.test(spanId);
990
+ }
991
+
992
+ // src/server/mixins/trace-context.mixin.ts
993
+ function createTraceContextMixin(logContext, opts) {
994
+ const traceApi = opts.shouldAutoInjectTraceContext ? detectOtelTraceApi() : void 0;
995
+ return function mixin(_mergeObject, _level, _logger) {
996
+ const merged = {};
997
+ const store = logContext.getStore();
998
+ if (store) {
999
+ Object.assign(merged, store);
1000
+ }
1001
+ if (traceApi) {
1002
+ const span = traceApi.getActiveSpan();
1003
+ if (span) {
1004
+ const ctx = span.spanContext();
1005
+ if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
1006
+ Reflect.set(merged, opts.traceIdField, ctx.traceId);
1007
+ Reflect.set(merged, opts.spanIdField, ctx.spanId);
1008
+ if (typeof ctx.traceFlags === "number") {
1009
+ const flagsByte = ctx.traceFlags & 255;
1010
+ Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ return merged;
1016
+ };
1017
+ }
827
1018
 
828
1019
  // src/server/utils/compile-redact-paths.util.ts
829
1020
  function compileRedactPaths(extraPaths, shouldSkipDefaults) {
@@ -872,6 +1063,241 @@ function destinationToStream(destination) {
872
1063
  });
873
1064
  }
874
1065
 
1066
+ // src/server/utils/redact-by-name.util.ts
1067
+ var REDACT_MAX_TRAVERSAL_DEPTH = 100;
1068
+ var REDACT_DEPTH_EXCEEDED = "[REDACTION_DEPTH_EXCEEDED]";
1069
+ var REDACT_CIRCULAR = "[Circular]";
1070
+ function resolveToJson(value) {
1071
+ let getter;
1072
+ let candidate;
1073
+ let current = value;
1074
+ while (current !== null) {
1075
+ const descriptor = Object.getOwnPropertyDescriptor(current, "toJSON");
1076
+ if (descriptor !== void 0) {
1077
+ getter = descriptor.get;
1078
+ candidate = getter === void 0 ? descriptor.value : getter.call(value);
1079
+ break;
1080
+ }
1081
+ current = Object.getPrototypeOf(current);
1082
+ }
1083
+ return {
1084
+ method: typeof candidate === "function" ? candidate : void 0,
1085
+ fromAccessor: getter !== void 0
1086
+ };
1087
+ }
1088
+ function cloneError(error) {
1089
+ const clone = Object.create(
1090
+ Object.getPrototypeOf(error),
1091
+ Object.getOwnPropertyDescriptors(error)
1092
+ );
1093
+ const { stack } = error;
1094
+ if (typeof stack === "string") {
1095
+ Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
1096
+ }
1097
+ return clone;
1098
+ }
1099
+ var BUFFER_TO_JSON = Buffer.prototype.toJSON;
1100
+ var WRITE_FAILED = new Error();
1101
+ function defineOwn(target, key, value) {
1102
+ if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
1103
+ throw WRITE_FAILED;
1104
+ }
1105
+ }
1106
+ function walkArray(value, sensitive, censor, depth2, ancestors) {
1107
+ const length = value.length;
1108
+ const copy = [];
1109
+ for (let index = 0; index < length; index++) {
1110
+ Reflect.set(
1111
+ copy,
1112
+ index,
1113
+ walk(Reflect.get(value, index), sensitive, censor, depth2 + 1, ancestors)
1114
+ );
1115
+ }
1116
+ return copy;
1117
+ }
1118
+ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
1119
+ if (value === null) {
1120
+ return value;
1121
+ }
1122
+ if (typeof value !== "object" && typeof value !== "function") {
1123
+ return value;
1124
+ }
1125
+ if (depth2 > REDACT_MAX_TRAVERSAL_DEPTH) {
1126
+ return REDACT_DEPTH_EXCEEDED;
1127
+ }
1128
+ if (ancestors.has(value)) {
1129
+ return REDACT_CIRCULAR;
1130
+ }
1131
+ const resolvedToJson = resolveToJson(value);
1132
+ if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
1133
+ return value;
1134
+ }
1135
+ ancestors.add(value);
1136
+ try {
1137
+ const toJson = honorToJson ? resolvedToJson.method : void 0;
1138
+ if (toJson !== void 0) {
1139
+ if (Object.keys(value).some((key) => sensitive.has(key.toLowerCase()))) {
1140
+ return censor;
1141
+ }
1142
+ return walk(toJson.call(value), sensitive, censor, depth2 + 1, ancestors);
1143
+ }
1144
+ if (typeof value === "function") {
1145
+ return value;
1146
+ }
1147
+ if (Array.isArray(value)) {
1148
+ return walkArray(value, sensitive, censor, depth2, ancestors);
1149
+ }
1150
+ const source = value;
1151
+ const keys = Object.keys(source);
1152
+ const copy = value instanceof Error ? cloneError(value) : {};
1153
+ for (const key of keys) {
1154
+ const current = Reflect.get(source, key);
1155
+ defineOwn(
1156
+ copy,
1157
+ key,
1158
+ sensitive.has(key.toLowerCase()) ? censor : walk(current, sensitive, censor, depth2 + 1, ancestors)
1159
+ );
1160
+ }
1161
+ return copy;
1162
+ } finally {
1163
+ ancestors.delete(value);
1164
+ }
1165
+ }
1166
+ function createNameRedactor(fieldNames, censor) {
1167
+ const sensitive = new Set(fieldNames.map((name) => name.toLowerCase()));
1168
+ return (value, isRecordRoot = false) => {
1169
+ try {
1170
+ return walk(value, sensitive, censor, 0, /* @__PURE__ */ new Set(), !isRecordRoot);
1171
+ } catch {
1172
+ return {
1173
+ _redactionFailed: true,
1174
+ _logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
1175
+ };
1176
+ }
1177
+ };
1178
+ }
1179
+
1180
+ // src/server/utils/resolve-resource.util.ts
1181
+ var ATTR_NAMESPACE = "service.namespace";
1182
+ var ATTR_VERSION = "service.version";
1183
+ var ATTR_NAME = "service.name";
1184
+ var ATTR_INSTANCE_ID = "service.instance.id";
1185
+ var ATTR_ENVIRONMENT = "deployment.environment.name";
1186
+ function parseResourceAttributes(raw) {
1187
+ const parsed = {};
1188
+ if (raw === void 0) {
1189
+ return parsed;
1190
+ }
1191
+ for (const pair of raw.split(",")) {
1192
+ const separator = pair.indexOf("=");
1193
+ if (separator <= 0) {
1194
+ continue;
1195
+ }
1196
+ const key = pair.slice(0, separator).trim();
1197
+ const rawValue = pair.slice(separator + 1).trim();
1198
+ if (rawValue.length === 0) {
1199
+ continue;
1200
+ }
1201
+ let value;
1202
+ try {
1203
+ value = decodeURIComponent(rawValue);
1204
+ } catch {
1205
+ value = rawValue;
1206
+ }
1207
+ Object.defineProperty(parsed, key, { value, enumerable: true, configurable: true });
1208
+ }
1209
+ return parsed;
1210
+ }
1211
+ function attribute(attributes, key) {
1212
+ const value = Object.hasOwn(attributes, key) ? Reflect.get(attributes, key) : void 0;
1213
+ return typeof value === "string" ? value : void 0;
1214
+ }
1215
+ function firstNonEmpty(...candidates) {
1216
+ const found = candidates.find(
1217
+ (candidate) => typeof candidate === "string" && candidate.length > 0
1218
+ );
1219
+ return found;
1220
+ }
1221
+ function resolveServiceMetadata(service, env = process.env) {
1222
+ const attributes = parseResourceAttributes(env["OTEL_RESOURCE_ATTRIBUTES"]);
1223
+ const resolved = {
1224
+ // The spec's precedence: OTEL_SERVICE_NAME beats service.name in the
1225
+ // attribute list. Explicit configuration still beats both.
1226
+ name: firstNonEmpty(service.name, env["OTEL_SERVICE_NAME"], attribute(attributes, ATTR_NAME)) ?? "",
1227
+ version: firstNonEmpty(service.version, attribute(attributes, ATTR_VERSION)) ?? ""
1228
+ };
1229
+ const namespace = firstNonEmpty(service.namespace, attribute(attributes, ATTR_NAMESPACE));
1230
+ if (namespace !== void 0) {
1231
+ resolved.namespace = namespace;
1232
+ }
1233
+ const instanceId = firstNonEmpty(service.instanceId, attribute(attributes, ATTR_INSTANCE_ID));
1234
+ if (instanceId !== void 0) {
1235
+ resolved.instanceId = instanceId;
1236
+ }
1237
+ const environment = firstNonEmpty(
1238
+ service.environment,
1239
+ attribute(attributes, ATTR_ENVIRONMENT),
1240
+ env["NODE_ENV"]
1241
+ );
1242
+ if (environment !== void 0) {
1243
+ resolved.environment = environment;
1244
+ }
1245
+ return resolved;
1246
+ }
1247
+ function buildResourceBindings(resolved, format, extras = {}) {
1248
+ if (format === "flat") {
1249
+ const flat = {
1250
+ [ATTR_NAME]: resolved.name,
1251
+ [ATTR_VERSION]: resolved.version
1252
+ };
1253
+ for (const [key, value] of Object.entries(extras)) {
1254
+ Reflect.set(flat, `service.${key}`, value);
1255
+ }
1256
+ if (resolved.namespace !== void 0) {
1257
+ Reflect.set(flat, ATTR_NAMESPACE, resolved.namespace);
1258
+ }
1259
+ if (resolved.instanceId !== void 0) {
1260
+ Reflect.set(flat, ATTR_INSTANCE_ID, resolved.instanceId);
1261
+ }
1262
+ if (resolved.environment !== void 0) {
1263
+ Reflect.set(flat, ATTR_ENVIRONMENT, resolved.environment);
1264
+ }
1265
+ return flat;
1266
+ }
1267
+ const service = {
1268
+ ...extras,
1269
+ name: resolved.name,
1270
+ version: resolved.version
1271
+ };
1272
+ if (resolved.namespace !== void 0) {
1273
+ service["namespace"] = resolved.namespace;
1274
+ }
1275
+ if (resolved.instanceId !== void 0) {
1276
+ service["instance"] = { id: resolved.instanceId };
1277
+ }
1278
+ const bindings = { service };
1279
+ if (resolved.environment !== void 0) {
1280
+ bindings["deployment"] = { environment: { name: resolved.environment } };
1281
+ }
1282
+ return bindings;
1283
+ }
1284
+ var OWNED_SERVICE_KEYS = /* @__PURE__ */ new Set([
1285
+ "name",
1286
+ "version",
1287
+ "namespace",
1288
+ "instanceId",
1289
+ "environment"
1290
+ ]);
1291
+ function extraServiceFields(service) {
1292
+ const extras = {};
1293
+ for (const [key, value] of Object.entries({ ...service })) {
1294
+ if (!OWNED_SERVICE_KEYS.has(key)) {
1295
+ Reflect.set(extras, key, value);
1296
+ }
1297
+ }
1298
+ return extras;
1299
+ }
1300
+
875
1301
  // src/server/utils/truncate-large-entries.ts
876
1302
  var PREVIEW_LENGTH = 200;
877
1303
  function createSizeBoundedSerializer(baseSerializer, maxBytes) {
@@ -900,36 +1326,188 @@ function createSizeBoundedSerializer(baseSerializer, maxBytes) {
900
1326
  }
901
1327
 
902
1328
  // src/server/pino-factory.ts
1329
+ var PATH_SEGMENT = /\[\s*['"]([^'"]+)['"]\s*\]|([^.[\]]+)/g;
1330
+ var INDEX_SEGMENT = /^\d+$/;
1331
+ function leafNameOf(path) {
1332
+ let leaf;
1333
+ for (const match of path.matchAll(PATH_SEGMENT)) {
1334
+ const quoted = match[1];
1335
+ const name = quoted ?? (match[2] !== void 0 && INDEX_SEGMENT.test(match[2]) ? void 0 : match[2]);
1336
+ if (name !== void 0 && name !== "*") {
1337
+ leaf = name;
1338
+ }
1339
+ }
1340
+ return leaf;
1341
+ }
1342
+ var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
1343
+ "name",
1344
+ "message",
1345
+ "stack",
1346
+ "cause",
1347
+ "errors"
1348
+ ]);
1349
+ function serializeErrorValue(value_) {
1350
+ const sanitized = sanitizeError(value_);
1351
+ const serialized = {
1352
+ // `type` rather than `name`: the field consumers already query.
1353
+ type: sanitized.name,
1354
+ message: sanitized.message
1355
+ };
1356
+ if (sanitized.stack !== void 0) {
1357
+ serialized["stack"] = sanitized.stack;
1358
+ }
1359
+ if (sanitized.cause !== void 0) {
1360
+ serialized["cause"] = sanitized.cause;
1361
+ }
1362
+ if (sanitized.errors !== void 0) {
1363
+ serialized["errors"] = sanitized.errors;
1364
+ }
1365
+ try {
1366
+ const source = (
1367
+ // Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
1368
+ typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
1369
+ );
1370
+ for (const [key, value] of Object.entries(source)) {
1371
+ if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
1372
+ Reflect.set(serialized, key, value);
1373
+ }
1374
+ }
1375
+ } catch {
1376
+ }
1377
+ return serialized;
1378
+ }
1379
+ function withSemconvException(record, format) {
1380
+ if (format === "pino") {
1381
+ return record;
1382
+ }
1383
+ try {
1384
+ const candidate = record["err"];
1385
+ if (typeof candidate?.name === "string") {
1386
+ record["exception.type"] = candidate.name;
1387
+ record["error.type"] = candidate.name;
1388
+ }
1389
+ if (typeof candidate?.message === "string") {
1390
+ record["exception.message"] = candidate.message;
1391
+ }
1392
+ if (typeof candidate?.stack === "string") {
1393
+ record["exception.stacktrace"] = scrubStack(candidate.stack);
1394
+ }
1395
+ if (format === "semconv") {
1396
+ const withoutErr = {};
1397
+ for (const [key, value] of Object.entries(record)) {
1398
+ if (key !== "err") {
1399
+ Reflect.set(withoutErr, key, value);
1400
+ }
1401
+ }
1402
+ return withoutErr;
1403
+ }
1404
+ } catch {
1405
+ }
1406
+ return record;
1407
+ }
1408
+ function toEventName(logKey) {
1409
+ return logKey.toLowerCase().replaceAll("_", ".");
1410
+ }
1411
+ function withEventName(record, field) {
1412
+ if (field === false) {
1413
+ return record;
1414
+ }
1415
+ const logKey = record["logKey"];
1416
+ if (typeof logKey === "string" && logKey.length > 0) {
1417
+ Reflect.set(record, field, toEventName(logKey));
1418
+ }
1419
+ return record;
1420
+ }
1421
+ var PASS_THROUGH = (value) => value;
1422
+ function resolveNameRedactor(options) {
1423
+ if (options.redactStrategy !== "names") {
1424
+ return PASS_THROUGH;
1425
+ }
1426
+ const names = [
1427
+ ...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
1428
+ ...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
1429
+ ];
1430
+ if (names.length === 0) {
1431
+ return PASS_THROUGH;
1432
+ }
1433
+ return createNameRedactor(names, options.redactCensor);
1434
+ }
1435
+ function resolveRedactOption(options) {
1436
+ const paths = options.redactStrategy === "paths" ? compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact) : [...new Set(options.redactPaths)];
1437
+ if (paths.length === 0) {
1438
+ return void 0;
1439
+ }
1440
+ return { paths, censor: options.redactCensor };
1441
+ }
903
1442
  function buildPinoInstance(options, logContext, destinations) {
904
1443
  const maxBytes = options.maxEntrySizeBytes;
1444
+ const redact = resolveNameRedactor(options);
1445
+ const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
905
1446
  const serializers = {
906
- err: createSizeBoundedSerializer(pino__default.default.stdSerializers.err, maxBytes),
1447
+ err: bound(serializeErrorValue),
907
1448
  ...Object.fromEntries(
908
1449
  Object.entries(options.serializers).map(
909
- ([key, serializer]) => [
910
- key,
911
- createSizeBoundedSerializer(serializer, maxBytes)
912
- ]
1450
+ ([key, serializer]) => [key, bound(serializer)]
913
1451
  )
914
1452
  )
915
1453
  };
1454
+ const redactOption = resolveRedactOption(options);
916
1455
  const pinoOptions = {
917
1456
  level: options.level,
918
- redact: {
919
- paths: compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact),
920
- censor: options.redactCensor
921
- },
922
- base: { service: options.service },
1457
+ ...redactOption === void 0 ? {} : { redact: redactOption },
1458
+ // Resource identity, resolved ONCE here rather than per entry: Pino
1459
+ // pre-serializes `base` into the instance's chindings, so the cost of the
1460
+ // extra attributes is one string built at construction, not work per log.
1461
+ base: buildResourceBindings(
1462
+ resolveServiceMetadata(options.service),
1463
+ options.resourceFormat,
1464
+ extraServiceFields(options.service)
1465
+ ),
923
1466
  // Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
924
1467
  timestamp: () => `,"time":"${options.timestamp()}"`,
925
1468
  formatters: {
926
1469
  // Emit the level as a string label instead of the numeric code — easier
927
1470
  // for log aggregators. Trace context is injected via `mixin`, never via
928
1471
  // `formatters.log` (which cannot see ambient ALS / OTel state).
929
- level: (label) => ({ level: label })
1472
+ level: (label) => ({ level: label }),
1473
+ // `base` bindings never reach `formatters.log` — Pino pre-serializes them
1474
+ // into the instance's `chindings`, the same reason `child()` has to redact
1475
+ // its own. That matters because `service` is CONSUMER-supplied and
1476
+ // `applyDefaults` keeps whatever it was given: a `{ name, version, apiKey }`
1477
+ // went out in clear, where the path expansion had covered it via `*.*.apiKey`.
1478
+ // Redacting here rather than trimming to the two declared fields keeps a
1479
+ // consumer's extra metadata instead of silently dropping it. Runs ONCE, at
1480
+ // logger construction — no per-entry cost. `true`: bindings are a record
1481
+ // root, iterated rather than serialized.
1482
+ bindings: (bindings) => redact(bindings, true),
1483
+ // The name-walk redactor runs here, on the merged record (mixin output +
1484
+ // the caller's object), which is exactly the surface a caller controls.
1485
+ // `base` and child bindings are NOT visible at this hook — they are
1486
+ // library-owned (`service`, the `@InjectLogger` context) and carry nothing
1487
+ // sensitive, which `pino-factory.spec.ts` pins.
1488
+ log: (record) => withSemconvException(
1489
+ withEventName(redact(record, true), options.eventNameField),
1490
+ options.errorFormat
1491
+ )
930
1492
  },
931
1493
  serializers,
932
- mixin: createTraceContextMixin(logContext, options.otel)
1494
+ mixin: createTraceContextMixin(logContext, options.otel),
1495
+ // `formatters.log` is NOT the first code to touch the caller's object: Pino
1496
+ // merges the mixin result with it first, and the default strategy's
1497
+ // `Object.assign` invokes every own getter. A throwing getter therefore
1498
+ // crashed the log call before the redactor could return its fail-closed
1499
+ // envelope. Owning the merge moves that boundary inside the guarantee.
1500
+ mixinMergeStrategy: (mergeObject, mixinObject) => {
1501
+ try {
1502
+ return Object.assign({}, mixinObject, mergeObject);
1503
+ } catch {
1504
+ return {
1505
+ ...mixinObject,
1506
+ _redactionFailed: true,
1507
+ _logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
1508
+ };
1509
+ }
1510
+ }
933
1511
  };
934
1512
  const streams = destinations.map((destination) => ({
935
1513
  level: destination.minLevel ?? options.level,
@@ -945,9 +1523,10 @@ var DestinationRegistry = class {
945
1523
  * @param logger - Structured logger used to report init failures. Injected by
946
1524
  * token explicitly for the same bundling reason.
947
1525
  */
948
- constructor(registered, logger) {
1526
+ constructor(registered, logger, options) {
949
1527
  this.registered = registered;
950
1528
  this.logger = logger;
1529
+ this.options = options;
951
1530
  /**
952
1531
  * Destinations that initialized successfully, in registration order.
953
1532
  *
@@ -979,6 +1558,43 @@ var DestinationRegistry = class {
979
1558
  );
980
1559
  }
981
1560
  }
1561
+ this.announceBootstrap();
1562
+ }
1563
+ /**
1564
+ * Emit the one-shot bootstrap entries, AFTER every destination has been
1565
+ * initialized.
1566
+ *
1567
+ * Ordering is the whole point of doing this here rather than from an eagerly
1568
+ * instantiated provider factory: a provider factory runs while Nest is still
1569
+ * building the graph, before `onModuleInit`, so a sink that only accepts
1570
+ * writes once its own `onInit()` has run would drop these entries — including
1571
+ * `LOGGER_BOOTSTRAP_WARNING`, which exists precisely so a security review can
1572
+ * see that PII redaction was turned off. A signal that a custom destination
1573
+ * can silently swallow is not an audit trail. Emitting from the registry that
1574
+ * owns destination initialization makes the ordering structural instead of a
1575
+ * cross-provider hook-order assumption.
1576
+ */
1577
+ announceBootstrap() {
1578
+ this.logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
1579
+ if (this.options.shouldDisableDefaultRedact) {
1580
+ this.logger.warnStructured(
1581
+ RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_WARNING,
1582
+ "Default PII redaction is DISABLED \u2014 sensitive fields will be logged verbatim unless every one of them is listed in options.redactPaths",
1583
+ void 0,
1584
+ {
1585
+ shouldDisableDefaultRedact: true,
1586
+ redactPathCount: this.options.redactPaths.length
1587
+ }
1588
+ );
1589
+ }
1590
+ if (this.options.otel.shouldAutoInjectTraceContext && detectOtelTraceApi() === void 0) {
1591
+ this.logger.warnStructured(
1592
+ RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_WARNING,
1593
+ "Trace-context auto-injection is enabled but @opentelemetry/api could not be resolved \u2014 traceId and spanId will be absent from every entry",
1594
+ void 0,
1595
+ { reason: "OTEL_API_UNAVAILABLE", shouldAutoInjectTraceContext: true }
1596
+ );
1597
+ }
982
1598
  }
983
1599
  /**
984
1600
  * Flush and close active destinations in REVERSE registration order, so a
@@ -987,6 +1603,15 @@ var DestinationRegistry = class {
987
1603
  * point in the shutdown sequence.
988
1604
  */
989
1605
  async onApplicationShutdown() {
1606
+ this.logger.info(
1607
+ RESERVED_LOG_KEYS.LOGGER_SHUTDOWN_OK,
1608
+ "BymaxLoggerModule shutting down",
1609
+ void 0,
1610
+ { destinations: this.active.length }
1611
+ );
1612
+ await new Promise((resolve) => {
1613
+ setImmediate(resolve);
1614
+ });
990
1615
  for (const destination of [...this.active].reverse()) {
991
1616
  try {
992
1617
  await destination.onShutdown?.();
@@ -1011,9 +1636,10 @@ var DestinationRegistry = class {
1011
1636
  DestinationRegistry = __decorateClass([
1012
1637
  common.Injectable(),
1013
1638
  __decorateParam(0, common.Inject(LOGGER_DESTINATIONS_TOKEN)),
1014
- __decorateParam(1, common.Inject(exports.PinoLoggerService))
1639
+ __decorateParam(1, common.Inject(exports.PinoLoggerService)),
1640
+ __decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
1015
1641
  ], DestinationRegistry);
1016
- var FORBIDDEN_CONTEXT_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1642
+ var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
1017
1643
  exports.LogContextService = class LogContextService {
1018
1644
  constructor() {
1019
1645
  this.als = new async_hooks.AsyncLocalStorage();
@@ -1099,24 +1725,21 @@ exports.LogContextService = __decorateClass([
1099
1725
  ], exports.LogContextService);
1100
1726
 
1101
1727
  // src/server/logger.module.ts
1102
- var LOGGER_BOOTSTRAP_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_BOOTSTRAP");
1103
1728
  function resolveDestinations(options) {
1104
1729
  return options.destinations.length > 0 ? options.destinations : [new exports.DefaultStdoutDestination()];
1105
1730
  }
1106
- function bootstrapProvider() {
1107
- return {
1108
- provide: LOGGER_BOOTSTRAP_TOKEN,
1109
- useFactory: (logger) => {
1110
- logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
1111
- return true;
1112
- },
1113
- inject: [exports.PinoLoggerService]
1114
- };
1115
- }
1116
1731
  function buildCommonProviders() {
1117
1732
  return [
1118
1733
  exports.LogContextService,
1119
1734
  { provide: LOG_CONTEXT_TOKEN, useExisting: exports.LogContextService },
1735
+ {
1736
+ // The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
1737
+ // `PinoLoggerService.child()`. The latter needs it as a provider because
1738
+ // Pino pre-serializes child bindings before `formatters.log` can see them.
1739
+ provide: LOGGER_REDACTOR_TOKEN,
1740
+ useFactory: (options) => resolveNameRedactor(options),
1741
+ inject: [LOGGER_OPTIONS_TOKEN]
1742
+ },
1120
1743
  {
1121
1744
  provide: LOGGER_PINO_INSTANCE_TOKEN,
1122
1745
  useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
@@ -1132,8 +1755,7 @@ function buildCommonProviders() {
1132
1755
  // exported — consumers never inject it directly.
1133
1756
  DestinationRegistry,
1134
1757
  // One child-logger provider per context discovered from @InjectLogger(context).
1135
- ...collectContextLoggerProviders(),
1136
- bootstrapProvider()
1758
+ ...collectContextLoggerProviders()
1137
1759
  ];
1138
1760
  }
1139
1761
  function asyncHttpInterceptorProvider() {
@@ -1153,6 +1775,7 @@ function augmentLoggerModule(base, providers) {
1153
1775
  LOGGER_OPTIONS_TOKEN,
1154
1776
  LOGGER_PINO_INSTANCE_TOKEN,
1155
1777
  LOGGER_DESTINATIONS_TOKEN,
1778
+ LOGGER_REDACTOR_TOKEN,
1156
1779
  LOG_CONTEXT_TOKEN,
1157
1780
  exports.LogContextService,
1158
1781
  exports.PinoLoggerService,
@@ -1343,6 +1966,126 @@ var PrettyDevDestination = class {
1343
1966
  });
1344
1967
  }
1345
1968
  };
1969
+ var HttpAccessLogMiddleware = class {
1970
+ /**
1971
+ * @param logger - The structured logger the access entries are written to.
1972
+ * Injected by token explicitly: the package is bundled without
1973
+ * `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
1974
+ * @param options - Resolved module options (read for `http.excludePaths`).
1975
+ */
1976
+ constructor(logger, logContext, options) {
1977
+ this.logger = logger;
1978
+ this.logContext = logContext;
1979
+ this.isEnabled = options.http.isEnabled;
1980
+ this.excludePaths = options.http.excludePaths;
1981
+ }
1982
+ /**
1983
+ * Emit START, arm the terminal entry on `'close'`, and continue the chain.
1984
+ *
1985
+ * @param req - The incoming request (Express satisfies the contract).
1986
+ * @param res - The outgoing response (Express satisfies the contract).
1987
+ * @param next - The next handler in the chain.
1988
+ */
1989
+ use(req, res, next) {
1990
+ if (!this.isEnabled) {
1991
+ next();
1992
+ return;
1993
+ }
1994
+ const target = req.originalUrl ?? req.url;
1995
+ const path = stripQueryString(target);
1996
+ if (this.excludePaths.some((pattern) => pattern.test(path))) {
1997
+ next();
1998
+ return;
1999
+ }
2000
+ markRecorderActive(req);
2001
+ const method = req.method;
2002
+ const url = normalizeUrl(target);
2003
+ const start = Date.now();
2004
+ this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${url}`, readUserId(req), {
2005
+ method,
2006
+ url,
2007
+ // Query string stripped: it may carry secrets (reset tokens, OAuth codes)
2008
+ // that no key-name redaction can scrub out of a string VALUE.
2009
+ fullUrl: path,
2010
+ // Conventional for an access log and personal data under GDPR/LGPD. Its
2011
+ // fidelity is the app's to establish: behind a proxy it is only the real
2012
+ // client when the app configures `trust proxy`, and a value the app has not
2013
+ // established is attacker-chosen. Suppress with `redactPaths: ['ip']`.
2014
+ ip: req.ip,
2015
+ userAgent: readUserAgent(req)
2016
+ });
2017
+ const emitTerminal = () => {
2018
+ this.logTerminal(req, res, method, url, Date.now() - start);
2019
+ };
2020
+ const bound = async_hooks.AsyncResource.bind(emitTerminal);
2021
+ res.on("close", () => {
2022
+ if (this.logContext.getStore() !== void 0) {
2023
+ emitTerminal();
2024
+ } else {
2025
+ bound();
2026
+ }
2027
+ });
2028
+ next();
2029
+ }
2030
+ /**
2031
+ * Emit the terminal entry for a closed request.
2032
+ *
2033
+ * `userId` is read HERE rather than at START because authentication runs in a
2034
+ * guard, downstream of this middleware — at START there is no principal yet.
2035
+ *
2036
+ * @param req - The request, carrying any error the interceptor recorded.
2037
+ * @param res - The closed response.
2038
+ * @param method - The HTTP method.
2039
+ * @param url - The normalized request URL.
2040
+ * @param duration - Elapsed milliseconds.
2041
+ */
2042
+ logTerminal(req, res, method, url, duration) {
2043
+ const statusCode = res.statusCode;
2044
+ const userId = readUserId(req);
2045
+ const meta = { method, url, statusCode, duration };
2046
+ if (!res.writableFinished) {
2047
+ this.logger.warnStructured(
2048
+ RESERVED_LOG_KEYS.HTTP_REQUEST_ABORTED,
2049
+ `${method} ${url} \u2192 ${statusCode} not delivered (${duration}ms)`,
2050
+ userId,
2051
+ meta
2052
+ );
2053
+ return;
2054
+ }
2055
+ if (statusCode >= HTTP_SERVER_ERROR_MIN) {
2056
+ const recorded = readRecordedError(req);
2057
+ this.logger.errorStructured(
2058
+ RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR,
2059
+ recorded?.error ?? new Error(`${method} ${url} \u2192 ${statusCode}`),
2060
+ userId,
2061
+ meta
2062
+ );
2063
+ return;
2064
+ }
2065
+ if (statusCode >= HTTP_CLIENT_ERROR_MIN) {
2066
+ const recorded = readRecordedError(req);
2067
+ this.logger.warnStructured(
2068
+ RESERVED_LOG_KEYS.HTTP_REQUEST_CLIENT_ERROR,
2069
+ `${method} ${url} \u2192 ${statusCode}`,
2070
+ userId,
2071
+ recorded === void 0 ? meta : { ...meta, errorMessage: recorded.error.message }
2072
+ );
2073
+ return;
2074
+ }
2075
+ this.logger.info(
2076
+ statusCode >= HTTP_REDIRECT_MIN ? RESERVED_LOG_KEYS.HTTP_REQUEST_REDIRECT : RESERVED_LOG_KEYS.HTTP_REQUEST_SUCCESS,
2077
+ `${method} ${url} \u2192 ${statusCode} (${duration}ms)`,
2078
+ userId,
2079
+ meta
2080
+ );
2081
+ }
2082
+ };
2083
+ HttpAccessLogMiddleware = __decorateClass([
2084
+ common.Injectable(),
2085
+ __decorateParam(0, common.Inject(exports.PinoLoggerService)),
2086
+ __decorateParam(1, common.Inject(exports.LogContextService)),
2087
+ __decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
2088
+ ], HttpAccessLogMiddleware);
1346
2089
  var REQUEST_ID_HEADER = "x-request-id";
1347
2090
  var MAX_CORRELATION_ID_LENGTH = 256;
1348
2091
  var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
@@ -1393,9 +2136,9 @@ exports.RequestIdMiddleware = __decorateClass([
1393
2136
  ], exports.RequestIdMiddleware);
1394
2137
 
1395
2138
  // src/server/middlewares/apply-request-id-middleware.ts
1396
- var ALL_ROUTES = "*";
2139
+ var ALL_ROUTES = "/";
1397
2140
  function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
1398
- consumer.apply(exports.RequestIdMiddleware).forRoutes(routes);
2141
+ consumer.apply(exports.RequestIdMiddleware, HttpAccessLogMiddleware).forRoutes(routes);
1399
2142
  }
1400
2143
  var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
1401
2144
  function InjectLogger(context) {
@@ -1468,6 +2211,9 @@ exports.LOG_CONTEXT_TOKEN = LOG_CONTEXT_TOKEN;
1468
2211
  exports.LOG_KEYS_CONVENTION_REGEX = LOG_KEYS_CONVENTION_REGEX;
1469
2212
  exports.LogContext = LogContext;
1470
2213
  exports.LogPerformance = LogPerformance;
2214
+ exports.PINO_LEVEL_NAMES = PINO_LEVEL_NAMES;
2215
+ exports.PINO_LEVEL_NUMBERS = PINO_LEVEL_NUMBERS;
1471
2216
  exports.PrettyDevDestination = PrettyDevDestination;
1472
2217
  exports.RESERVED_LOG_KEYS = RESERVED_LOG_KEYS;
2218
+ exports.RESERVED_LOG_KEYS_NOT_EMITTED = RESERVED_LOG_KEYS_NOT_EMITTED;
1473
2219
  exports.applyRequestIdMiddleware = applyRequestIdMiddleware;