@bymax-one/nest-logger 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1452 @@
1
+ import { Injectable, Inject, HttpStatus, Catch, ConfigurableModuleBuilder, Module, HttpException, SetMetadata } from '@nestjs/common';
2
+ import { APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
3
+ import { tap, catchError, throwError } from 'rxjs';
4
+ import pino from 'pino';
5
+ import { createRequire } from 'module';
6
+ import { join } from 'path';
7
+ import { Writable } from 'stream';
8
+ import { AsyncLocalStorage } from 'async_hooks';
9
+ import { randomUUID } from 'crypto';
10
+
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __decorateClass = (decorators, target, key, kind) => {
13
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
14
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
15
+ if (decorator = decorators[i])
16
+ result = (decorator(result)) || result;
17
+ return result;
18
+ };
19
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
20
+
21
+ // src/server/config/default-options.ts
22
+ var DEFAULT_HTTP = {
23
+ isEnabled: false,
24
+ shouldCaptureExceptions: true,
25
+ shouldGenerateRequestId: true,
26
+ excludePaths: [/^\/health$/, /^\/metrics$/],
27
+ tenantIdHeader: "x-tenant-id"
28
+ };
29
+ var DEFAULT_OTEL = {
30
+ shouldAutoInjectTraceContext: true,
31
+ fieldFormat: "camelCase",
32
+ traceIdField: "traceId",
33
+ spanIdField: "spanId",
34
+ traceFlagsField: "traceFlags"
35
+ };
36
+ var DEFAULT_MAX_ENTRY_SIZE_BYTES = 65536;
37
+ function applyOtelFieldFormat(merged, user) {
38
+ if (merged.fieldFormat === "snake_case") {
39
+ return {
40
+ ...merged,
41
+ traceIdField: user?.traceIdField ?? "trace_id",
42
+ spanIdField: user?.spanIdField ?? "span_id",
43
+ traceFlagsField: user?.traceFlagsField ?? "trace_flags"
44
+ };
45
+ }
46
+ return merged;
47
+ }
48
+ function applyDefaults(options) {
49
+ const isProduction = process.env["NODE_ENV"] === "production";
50
+ const http = { ...DEFAULT_HTTP, ...options.http ?? {} };
51
+ const otel = applyOtelFieldFormat(
52
+ { ...DEFAULT_OTEL, ...options.otel ?? {} },
53
+ options.otel
54
+ );
55
+ const merged = {
56
+ service: Object.freeze({ ...options.service }),
57
+ level: options.level ?? (isProduction ? "info" : "debug"),
58
+ isGlobal: options.isGlobal ?? true,
59
+ shouldUseAsNestLogger: options.shouldUseAsNestLogger ?? true,
60
+ redactPaths: Object.freeze([...options.redactPaths ?? []]),
61
+ redactCensor: options.redactCensor ?? "[REDACTED]",
62
+ shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
63
+ destinations: Object.freeze([...options.destinations ?? []]),
64
+ isPretty: options.isPretty ?? !isProduction,
65
+ http: Object.freeze({
66
+ ...http,
67
+ excludePaths: Object.freeze([...http.excludePaths])
68
+ }),
69
+ otel: Object.freeze(otel),
70
+ maxEntrySizeBytes: options.maxEntrySizeBytes ?? DEFAULT_MAX_ENTRY_SIZE_BYTES,
71
+ serializers: Object.freeze({ ...options.serializers ?? {} }),
72
+ timestamp: options.timestamp ?? (() => (/* @__PURE__ */ new Date()).toISOString())
73
+ };
74
+ return Object.freeze(merged);
75
+ }
76
+
77
+ // src/server/constants/log-levels.constants.ts
78
+ var PINO_LEVEL_NUMBERS = Object.freeze({
79
+ fatal: 60,
80
+ error: 50,
81
+ warn: 40,
82
+ info: 30,
83
+ debug: 20,
84
+ trace: 10
85
+ });
86
+ Object.freeze(
87
+ Object.fromEntries(
88
+ Object.entries(PINO_LEVEL_NUMBERS).map(([name, num]) => [num, name])
89
+ // keys of PINO_LEVEL_NUMBERS are always LogLevel
90
+ )
91
+ // Object.fromEntries infers Record<string, V>; numeric index is intentional for level lookups
92
+ );
93
+ var LOG_LEVEL_PRIORITY = Object.freeze([
94
+ "trace",
95
+ "debug",
96
+ "info",
97
+ "warn",
98
+ "error",
99
+ "fatal"
100
+ ]);
101
+
102
+ // src/server/config/validate-options.ts
103
+ function validateOptions(options) {
104
+ if (!options.service) {
105
+ throw new Error("[BymaxLoggerModule] options.service is required");
106
+ }
107
+ if (typeof options.service.name !== "string" || options.service.name.trim() === "") {
108
+ throw new Error("[BymaxLoggerModule] options.service.name must be a non-empty string");
109
+ }
110
+ if (typeof options.service.version !== "string" || options.service.version.trim() === "") {
111
+ throw new Error("[BymaxLoggerModule] options.service.version must be a non-empty string");
112
+ }
113
+ if (options.level !== void 0 && !LOG_LEVEL_PRIORITY.includes(options.level)) {
114
+ throw new Error(
115
+ `[BymaxLoggerModule] options.level must be one of: ${LOG_LEVEL_PRIORITY.join(
116
+ ", "
117
+ )}. Got: ${String(options.level)}`
118
+ );
119
+ }
120
+ if (options.maxEntrySizeBytes !== void 0 && options.maxEntrySizeBytes <= 0) {
121
+ throw new Error("[BymaxLoggerModule] options.maxEntrySizeBytes must be > 0");
122
+ }
123
+ }
124
+
125
+ // src/server/constants/injection-tokens.constants.ts
126
+ var LOGGER_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_OPTIONS");
127
+ var LOGGER_PINO_INSTANCE_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_PINO_INSTANCE");
128
+ var LOGGER_DESTINATIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_DESTINATIONS");
129
+ var LOG_CONTEXT_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_LOG_CONTEXT");
130
+ var PinoLoggerService = class {
131
+ constructor(pino2) {
132
+ this.pino = pino2;
133
+ }
134
+ // ─── NestJS LoggerService variadic interface ──────────────────────────────
135
+ /**
136
+ * NestJS `log` (info level). Last string parameter is treated as the context.
137
+ *
138
+ * @param message - The message to log.
139
+ * @param optionalParams - NestJS trailing params (context as last string).
140
+ */
141
+ log(message, ...optionalParams) {
142
+ this.emitNestStyle("info", message, optionalParams);
143
+ }
144
+ /**
145
+ * NestJS `error`. When `message` is an `Error`, routes to the structured
146
+ * error path (serializing `err`). Otherwise follows the NestJS variadic
147
+ * contract `error(message, stack?, context?)`, reading the stack and context
148
+ * positionally so a trailing stack is never mistaken for the context.
149
+ *
150
+ * @param message - The message or `Error` to log.
151
+ * @param optionalParams - NestJS trailing params (`stack` at index 0, `context` at index 1).
152
+ */
153
+ error(message, ...optionalParams) {
154
+ if (message instanceof Error) {
155
+ this.pino.error(
156
+ { context: this.resolveContext(optionalParams), err: this.serializeError(message) },
157
+ message.message
158
+ );
159
+ return;
160
+ }
161
+ const stack = typeof optionalParams[0] === "string" ? optionalParams[0] : void 0;
162
+ const context = typeof optionalParams[1] === "string" ? optionalParams[1] : this.context;
163
+ const payload = { context };
164
+ if (stack !== void 0) {
165
+ payload["stack"] = stack;
166
+ }
167
+ this.pino.error(payload, typeof message === "string" ? message : String(message));
168
+ }
169
+ /**
170
+ * NestJS `warn` (warn level).
171
+ *
172
+ * @param message - The message to log.
173
+ * @param optionalParams - NestJS trailing params (context as last string).
174
+ */
175
+ warn(message, ...optionalParams) {
176
+ this.emitNestStyle("warn", message, optionalParams);
177
+ }
178
+ /**
179
+ * NestJS `debug` (debug level).
180
+ *
181
+ * @param message - The message to log.
182
+ * @param optionalParams - NestJS trailing params (context as last string).
183
+ */
184
+ debug(message, ...optionalParams) {
185
+ this.emitNestStyle("debug", message, optionalParams);
186
+ }
187
+ /**
188
+ * NestJS `verbose` (mapped to Pino `trace`).
189
+ *
190
+ * @param message - The message to log.
191
+ * @param optionalParams - NestJS trailing params (context as last string).
192
+ */
193
+ verbose(message, ...optionalParams) {
194
+ this.emitNestStyle("trace", message, optionalParams);
195
+ }
196
+ /**
197
+ * NestJS `fatal` (fatal level). Declared non-optional here: Pino supports
198
+ * level 60 natively, and widening the optional `LoggerService.fatal?` to a
199
+ * required method stays type-compatible.
200
+ *
201
+ * @param message - The message to log.
202
+ * @param optionalParams - NestJS trailing params (context as last string).
203
+ */
204
+ fatal(message, ...optionalParams) {
205
+ this.emitNestStyle("fatal", message, optionalParams);
206
+ }
207
+ // ─── Structured API (MODULE_ACTION_RESULT convention) ─────────────────────
208
+ /**
209
+ * Emit a structured info log.
210
+ *
211
+ * @param logKey - Convention key, e.g. `USER_CREATED`.
212
+ * @param message - Human-readable message.
213
+ * @param userId - Optional acting user identifier.
214
+ * @param metadata - Optional extra structured fields.
215
+ * @example
216
+ * logger.info('USER_CREATED', 'New user registered', userId, { plan: 'pro' })
217
+ */
218
+ info(logKey, message, userId, metadata) {
219
+ this.emitStructured("info", logKey, message, userId, metadata);
220
+ }
221
+ /**
222
+ * Emit a structured warning log.
223
+ *
224
+ * @param logKey - Convention key.
225
+ * @param message - Human-readable message.
226
+ * @param userId - Optional acting user identifier.
227
+ * @param metadata - Optional extra structured fields.
228
+ */
229
+ warnStructured(logKey, message, userId, metadata) {
230
+ this.emitStructured("warn", logKey, message, userId, metadata);
231
+ }
232
+ /**
233
+ * Emit a structured error log with a serialized `err` object.
234
+ *
235
+ * @param logKey - Convention key.
236
+ * @param error - The error to serialize (`name` / `message` / `stack`).
237
+ * @param userId - Optional acting user identifier.
238
+ * @param metadata - Optional extra structured fields.
239
+ */
240
+ errorStructured(logKey, error, userId, metadata) {
241
+ this.pino.error(
242
+ { ...metadata, logKey, userId, context: this.context, err: this.serializeError(error) },
243
+ error.message
244
+ );
245
+ }
246
+ // ─── Helpers / escape hatches ─────────────────────────────────────────────
247
+ /**
248
+ * Set the global context label applied to subsequent logs from THIS instance.
249
+ *
250
+ * WARNING: `PinoLoggerService` is a singleton. Calling this per class clobbers
251
+ * the label for every other holder, and calling it at request time is a
252
+ * cross-request race. For a per-class label use `@InjectLogger(MyClass.name)`
253
+ * (which binds a child logger instead of mutating shared state); reserve
254
+ * `setContext()` for a single global / bootstrap label.
255
+ *
256
+ * @param context - The context label.
257
+ */
258
+ setContext(context) {
259
+ this.context = context;
260
+ }
261
+ /**
262
+ * Access the underlying Pino instance for advanced use cases.
263
+ *
264
+ * @returns The wrapped Pino logger.
265
+ */
266
+ getRawLogger() {
267
+ return this.pino;
268
+ }
269
+ /**
270
+ * Create a child logger with additional permanent bindings, inheriting the
271
+ * current context label.
272
+ *
273
+ * @param bindings - Fields merged into every entry of the child logger.
274
+ * @returns A new `PinoLoggerService` wrapping the Pino child.
275
+ */
276
+ child(bindings) {
277
+ const childService = new PinoLoggerService(this.pino.child(bindings));
278
+ if (this.context !== void 0) {
279
+ childService.setContext(this.context);
280
+ }
281
+ return childService;
282
+ }
283
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
284
+ /**
285
+ * NestJS shutdown hook. Destination flushing is handled by `DestinationRegistry`;
286
+ * this service has no additional teardown work.
287
+ */
288
+ onApplicationShutdown() {
289
+ }
290
+ // ─── Private ──────────────────────────────────────────────────────────────
291
+ /**
292
+ * Serialize an Error into a plain, log-safe object matching Pino's built-in
293
+ * `err` serializer shape (`type`, `message`, `stack`).
294
+ *
295
+ * Reads `error.name` (NOT `error.constructor.name`) for `type`: native
296
+ * subclasses set `name` to their class name, and a sanitized plain-object error
297
+ * (from `sanitizeError`) carries the real class name on `name` — whereas its
298
+ * `constructor.name` would be the unhelpful `'Object'`, breaking `err.type`
299
+ * dashboards for every exception logged through `HttpExceptionFilter`.
300
+ */
301
+ serializeError(error) {
302
+ return { type: error.name, message: error.message, stack: error.stack };
303
+ }
304
+ /** Resolve the context: last string param wins, else the instance context. */
305
+ resolveContext(optionalParams) {
306
+ const last = optionalParams[optionalParams.length - 1];
307
+ return typeof last === "string" ? last : this.context;
308
+ }
309
+ /** Emit a structured payload at info or warn level. */
310
+ emitStructured(level, logKey, message, userId, metadata) {
311
+ const payload = {
312
+ ...metadata,
313
+ logKey,
314
+ userId,
315
+ context: this.context
316
+ };
317
+ if (level === "info") {
318
+ this.pino.info(payload, message);
319
+ } else {
320
+ this.pino.warn(payload, message);
321
+ }
322
+ }
323
+ /** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
324
+ emitNestStyle(level, message, optionalParams) {
325
+ const payload = { context: this.resolveContext(optionalParams) };
326
+ const msg = typeof message === "string" ? message : String(message);
327
+ switch (level) {
328
+ case "info":
329
+ this.pino.info(payload, msg);
330
+ break;
331
+ case "warn":
332
+ this.pino.warn(payload, msg);
333
+ break;
334
+ case "debug":
335
+ this.pino.debug(payload, msg);
336
+ break;
337
+ case "trace":
338
+ this.pino.trace(payload, msg);
339
+ break;
340
+ case "fatal":
341
+ this.pino.fatal(payload, msg);
342
+ break;
343
+ }
344
+ }
345
+ };
346
+ PinoLoggerService = __decorateClass([
347
+ Injectable(),
348
+ __decorateParam(0, Inject(LOGGER_PINO_INSTANCE_TOKEN))
349
+ ], PinoLoggerService);
350
+
351
+ // src/server/decorators/inject-logger.provider.ts
352
+ var contextTokens = /* @__PURE__ */ new Map();
353
+ function getContextLoggerToken(context) {
354
+ const existing = contextTokens.get(context);
355
+ if (existing !== void 0) {
356
+ return existing;
357
+ }
358
+ const token = /* @__PURE__ */ Symbol(`INJECTED_LOGGER_${context}`);
359
+ contextTokens.set(context, token);
360
+ return token;
361
+ }
362
+ function createContextLoggerProvider(context) {
363
+ return {
364
+ provide: getContextLoggerToken(context),
365
+ useFactory: (rootLogger) => rootLogger.child({ context }),
366
+ inject: [PinoLoggerService]
367
+ };
368
+ }
369
+ function collectContextLoggerProviders() {
370
+ return Array.from(contextTokens.keys(), (context) => createContextLoggerProvider(context));
371
+ }
372
+ function collectContextLoggerTokens() {
373
+ return Array.from(contextTokens.values());
374
+ }
375
+ var DefaultStdoutDestination = class {
376
+ /**
377
+ * @param opts.minLevel — Optional minimum level filter. Entries below this
378
+ * level are dropped by the destination registry before reaching `write`.
379
+ * Omit (or pass `undefined`) to accept every level.
380
+ */
381
+ constructor(opts = {}) {
382
+ this.name = "stdout-json";
383
+ if (opts.minLevel !== void 0) {
384
+ this.minLevel = opts.minLevel;
385
+ }
386
+ }
387
+ /**
388
+ * Write a serialized JSON log entry (terminated with `\n`) to `stdout`.
389
+ */
390
+ write(payload) {
391
+ process.stdout.write(payload);
392
+ }
393
+ };
394
+ DefaultStdoutDestination = __decorateClass([
395
+ Injectable()
396
+ ], DefaultStdoutDestination);
397
+
398
+ // src/shared/constants/reserved-log-keys.constants.ts
399
+ var RESERVED_LOG_KEYS = Object.freeze({
400
+ LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK",
401
+ LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING",
402
+ LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK",
403
+ HTTP_REQUEST_START: "HTTP_REQUEST_START",
404
+ HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS",
405
+ HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT",
406
+ HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR",
407
+ HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR",
408
+ HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED",
409
+ HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
410
+ HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
411
+ METHOD_EXECUTION: "METHOD_EXECUTION",
412
+ METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
413
+ LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
414
+ LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
415
+ LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED"
416
+ });
417
+
418
+ // src/server/utils/normalize-url.util.ts
419
+ 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;
420
+ var ULID_REGEX = /\/[0-9A-HJKMNP-TV-Z]{26}(?![0-9A-HJKMNP-TV-Z])/g;
421
+ var NANOID_REGEX = /\/[A-Za-z0-9_-]{21}(?![A-Za-z0-9_-])/g;
422
+ var NUMERIC_ID_REGEX = /\/\d+/g;
423
+ var ID_PLACEHOLDER = "/:id";
424
+ function normalizeUrl(url) {
425
+ return stripQueryString(url).replace(UUID_REGEX, ID_PLACEHOLDER).replace(ULID_REGEX, ID_PLACEHOLDER).replace(NANOID_REGEX, ID_PLACEHOLDER).replace(NUMERIC_ID_REGEX, ID_PLACEHOLDER);
426
+ }
427
+ function stripQueryString(url) {
428
+ const queryStart = url.indexOf("?");
429
+ return queryStart === -1 ? url : url.slice(0, queryStart);
430
+ }
431
+
432
+ // src/server/utils/sanitize-error.util.ts
433
+ var DEFAULT_MAX_CAUSE_DEPTH = 3;
434
+ var MAX_AGGREGATE_ERRORS = 10;
435
+ var CIRCULAR_SENTINEL = "[Circular]";
436
+ function sanitizeError(err, options) {
437
+ const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
438
+ return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
439
+ }
440
+ function toSanitizedError(value, seen, depth2, maxDepth) {
441
+ try {
442
+ if (!(value instanceof Error)) {
443
+ return { name: "UnknownError", message: String(value) };
444
+ }
445
+ seen.add(value);
446
+ const result = { name: value.name, message: value.message };
447
+ if (value.stack !== void 0) {
448
+ result.stack = scrubStack(value.stack);
449
+ }
450
+ const isAtDepthLimit = depth2 >= maxDepth;
451
+ if (value.cause !== void 0) {
452
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
453
+ }
454
+ const aggregated = readAggregatedErrors(value);
455
+ if (aggregated !== void 0) {
456
+ const capped = aggregated.slice(0, MAX_AGGREGATE_ERRORS);
457
+ result.errors = capped.map(
458
+ (inner) => isAtDepthLimit ? createDepthMarker() : toSanitizedChild(inner, seen, depth2 + 1, maxDepth)
459
+ );
460
+ if (aggregated.length > MAX_AGGREGATE_ERRORS) {
461
+ result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
462
+ }
463
+ }
464
+ return result;
465
+ } catch {
466
+ return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
467
+ }
468
+ }
469
+ function toSanitizedChild(value, seen, depth2, maxDepth) {
470
+ if (isObject(value) && seen.has(value)) {
471
+ return CIRCULAR_SENTINEL;
472
+ }
473
+ return toSanitizedError(value, seen, depth2, maxDepth);
474
+ }
475
+ function readAggregatedErrors(value) {
476
+ const candidate = value.errors;
477
+ return Array.isArray(candidate) ? candidate : void 0;
478
+ }
479
+ function scrubStack(stack) {
480
+ return stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n");
481
+ }
482
+ function isObject(value) {
483
+ return typeof value === "object" && value !== null;
484
+ }
485
+ function createDepthMarker() {
486
+ return { _truncated: true, _reason: "cause-depth-exceeded" };
487
+ }
488
+ function createWidthMarker(omitted) {
489
+ return { _truncated: true, _reason: "aggregate-width-exceeded", _omitted: omitted };
490
+ }
491
+
492
+ // src/server/filters/http-exception.filter.ts
493
+ var INTERNAL_ERROR_BODY = {
494
+ statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
495
+ message: "Internal server error"
496
+ };
497
+ var HttpExceptionFilter = class {
498
+ /**
499
+ * @param logger - The structured logger exception entries are written to.
500
+ * Injected by token explicitly: the package is bundled without
501
+ * `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
502
+ */
503
+ constructor(logger) {
504
+ this.logger = logger;
505
+ }
506
+ /**
507
+ * Log the exception at the status-appropriate level and write the JSON
508
+ * response.
509
+ *
510
+ * @param exception - The thrown value (anything).
511
+ * @param host - The arguments host (switched to HTTP).
512
+ */
513
+ catch(exception, host) {
514
+ const ctx = host.switchToHttp();
515
+ const res = ctx.getResponse();
516
+ const req = ctx.getRequest();
517
+ const userId = req.user?.id;
518
+ const url = stripQueryString(req.url);
519
+ if (exception instanceof HttpException) {
520
+ const status2 = exception.getStatus();
521
+ const metadata = { method: req.method, url, status: status2 };
522
+ if (status2 >= HttpStatus.INTERNAL_SERVER_ERROR) {
523
+ this.logger.errorStructured(
524
+ RESERVED_LOG_KEYS.HTTP_EXCEPTION_UNHANDLED,
525
+ sanitizeError(exception),
526
+ userId,
527
+ metadata
528
+ );
529
+ } else {
530
+ this.logger.warnStructured(
531
+ RESERVED_LOG_KEYS.HTTP_EXCEPTION_HANDLED,
532
+ exception.message,
533
+ userId,
534
+ metadata
535
+ );
536
+ }
537
+ res.status(status2).json(exception.getResponse());
538
+ return;
539
+ }
540
+ const status = HttpStatus.INTERNAL_SERVER_ERROR;
541
+ this.logger.errorStructured(
542
+ RESERVED_LOG_KEYS.HTTP_EXCEPTION_UNHANDLED,
543
+ sanitizeError(exception),
544
+ userId,
545
+ {
546
+ method: req.method,
547
+ url,
548
+ status
549
+ }
550
+ );
551
+ res.status(status).json(INTERNAL_ERROR_BODY);
552
+ }
553
+ };
554
+ HttpExceptionFilter = __decorateClass([
555
+ Catch(),
556
+ __decorateParam(0, Inject(PinoLoggerService))
557
+ ], HttpExceptionFilter);
558
+ var HTTP_SUCCESS_MIN = 200;
559
+ var HTTP_REDIRECT_MIN = 300;
560
+ var HTTP_CLIENT_ERROR_MIN = 400;
561
+ var HTTP_SERVER_ERROR_MIN = 500;
562
+ var UNKNOWN_USER_AGENT = "unknown";
563
+ var HttpLoggingInterceptor = class {
564
+ /**
565
+ * @param logger - The structured logger the lifecycle entries are written to.
566
+ * Injected by token explicitly: the package is bundled without
567
+ * `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
568
+ * @param options - Resolved module options (read for `http.excludePaths`).
569
+ * Injected by token for the same bundling reason.
570
+ */
571
+ constructor(logger, options) {
572
+ this.logger = logger;
573
+ this.excludePaths = options.http.excludePaths;
574
+ }
575
+ /**
576
+ * Whether a request path bypasses HTTP logging (matches any `excludePaths`).
577
+ *
578
+ * @param path - The query-stripped request path.
579
+ * @returns `true` when the path matches a configured exclude pattern.
580
+ */
581
+ isExcluded(path) {
582
+ return this.excludePaths.some((pattern) => pattern.test(path));
583
+ }
584
+ /**
585
+ * Emit the START entry, then attach success/redirect/error logging to the
586
+ * downstream observable while always re-propagating any thrown exception.
587
+ *
588
+ * @param context - The current execution context (HTTP).
589
+ * @param next - The downstream call handler.
590
+ * @returns The (unmodified) response stream.
591
+ */
592
+ intercept(context, next) {
593
+ const http = context.switchToHttp();
594
+ const req = http.getRequest();
595
+ const res = http.getResponse();
596
+ const { method, url, ip } = req;
597
+ if (this.isExcluded(stripQueryString(url))) {
598
+ return next.handle();
599
+ }
600
+ const rawUserAgent = req.headers["user-agent"];
601
+ const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : UNKNOWN_USER_AGENT;
602
+ const userId = req.user?.id;
603
+ const normalizedUrl = normalizeUrl(url);
604
+ const start = Date.now();
605
+ this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
606
+ method,
607
+ url: normalizedUrl,
608
+ // Query string stripped: it may carry secrets (reset tokens, OAuth codes)
609
+ // that Pino's path-based redaction cannot scrub from a string value. The
610
+ // raw (un-normalized) path is kept for per-request debugging.
611
+ fullUrl: stripQueryString(url),
612
+ ip,
613
+ userAgent
614
+ });
615
+ return next.handle().pipe(
616
+ tap(() => {
617
+ this.logSuccess(res.statusCode, method, normalizedUrl, userId, Date.now() - start);
618
+ }),
619
+ catchError((err) => {
620
+ this.logError(err, method, normalizedUrl, userId, Date.now() - start);
621
+ return throwError(() => err);
622
+ })
623
+ );
624
+ }
625
+ /**
626
+ * Log a non-throwing completion: SUCCESS for 2xx, REDIRECT for 3xx, nothing
627
+ * for any other status (those are surfaced via the error path instead).
628
+ *
629
+ * @param statusCode - The final response status code.
630
+ * @param method - The HTTP method.
631
+ * @param url - The normalized request URL.
632
+ * @param userId - The acting user id, when known.
633
+ * @param duration - The elapsed time in milliseconds.
634
+ */
635
+ logSuccess(statusCode, method, url, userId, duration) {
636
+ if (statusCode >= HTTP_SUCCESS_MIN && statusCode < HTTP_REDIRECT_MIN) {
637
+ this.logger.info(
638
+ RESERVED_LOG_KEYS.HTTP_REQUEST_SUCCESS,
639
+ `${method} ${url} \u2192 ${statusCode} (${duration}ms)`,
640
+ userId,
641
+ { method, url, statusCode, duration }
642
+ );
643
+ } else if (statusCode >= HTTP_REDIRECT_MIN && statusCode < HTTP_CLIENT_ERROR_MIN) {
644
+ this.logger.info(
645
+ RESERVED_LOG_KEYS.HTTP_REQUEST_REDIRECT,
646
+ `${method} ${url} \u2192 ${statusCode}`,
647
+ userId,
648
+ { method, url, statusCode, duration }
649
+ );
650
+ }
651
+ }
652
+ /**
653
+ * Log a thrown completion: SERVER_ERROR (error level, with stack) for 5xx,
654
+ * CLIENT_ERROR (warn level) for an `HttpException` below 500. A non-`HttpException`
655
+ * always maps to 500, so it is reported as a server error.
656
+ *
657
+ * @param err - The thrown value (anything).
658
+ * @param method - The HTTP method.
659
+ * @param url - The normalized request URL.
660
+ * @param userId - The acting user id, when known.
661
+ * @param duration - The elapsed time in milliseconds.
662
+ */
663
+ logError(err, method, url, userId, duration) {
664
+ if (err instanceof HttpException) {
665
+ const statusCode = err.getStatus();
666
+ if (statusCode >= HTTP_SERVER_ERROR_MIN) {
667
+ this.logger.errorStructured(RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR, err, userId, {
668
+ method,
669
+ url,
670
+ statusCode,
671
+ duration
672
+ });
673
+ } else {
674
+ this.logger.warnStructured(
675
+ RESERVED_LOG_KEYS.HTTP_REQUEST_CLIENT_ERROR,
676
+ `${method} ${url} \u2192 ${statusCode}`,
677
+ userId,
678
+ { method, url, statusCode, duration, errorMessage: err.message }
679
+ );
680
+ }
681
+ return;
682
+ }
683
+ this.logger.errorStructured(
684
+ RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR,
685
+ err instanceof Error ? err : new Error(String(err)),
686
+ userId,
687
+ { method, url, statusCode: HttpStatus.INTERNAL_SERVER_ERROR, duration }
688
+ );
689
+ }
690
+ };
691
+ HttpLoggingInterceptor = __decorateClass([
692
+ Injectable(),
693
+ __decorateParam(0, Inject(PinoLoggerService)),
694
+ __decorateParam(1, Inject(LOGGER_OPTIONS_TOKEN))
695
+ ], HttpLoggingInterceptor);
696
+ var PassThroughInterceptor = class {
697
+ /**
698
+ * Forward the downstream stream without observing or modifying it.
699
+ *
700
+ * @param _context - The execution context (unused).
701
+ * @param next - The downstream call handler.
702
+ * @returns The unmodified downstream stream.
703
+ */
704
+ intercept(_context, next) {
705
+ return next.handle();
706
+ }
707
+ };
708
+ PassThroughInterceptor = __decorateClass([
709
+ Injectable()
710
+ ], PassThroughInterceptor);
711
+ var {
712
+ ConfigurableModuleClass: BymaxLoggerModuleBase,
713
+ MODULE_OPTIONS_TOKEN: BUILDER_OPTIONS_TOKEN,
714
+ OPTIONS_TYPE,
715
+ ASYNC_OPTIONS_TYPE
716
+ } = new ConfigurableModuleBuilder().setClassMethodName("forRoot").setFactoryMethodName("createLoggerOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
717
+ ...definition,
718
+ // `setExtras` merges the `{ isGlobal: true }` default before this runs, so
719
+ // `isGlobal` is always defined; `!== false` keeps "global unless explicitly
720
+ // disabled" without a dead nullish branch.
721
+ global: extras.isGlobal !== false
722
+ })).build();
723
+ function detectOtelTraceApi() {
724
+ try {
725
+ const requireFromCwd = createRequire(join(process.cwd(), "noop.cjs"));
726
+ const mod = requireFromCwd("@opentelemetry/api");
727
+ return mod.trace;
728
+ } catch {
729
+ return void 0;
730
+ }
731
+ }
732
+ function isValidTraceId(traceId) {
733
+ return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
734
+ }
735
+ function isValidSpanId(spanId) {
736
+ return /^[0-9a-f]{16}$/.test(spanId) && !/^0+$/.test(spanId);
737
+ }
738
+
739
+ // src/server/mixins/trace-context.mixin.ts
740
+ function createTraceContextMixin(logContext, opts) {
741
+ const traceApi = opts.shouldAutoInjectTraceContext ? detectOtelTraceApi() : void 0;
742
+ return function mixin(_mergeObject, _level, _logger) {
743
+ const merged = {};
744
+ const store = logContext.getStore();
745
+ if (store) {
746
+ Object.assign(merged, store);
747
+ }
748
+ if (traceApi) {
749
+ const span = traceApi.getActiveSpan();
750
+ if (span) {
751
+ const ctx = span.spanContext();
752
+ if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
753
+ Reflect.set(merged, opts.traceIdField, ctx.traceId);
754
+ Reflect.set(merged, opts.spanIdField, ctx.spanId);
755
+ if (typeof ctx.traceFlags === "number") {
756
+ const flagsByte = ctx.traceFlags & 255;
757
+ Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
758
+ }
759
+ }
760
+ }
761
+ }
762
+ return merged;
763
+ };
764
+ }
765
+
766
+ // src/server/constants/default-redact-paths.constants.ts
767
+ var REDACT_MAX_DEPTH = 4;
768
+ var WILDCARD_PREFIXES = Array.from(
769
+ { length: REDACT_MAX_DEPTH },
770
+ (_, idx) => "*" + ".*".repeat(idx)
771
+ );
772
+ var depth = (field) => WILDCARD_PREFIXES.map((prefix) => `${prefix}.${field}`);
773
+ var REDACT_COMMON_FIELDS = [
774
+ // Passwords (5)
775
+ "password",
776
+ "passwordHash",
777
+ "passwordConfirm",
778
+ "newPassword",
779
+ "oldPassword",
780
+ // Tokens (6)
781
+ "token",
782
+ "accessToken",
783
+ "refreshToken",
784
+ "idToken",
785
+ "apiKey",
786
+ "apiSecret",
787
+ // MFA (3)
788
+ "mfaSecret",
789
+ "mfaRecoveryCodes",
790
+ "totpSecret",
791
+ // Payment / PCI DSS (5)
792
+ "cardNumber",
793
+ "cardCvv",
794
+ "cvv",
795
+ "cvc",
796
+ "cardExpiry",
797
+ // Personal documents — BR / LGPD (3)
798
+ "cpf",
799
+ "cnpj",
800
+ "rg",
801
+ // Generic secrets (4) — the most common short-form credential field names
802
+ "secret",
803
+ "clientSecret",
804
+ "signingSecret",
805
+ "privateKey",
806
+ // Conservative PII (1) — consumer can disable if logging email is justified
807
+ "email"
808
+ ];
809
+ var REDACT_ABSOLUTE_PATHS = [
810
+ "req.headers.authorization",
811
+ "req.headers.cookie",
812
+ 'req.headers["x-api-key"]',
813
+ 'req.headers["x-auth-token"]',
814
+ 'res.headers["set-cookie"]'
815
+ ];
816
+ var DEFAULT_REDACT_PATHS = [
817
+ ...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
818
+ ...REDACT_ABSOLUTE_PATHS
819
+ ];
820
+
821
+ // src/server/utils/compile-redact-paths.util.ts
822
+ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
823
+ const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
824
+ return Array.from(new Set(sources.flat()));
825
+ }
826
+ function reportWriteFailure(name, cause) {
827
+ const err = cause instanceof Error ? { type: cause.name, message: cause.message } : { type: "UnknownError", message: String(cause) };
828
+ try {
829
+ process.stderr.write(
830
+ JSON.stringify({
831
+ level: "error",
832
+ logKey: RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
833
+ destination: name,
834
+ err,
835
+ msg: `Log destination "${name}" failed to write; the entry was dropped`
836
+ }) + "\n"
837
+ );
838
+ } catch {
839
+ }
840
+ }
841
+ function destinationToStream(destination) {
842
+ return new Writable({
843
+ // Keep string chunks as strings (Node's default re-encodes them to Buffers
844
+ // before `_write`): the common Pino path emits NDJSON strings, so this skips
845
+ // a string→Buffer→string round-trip on the hot logging path.
846
+ decodeStrings: false,
847
+ write(chunk, _encoding, callback) {
848
+ const onFailure = (cause) => {
849
+ reportWriteFailure(destination.name, cause);
850
+ callback();
851
+ };
852
+ try {
853
+ const payload = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
854
+ const result = destination.write(payload);
855
+ if (result instanceof Promise) {
856
+ result.then(() => callback(), onFailure);
857
+ } else {
858
+ callback();
859
+ }
860
+ } catch (cause) {
861
+ onFailure(cause);
862
+ }
863
+ }
864
+ });
865
+ }
866
+
867
+ // src/server/utils/truncate-large-entries.ts
868
+ var PREVIEW_LENGTH = 200;
869
+ function createSizeBoundedSerializer(baseSerializer, maxBytes) {
870
+ return (input) => {
871
+ const serialized = baseSerializer(input);
872
+ let json;
873
+ try {
874
+ json = JSON.stringify(serialized);
875
+ } catch {
876
+ return serialized;
877
+ }
878
+ if (json === void 0) {
879
+ return serialized;
880
+ }
881
+ const byteSize = Buffer.byteLength(json, "utf-8");
882
+ if (byteSize > maxBytes) {
883
+ return {
884
+ _truncated: true,
885
+ _logKey: RESERVED_LOG_KEYS.LOGGER_ENTRY_TRUNCATED,
886
+ _originalSize: byteSize,
887
+ _preview: json.slice(0, PREVIEW_LENGTH)
888
+ };
889
+ }
890
+ return serialized;
891
+ };
892
+ }
893
+
894
+ // src/server/pino-factory.ts
895
+ function buildPinoInstance(options, logContext, destinations) {
896
+ const maxBytes = options.maxEntrySizeBytes;
897
+ const serializers = {
898
+ err: createSizeBoundedSerializer(pino.stdSerializers.err, maxBytes),
899
+ ...Object.fromEntries(
900
+ Object.entries(options.serializers).map(
901
+ ([key, serializer]) => [
902
+ key,
903
+ createSizeBoundedSerializer(serializer, maxBytes)
904
+ ]
905
+ )
906
+ )
907
+ };
908
+ const pinoOptions = {
909
+ level: options.level,
910
+ redact: {
911
+ paths: compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact),
912
+ censor: options.redactCensor
913
+ },
914
+ base: { service: options.service },
915
+ // Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
916
+ timestamp: () => `,"time":"${options.timestamp()}"`,
917
+ formatters: {
918
+ // Emit the level as a string label instead of the numeric code — easier
919
+ // for log aggregators. Trace context is injected via `mixin`, never via
920
+ // `formatters.log` (which cannot see ambient ALS / OTel state).
921
+ level: (label) => ({ level: label })
922
+ },
923
+ serializers,
924
+ mixin: createTraceContextMixin(logContext, options.otel)
925
+ };
926
+ const streams = destinations.map((destination) => ({
927
+ level: destination.minLevel ?? options.level,
928
+ stream: destinationToStream(destination)
929
+ }));
930
+ return pino(pinoOptions, pino.multistream(streams));
931
+ }
932
+ var DestinationRegistry = class {
933
+ /**
934
+ * @param registered - Every destination declared under
935
+ * `LOGGER_DESTINATIONS_TOKEN`. Injected by token explicitly because the
936
+ * package is bundled without `emitDecoratorMetadata`.
937
+ * @param logger - Structured logger used to report init failures. Injected by
938
+ * token explicitly for the same bundling reason.
939
+ */
940
+ constructor(registered, logger) {
941
+ this.registered = registered;
942
+ this.logger = logger;
943
+ /**
944
+ * Destinations that initialized successfully, in registration order.
945
+ *
946
+ * Note: the Pino multistream is built from the full registered list, and the
947
+ * write path is independently fail-soft (see `destinationToStream`). This set
948
+ * is the authoritative record of init success — it drives reverse-order
949
+ * shutdown — but is not currently used to remove a failed-init sink from the
950
+ * write fan-out; a sink that failed `onInit` still receives writes, contained
951
+ * fail-soft by the wrapper.
952
+ */
953
+ this.active = [];
954
+ }
955
+ /**
956
+ * Initialize every registered destination. A destination whose `onInit`
957
+ * rejects is skipped (not added to the active set) and reported — it must not
958
+ * block application bootstrap.
959
+ */
960
+ async onModuleInit() {
961
+ for (const destination of this.registered) {
962
+ try {
963
+ await destination.onInit?.();
964
+ this.active.push(destination);
965
+ } catch (cause) {
966
+ this.logger.errorStructured(
967
+ RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
968
+ cause instanceof Error ? cause : new Error(String(cause)),
969
+ void 0,
970
+ { destination: destination.name }
971
+ );
972
+ }
973
+ }
974
+ }
975
+ /**
976
+ * Flush and close active destinations in REVERSE registration order, so a
977
+ * destination registered first (e.g. stdout) closes last. Failures are written
978
+ * directly to stderr: `PinoLoggerService` may already be torn down at this
979
+ * point in the shutdown sequence.
980
+ */
981
+ async onApplicationShutdown() {
982
+ for (const destination of [...this.active].reverse()) {
983
+ try {
984
+ await destination.onShutdown?.();
985
+ } catch (cause) {
986
+ const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
987
+ process.stderr.write(
988
+ `[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
989
+ `
990
+ );
991
+ }
992
+ }
993
+ }
994
+ /**
995
+ * The destinations that initialized successfully.
996
+ *
997
+ * @returns A read-only view of the active destination set.
998
+ */
999
+ getActive() {
1000
+ return this.active;
1001
+ }
1002
+ };
1003
+ DestinationRegistry = __decorateClass([
1004
+ Injectable(),
1005
+ __decorateParam(0, Inject(LOGGER_DESTINATIONS_TOKEN)),
1006
+ __decorateParam(1, Inject(PinoLoggerService))
1007
+ ], DestinationRegistry);
1008
+ var FORBIDDEN_CONTEXT_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1009
+ var LogContextService = class {
1010
+ constructor() {
1011
+ this.als = new AsyncLocalStorage();
1012
+ }
1013
+ /**
1014
+ * Run `callback` inside a fresh context scope. Every log emitted
1015
+ * synchronously or asynchronously within the callback inherits `context`.
1016
+ *
1017
+ * @typeParam T - Return type of the callback.
1018
+ * @param context - Initial context bag for the scope.
1019
+ * @param callback - Function executed within the scope.
1020
+ * @returns Whatever `callback` returns.
1021
+ * @example
1022
+ * logContext.run({ requestId: 'r_1', tenantId: 't_1' }, async () => {
1023
+ * await userService.create(dto) // logs carry requestId + tenantId
1024
+ * })
1025
+ */
1026
+ run(context, callback) {
1027
+ return this.als.run(this.sanitizeContext(context), callback);
1028
+ }
1029
+ /**
1030
+ * Read the active context, or `undefined` when called outside any scope.
1031
+ *
1032
+ * @returns The active `LogContext`, or `undefined`.
1033
+ */
1034
+ getStore() {
1035
+ return this.als.getStore();
1036
+ }
1037
+ /**
1038
+ * Add or override a single field on the active context.
1039
+ *
1040
+ * @param key - Field name to set.
1041
+ * @param value - Field value.
1042
+ * @throws Error When called outside a `run()` scope, or with a prototype-polluting key.
1043
+ */
1044
+ set(key, value) {
1045
+ const store = this.als.getStore();
1046
+ if (!store) {
1047
+ throw new Error("[LogContextService] set() called outside run() scope");
1048
+ }
1049
+ if (FORBIDDEN_CONTEXT_KEYS.has(key)) {
1050
+ throw new Error(`[LogContextService] Refusing to set unsafe context key: ${key}`);
1051
+ }
1052
+ Reflect.set(store, key, value);
1053
+ }
1054
+ /**
1055
+ * Read a single field from the active context.
1056
+ *
1057
+ * @typeParam T - Expected value type (caller-asserted).
1058
+ * @param key - Field name to read.
1059
+ * @returns The value cast to `T`, or `undefined` when unset / out of scope.
1060
+ */
1061
+ get(key) {
1062
+ const store = this.als.getStore();
1063
+ if (!store) {
1064
+ return void 0;
1065
+ }
1066
+ return Reflect.get(store, key);
1067
+ }
1068
+ /**
1069
+ * Strip prototype-polluting keys from a caller-supplied context so an own
1070
+ * `__proto__` / `constructor` / `prototype` key (e.g. from `JSON.parse` of an
1071
+ * untrusted payload) can never reach the store — and, via the trace mixin's
1072
+ * `Object.assign`, the emitted log entry. Mirrors the guard `set()` enforces,
1073
+ * but drops the key rather than throwing: a logging concern must never crash
1074
+ * the request that produced it.
1075
+ *
1076
+ * @param context - The raw context bag passed to `run()`.
1077
+ * @returns A shallow copy with any forbidden keys removed.
1078
+ */
1079
+ sanitizeContext(context) {
1080
+ const safe = {};
1081
+ for (const key of Object.keys(context)) {
1082
+ if (!FORBIDDEN_CONTEXT_KEYS.has(key)) {
1083
+ Reflect.set(safe, key, Reflect.get(context, key));
1084
+ }
1085
+ }
1086
+ return safe;
1087
+ }
1088
+ };
1089
+ LogContextService = __decorateClass([
1090
+ Injectable()
1091
+ ], LogContextService);
1092
+
1093
+ // src/server/logger.module.ts
1094
+ var LOGGER_BOOTSTRAP_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_BOOTSTRAP");
1095
+ function resolveDestinations(options) {
1096
+ return options.destinations.length > 0 ? options.destinations : [new DefaultStdoutDestination()];
1097
+ }
1098
+ function bootstrapProvider() {
1099
+ return {
1100
+ provide: LOGGER_BOOTSTRAP_TOKEN,
1101
+ useFactory: (logger) => {
1102
+ logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
1103
+ return true;
1104
+ },
1105
+ inject: [PinoLoggerService]
1106
+ };
1107
+ }
1108
+ function buildCommonProviders() {
1109
+ return [
1110
+ LogContextService,
1111
+ { provide: LOG_CONTEXT_TOKEN, useExisting: LogContextService },
1112
+ {
1113
+ provide: LOGGER_PINO_INSTANCE_TOKEN,
1114
+ useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
1115
+ inject: [LOGGER_OPTIONS_TOKEN, LogContextService, LOGGER_DESTINATIONS_TOKEN]
1116
+ },
1117
+ {
1118
+ provide: LOGGER_DESTINATIONS_TOKEN,
1119
+ useFactory: (options) => resolveDestinations(options),
1120
+ inject: [LOGGER_OPTIONS_TOKEN]
1121
+ },
1122
+ PinoLoggerService,
1123
+ // Internal lifecycle owner for destinations (onInit / onShutdown). Not
1124
+ // exported — consumers never inject it directly.
1125
+ DestinationRegistry,
1126
+ // One child-logger provider per context discovered from @InjectLogger(context).
1127
+ ...collectContextLoggerProviders(),
1128
+ bootstrapProvider()
1129
+ ];
1130
+ }
1131
+ function asyncHttpInterceptorProvider() {
1132
+ return {
1133
+ provide: APP_INTERCEPTOR,
1134
+ useFactory: (options, logger) => options.http.isEnabled ? new HttpLoggingInterceptor(logger, options) : new PassThroughInterceptor(),
1135
+ inject: [LOGGER_OPTIONS_TOKEN, PinoLoggerService]
1136
+ };
1137
+ }
1138
+ function augmentLoggerModule(base, providers) {
1139
+ return {
1140
+ ...base,
1141
+ module: BymaxLoggerModule,
1142
+ providers: [...base.providers ?? [], ...providers],
1143
+ exports: [
1144
+ ...base.exports ?? [],
1145
+ LOGGER_OPTIONS_TOKEN,
1146
+ LOGGER_PINO_INSTANCE_TOKEN,
1147
+ LOGGER_DESTINATIONS_TOKEN,
1148
+ LOG_CONTEXT_TOKEN,
1149
+ LogContextService,
1150
+ PinoLoggerService,
1151
+ // Export the per-context child-logger tokens so consumer modules can inject
1152
+ // them (the module is global, so only exported providers are visible).
1153
+ ...collectContextLoggerTokens()
1154
+ ]
1155
+ };
1156
+ }
1157
+ var BymaxLoggerModule = class extends BymaxLoggerModuleBase {
1158
+ /**
1159
+ * Synchronous registration.
1160
+ *
1161
+ * @param options - Logger options (plus the `isGlobal` extra).
1162
+ * @returns The configured `DynamicModule`.
1163
+ * @throws Error When the options fail validation.
1164
+ * @example
1165
+ * BymaxLoggerModule.forRoot({ service: { name: 'api', version: '1.0.0' } })
1166
+ */
1167
+ static forRoot(options) {
1168
+ validateOptions(options);
1169
+ const resolved = applyDefaults(options);
1170
+ const providers = [
1171
+ { provide: LOGGER_OPTIONS_TOKEN, useValue: resolved },
1172
+ ...buildCommonProviders()
1173
+ ];
1174
+ if (resolved.http.isEnabled) {
1175
+ providers.push({ provide: APP_INTERCEPTOR, useClass: HttpLoggingInterceptor });
1176
+ if (resolved.http.shouldCaptureExceptions) {
1177
+ providers.push({ provide: APP_FILTER, useClass: HttpExceptionFilter });
1178
+ }
1179
+ }
1180
+ return augmentLoggerModule(super.forRoot(options), providers);
1181
+ }
1182
+ /**
1183
+ * Asynchronous registration. Options are resolved lazily via the consumer's
1184
+ * factory (`useFactory` + `inject` + `imports`, `useClass`, or `useExisting`);
1185
+ * the Pino instance and the bootstrap log are produced only after they
1186
+ * resolve, never at module-decoration time.
1187
+ *
1188
+ * Note: the destination-registry `onModuleInit` lifecycle hook is managed by
1189
+ * `DestinationRegistry` itself, not by this factory. The destinations array is
1190
+ * registered under `LOGGER_DESTINATIONS_TOKEN`; no built-in destination defines
1191
+ * an `onInit` hook, so nothing is lost.
1192
+ *
1193
+ * HTTP access logging IS supported on this path: the global interceptor slot
1194
+ * is always registered and gated at runtime against the resolved
1195
+ * `http.isEnabled` (see {@link asyncHttpInterceptorProvider}). The catch-all
1196
+ * exception FILTER is intentionally NOT auto-wired here — registering a global
1197
+ * `@Catch()` filter from async config would either interfere with a consumer's
1198
+ * own filters (when disabled) or require unsafe re-throwing. Async consumers who
1199
+ * want it register `HttpExceptionFilter` themselves:
1200
+ * `{ provide: APP_FILTER, useClass: HttpExceptionFilter }`.
1201
+ *
1202
+ * @param options - Async options (factory + inject + imports, or class).
1203
+ * @returns The configured `DynamicModule`.
1204
+ */
1205
+ static forRootAsync(options) {
1206
+ const providers = [
1207
+ {
1208
+ provide: LOGGER_OPTIONS_TOKEN,
1209
+ useFactory: (raw) => {
1210
+ validateOptions(raw);
1211
+ return applyDefaults(raw);
1212
+ },
1213
+ inject: [BUILDER_OPTIONS_TOKEN]
1214
+ },
1215
+ ...buildCommonProviders(),
1216
+ // Access-log interceptor with parity to the sync path: always registered,
1217
+ // gated at runtime against the async-resolved `http.isEnabled`.
1218
+ asyncHttpInterceptorProvider()
1219
+ ];
1220
+ return augmentLoggerModule(super.forRootAsync(options), providers);
1221
+ }
1222
+ /**
1223
+ * Replace NestJS's internal logger with this library's `PinoLoggerService` and
1224
+ * flush any logs buffered since `NestFactory.create(..., { bufferLogs: true })`.
1225
+ *
1226
+ * Call once in `main.ts` AFTER creating the app and BEFORE `app.listen(...)`.
1227
+ * The module cannot do this itself — a module never sees the
1228
+ * `INestApplication` instance — so this static helper bridges the gap.
1229
+ *
1230
+ * @param app - The Nest application returned by `NestFactory.create()`.
1231
+ * @throws Error When `BymaxLoggerModule` was not imported (no `PinoLoggerService`
1232
+ * in the container) — a clear message instead of a cryptic DI failure. The
1233
+ * original DI error is attached as the error `cause` so the underlying
1234
+ * failure is preserved for debugging.
1235
+ * @example
1236
+ * const app = await NestFactory.create(AppModule, { bufferLogs: true })
1237
+ * BymaxLoggerModule.useNestLogger(app)
1238
+ * await app.listen(3000)
1239
+ */
1240
+ static useNestLogger(app) {
1241
+ let logger;
1242
+ try {
1243
+ logger = app.get(PinoLoggerService, { strict: false });
1244
+ } catch (cause) {
1245
+ throw new Error(
1246
+ "[BymaxLoggerModule] useNestLogger(app) called but BymaxLoggerModule was not imported",
1247
+ { cause }
1248
+ );
1249
+ }
1250
+ app.useLogger(logger);
1251
+ app.flushLogs();
1252
+ }
1253
+ };
1254
+ BymaxLoggerModule = __decorateClass([
1255
+ Module({})
1256
+ ], BymaxLoggerModule);
1257
+
1258
+ // src/server/destinations/pretty-dev.destination.ts
1259
+ var PrettyDevDestination = class {
1260
+ /**
1261
+ * @param opts.minLevel — Optional minimum level filter applied by the
1262
+ * destination registry's multi-stream wiring. Omit to accept every level.
1263
+ */
1264
+ constructor(opts = {}) {
1265
+ this.name = "pretty-dev";
1266
+ /**
1267
+ * Set when {@link onInit} fails (e.g. `pino-pretty` absent). Distinguishes a
1268
+ * permanently-failed destination — whose writes are DROPPED, since the registry
1269
+ * dropped it from the active set — from the transient pre-init window, where
1270
+ * {@link write} falls back to raw stdout so a bootstrap log is not lost.
1271
+ */
1272
+ this.initFailed = false;
1273
+ if (opts.minLevel !== void 0) {
1274
+ this.minLevel = opts.minLevel;
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Lazily resolve `pino-pretty` and build the colorized transform that renders
1279
+ * each entry to `process.stdout`.
1280
+ *
1281
+ * @throws Error When `pino-pretty` is not installed — with an actionable
1282
+ * message telling the consumer to install it or drop this destination.
1283
+ */
1284
+ async onInit() {
1285
+ try {
1286
+ const { build } = await import('pino-pretty');
1287
+ this.stream = build({
1288
+ colorize: true,
1289
+ translateTime: "SYS:HH:MM:ss.l",
1290
+ ignore: "pid,hostname,service",
1291
+ singleLine: false,
1292
+ destination: process.stdout
1293
+ });
1294
+ } catch {
1295
+ this.initFailed = true;
1296
+ throw new Error(
1297
+ "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
1298
+ );
1299
+ }
1300
+ }
1301
+ /**
1302
+ * Write a serialized NDJSON entry to the pretty transform.
1303
+ *
1304
+ * Three states:
1305
+ * - initialized → forward to the pino-pretty transform;
1306
+ * - init failed → DROP (the destination was removed from the active set, so
1307
+ * it must not duplicate raw NDJSON to stdout);
1308
+ * - pre-init (transient window before {@link onInit}, e.g. a bootstrap log)
1309
+ * → fall back to raw stdout so the entry is not lost.
1310
+ *
1311
+ * @param payload — Newline-terminated JSON entry, UTF-8 encoded.
1312
+ */
1313
+ write(payload) {
1314
+ if (this.stream !== void 0) {
1315
+ this.stream.write(payload);
1316
+ return;
1317
+ }
1318
+ if (this.initFailed) {
1319
+ return;
1320
+ }
1321
+ process.stdout.write(payload);
1322
+ }
1323
+ /**
1324
+ * Flush and close the pretty transform, resolving once the stream has ended.
1325
+ * A no-op when {@link onInit} never ran.
1326
+ */
1327
+ async onShutdown() {
1328
+ const stream = this.stream;
1329
+ if (stream === void 0) {
1330
+ return;
1331
+ }
1332
+ await new Promise((resolve, reject) => {
1333
+ stream.once("error", reject);
1334
+ stream.end(resolve);
1335
+ });
1336
+ }
1337
+ };
1338
+ var REQUEST_ID_HEADER = "x-request-id";
1339
+ var MAX_CORRELATION_ID_LENGTH = 256;
1340
+ function isAcceptableHeaderValue(value) {
1341
+ return typeof value === "string" && value.length <= MAX_CORRELATION_ID_LENGTH;
1342
+ }
1343
+ var RequestIdMiddleware = class {
1344
+ /**
1345
+ * @param logContext - The AsyncLocalStorage-backed context service. Injected by
1346
+ * token explicitly: the package is bundled without `emitDecoratorMetadata`,
1347
+ * so implicit class-type injection would not resolve.
1348
+ * @param options - Resolved module options (read for `http.tenantIdHeader` and
1349
+ * `http.shouldGenerateRequestId`).
1350
+ */
1351
+ constructor(logContext, options) {
1352
+ this.logContext = logContext;
1353
+ this.tenantIdHeader = options.http.tenantIdHeader;
1354
+ this.shouldGenerateRequestId = options.http.shouldGenerateRequestId;
1355
+ }
1356
+ /**
1357
+ * Echo or mint the `x-request-id`, expose it on the response, and run the
1358
+ * remainder of the request inside a fresh log-context scope.
1359
+ *
1360
+ * @param req - The incoming Express request.
1361
+ * @param res - The outgoing Express response.
1362
+ * @param next - The next handler in the chain.
1363
+ */
1364
+ use(req, res, next) {
1365
+ const incoming = Reflect.get(req.headers, REQUEST_ID_HEADER);
1366
+ const fromHeader = isAcceptableHeaderValue(incoming) ? incoming : void 0;
1367
+ const requestId = fromHeader ?? (this.shouldGenerateRequestId ? randomUUID() : void 0);
1368
+ const context = {};
1369
+ if (requestId !== void 0) {
1370
+ res.setHeader(REQUEST_ID_HEADER, requestId);
1371
+ context.requestId = requestId;
1372
+ }
1373
+ const rawTenantId = Reflect.get(req.headers, this.tenantIdHeader);
1374
+ if (isAcceptableHeaderValue(rawTenantId)) {
1375
+ context.tenantId = rawTenantId;
1376
+ }
1377
+ this.logContext.run(context, () => next());
1378
+ }
1379
+ };
1380
+ RequestIdMiddleware = __decorateClass([
1381
+ Injectable(),
1382
+ __decorateParam(0, Inject(LogContextService)),
1383
+ __decorateParam(1, Inject(LOGGER_OPTIONS_TOKEN))
1384
+ ], RequestIdMiddleware);
1385
+
1386
+ // src/server/middlewares/apply-request-id-middleware.ts
1387
+ var ALL_ROUTES = "*";
1388
+ function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
1389
+ consumer.apply(RequestIdMiddleware).forRoutes(routes);
1390
+ }
1391
+ var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
1392
+ function InjectLogger(context) {
1393
+ return (target, propertyKey, parameterIndex) => {
1394
+ if (context === void 0) {
1395
+ Inject(PinoLoggerService)(target, propertyKey, parameterIndex);
1396
+ return;
1397
+ }
1398
+ Inject(getContextLoggerToken(context))(target, propertyKey, parameterIndex);
1399
+ Reflect.defineMetadata(
1400
+ INJECT_LOGGER_CONTEXT_METADATA_KEY,
1401
+ context,
1402
+ target,
1403
+ `${String(propertyKey)}:${parameterIndex}`
1404
+ );
1405
+ };
1406
+ }
1407
+ var LOG_CONTEXT_METADATA_KEY = "bymax_logger:log_context";
1408
+ var LogContext = (name) => SetMetadata(LOG_CONTEXT_METADATA_KEY, name);
1409
+
1410
+ // src/server/decorators/log-performance.decorator.ts
1411
+ var DEFAULT_THRESHOLD_MS = 1e3;
1412
+ function emitTimingLog(logger, method, duration, thresholdMs) {
1413
+ if (duration > thresholdMs) {
1414
+ logger.warnStructured(
1415
+ RESERVED_LOG_KEYS.METHOD_SLOW_EXECUTION,
1416
+ `${method} took ${duration}ms (threshold ${thresholdMs}ms)`,
1417
+ void 0,
1418
+ { method, duration, thresholdMs }
1419
+ );
1420
+ } else {
1421
+ logger.info(RESERVED_LOG_KEYS.METHOD_EXECUTION, `${method} completed`, void 0, {
1422
+ method,
1423
+ duration
1424
+ });
1425
+ }
1426
+ }
1427
+ function LogPerformance(thresholdMs = DEFAULT_THRESHOLD_MS) {
1428
+ return (target, propertyKey, descriptor) => {
1429
+ const original = descriptor.value;
1430
+ const method = `${target.constructor.name}.${String(propertyKey)}`;
1431
+ descriptor.value = async function(...args) {
1432
+ const start = Date.now();
1433
+ try {
1434
+ return await original.apply(this, args);
1435
+ } finally {
1436
+ const duration = Date.now() - start;
1437
+ if (this.logger) {
1438
+ try {
1439
+ emitTimingLog(this.logger, method, duration, thresholdMs);
1440
+ } catch {
1441
+ }
1442
+ }
1443
+ }
1444
+ };
1445
+ return descriptor;
1446
+ };
1447
+ }
1448
+
1449
+ // src/shared/constants/log-keys-convention.constants.ts
1450
+ var LOG_KEYS_CONVENTION_REGEX = /^[A-Z][A-Z0-9_]+_[A-Z][A-Z0-9_]+(_[A-Z][A-Z0-9_]+)?$/;
1451
+
1452
+ export { BymaxLoggerModule, DEFAULT_REDACT_PATHS, DefaultStdoutDestination, HttpExceptionFilter, HttpLoggingInterceptor, InjectLogger, LOGGER_DESTINATIONS_TOKEN, LOGGER_OPTIONS_TOKEN, LOGGER_PINO_INSTANCE_TOKEN, LOG_CONTEXT_METADATA_KEY, LOG_CONTEXT_TOKEN, LOG_KEYS_CONVENTION_REGEX, LogContext, LogContextService, LogPerformance, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RequestIdMiddleware, applyRequestIdMiddleware };