@bymax-one/nest-logger 1.1.0 → 1.2.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.
- package/CHANGELOG.md +309 -1
- package/README.md +222 -87
- package/dist/server/index.cjs +597 -126
- package/dist/server/index.d.cts +234 -38
- package/dist/server/index.d.ts +234 -38
- package/dist/server/index.mjs +596 -128
- package/dist/shared/index.cjs +15 -1
- package/dist/shared/index.d.cts +48 -6
- package/dist/shared/index.d.ts +48 -6
- package/dist/shared/index.mjs +15 -2
- package/package.json +1 -1
package/dist/server/index.cjs
CHANGED
|
@@ -64,6 +64,7 @@ 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",
|
|
67
68
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
68
69
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
69
70
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
@@ -89,7 +90,7 @@ var PINO_LEVEL_NUMBERS = Object.freeze({
|
|
|
89
90
|
debug: 20,
|
|
90
91
|
trace: 10
|
|
91
92
|
});
|
|
92
|
-
Object.freeze(
|
|
93
|
+
var PINO_LEVEL_NAMES = Object.freeze(
|
|
93
94
|
Object.fromEntries(
|
|
94
95
|
Object.entries(PINO_LEVEL_NUMBERS).map(([name, num]) => [num, name])
|
|
95
96
|
// keys of PINO_LEVEL_NUMBERS are always LogLevel
|
|
@@ -126,16 +127,105 @@ function validateOptions(options) {
|
|
|
126
127
|
if (options.maxEntrySizeBytes !== void 0 && options.maxEntrySizeBytes <= 0) {
|
|
127
128
|
throw new Error("[BymaxLoggerModule] options.maxEntrySizeBytes must be > 0");
|
|
128
129
|
}
|
|
130
|
+
if (options.redactStrategy !== void 0 && options.redactStrategy !== "names" && options.redactStrategy !== "paths") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`[BymaxLoggerModule] options.redactStrategy must be 'names' or 'paths'. Got: ${String(
|
|
133
|
+
options.redactStrategy
|
|
134
|
+
)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
// src/server/constants/injection-tokens.constants.ts
|
|
132
140
|
var LOGGER_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_OPTIONS");
|
|
133
141
|
var LOGGER_PINO_INSTANCE_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_PINO_INSTANCE");
|
|
134
142
|
var LOGGER_DESTINATIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_DESTINATIONS");
|
|
143
|
+
var LOGGER_REDACTOR_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_REDACTOR");
|
|
135
144
|
var LOG_CONTEXT_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_LOG_CONTEXT");
|
|
145
|
+
|
|
146
|
+
// src/shared/constants/reserved-log-keys.constants.ts
|
|
147
|
+
var RESERVED_LOG_KEYS = Object.freeze({
|
|
148
|
+
LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK",
|
|
149
|
+
LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING",
|
|
150
|
+
LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK",
|
|
151
|
+
HTTP_REQUEST_START: "HTTP_REQUEST_START",
|
|
152
|
+
HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS",
|
|
153
|
+
HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT",
|
|
154
|
+
HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR",
|
|
155
|
+
HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR",
|
|
156
|
+
HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED",
|
|
157
|
+
HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
|
|
158
|
+
HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
|
|
159
|
+
HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
|
|
160
|
+
METHOD_EXECUTION: "METHOD_EXECUTION",
|
|
161
|
+
METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
|
|
162
|
+
LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
|
|
163
|
+
LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
|
|
164
|
+
LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
|
|
165
|
+
LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
|
|
166
|
+
});
|
|
167
|
+
var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
|
|
168
|
+
/**
|
|
169
|
+
* Superseded before it was ever used. The request lifecycle terminates in one
|
|
170
|
+
* of `HTTP_REQUEST_SUCCESS` / `_REDIRECT` / `_CLIENT_ERROR` / `_SERVER_ERROR`,
|
|
171
|
+
* each carrying the same `duration` a generic "completed" entry would. Emitting
|
|
172
|
+
* it as well would double the terminal access-log volume to say nothing new.
|
|
173
|
+
* It stays claimed so an application cannot repurpose a name that reads like a
|
|
174
|
+
* library event.
|
|
175
|
+
*/
|
|
176
|
+
HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys"
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// src/server/constants/prototype-polluting-keys.constants.ts
|
|
180
|
+
var PROTOTYPE_POLLUTING_KEYS = /* @__PURE__ */ new Set([
|
|
181
|
+
"__proto__",
|
|
182
|
+
"constructor",
|
|
183
|
+
"prototype"
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
// src/server/services/pino-logger.service.ts
|
|
187
|
+
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
188
|
+
function withoutOwnedKeys(metadata) {
|
|
189
|
+
if (metadata === void 0) {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const safe = {};
|
|
194
|
+
for (const key of Object.keys(metadata)) {
|
|
195
|
+
if (!OWNED_PAYLOAD_KEYS.includes(key) && !PROTOTYPE_POLLUTING_KEYS.has(key)) {
|
|
196
|
+
Reflect.set(safe, key, Reflect.get(metadata, key));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return safe;
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
_redactionFailed: true,
|
|
203
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function safeErrorMessage(error) {
|
|
208
|
+
try {
|
|
209
|
+
return String(error.message);
|
|
210
|
+
} catch {
|
|
211
|
+
return "Unreadable error message";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function assignIfDefined(payload, key, value) {
|
|
215
|
+
if (value !== void 0) {
|
|
216
|
+
Reflect.defineProperty(payload, key, { value, enumerable: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
136
219
|
exports.PinoLoggerService = class PinoLoggerService {
|
|
137
|
-
|
|
220
|
+
/**
|
|
221
|
+
* @param pino - The wrapped Pino instance.
|
|
222
|
+
* @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
|
|
223
|
+
* Defaults to the identity so a hand-built instance (benchmarks, unit tests)
|
|
224
|
+
* still works; the module always injects the configured one.
|
|
225
|
+
*/
|
|
226
|
+
constructor(pino2, redact = (value) => value) {
|
|
138
227
|
this.pino = pino2;
|
|
228
|
+
this.redact = redact;
|
|
139
229
|
}
|
|
140
230
|
// ─── NestJS LoggerService variadic interface ──────────────────────────────
|
|
141
231
|
/**
|
|
@@ -158,18 +248,16 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
158
248
|
*/
|
|
159
249
|
error(message, ...optionalParams) {
|
|
160
250
|
if (message instanceof Error) {
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
);
|
|
251
|
+
const payload2 = { err: this.serializeError(message) };
|
|
252
|
+
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
253
|
+
this.pino.error(payload2, safeErrorMessage(message));
|
|
165
254
|
return;
|
|
166
255
|
}
|
|
167
256
|
const stack = typeof optionalParams[0] === "string" ? optionalParams[0] : void 0;
|
|
168
257
|
const context = typeof optionalParams[1] === "string" ? optionalParams[1] : this.context;
|
|
169
|
-
const payload = {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
258
|
+
const payload = {};
|
|
259
|
+
assignIfDefined(payload, "context", context);
|
|
260
|
+
assignIfDefined(payload, "stack", stack);
|
|
173
261
|
this.pino.error(payload, typeof message === "string" ? message : String(message));
|
|
174
262
|
}
|
|
175
263
|
/**
|
|
@@ -244,10 +332,14 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
244
332
|
* @param metadata - Optional extra structured fields.
|
|
245
333
|
*/
|
|
246
334
|
errorStructured(logKey, error, userId, metadata) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
335
|
+
const payload = {
|
|
336
|
+
...withoutOwnedKeys(metadata),
|
|
337
|
+
logKey,
|
|
338
|
+
err: this.serializeError(error)
|
|
339
|
+
};
|
|
340
|
+
assignIfDefined(payload, "userId", userId);
|
|
341
|
+
assignIfDefined(payload, "context", this.context);
|
|
342
|
+
this.pino.error(payload, safeErrorMessage(error));
|
|
251
343
|
}
|
|
252
344
|
// ─── Helpers / escape hatches ─────────────────────────────────────────────
|
|
253
345
|
/**
|
|
@@ -280,7 +372,8 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
280
372
|
* @returns A new `PinoLoggerService` wrapping the Pino child.
|
|
281
373
|
*/
|
|
282
374
|
child(bindings) {
|
|
283
|
-
const
|
|
375
|
+
const safeBindings = this.redact(bindings, true);
|
|
376
|
+
const childService = new exports.PinoLoggerService(this.pino.child(safeBindings), this.redact);
|
|
284
377
|
if (this.context !== void 0) {
|
|
285
378
|
childService.setContext(this.context);
|
|
286
379
|
}
|
|
@@ -305,7 +398,11 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
305
398
|
* dashboards for every exception logged through `HttpExceptionFilter`.
|
|
306
399
|
*/
|
|
307
400
|
serializeError(error) {
|
|
308
|
-
|
|
401
|
+
try {
|
|
402
|
+
return { type: error.name, message: error.message, stack: error.stack };
|
|
403
|
+
} catch {
|
|
404
|
+
return { type: "SanitizeFailed", message: "Failed to read the thrown value" };
|
|
405
|
+
}
|
|
309
406
|
}
|
|
310
407
|
/** Resolve the context: last string param wins, else the instance context. */
|
|
311
408
|
resolveContext(optionalParams) {
|
|
@@ -314,12 +411,9 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
314
411
|
}
|
|
315
412
|
/** Emit a structured payload at info or warn level. */
|
|
316
413
|
emitStructured(level, logKey, message, userId, metadata) {
|
|
317
|
-
const payload = {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
userId,
|
|
321
|
-
context: this.context
|
|
322
|
-
};
|
|
414
|
+
const payload = { ...withoutOwnedKeys(metadata), logKey };
|
|
415
|
+
assignIfDefined(payload, "userId", userId);
|
|
416
|
+
assignIfDefined(payload, "context", this.context);
|
|
323
417
|
if (level === "info") {
|
|
324
418
|
this.pino.info(payload, message);
|
|
325
419
|
} else {
|
|
@@ -328,7 +422,8 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
328
422
|
}
|
|
329
423
|
/** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
|
|
330
424
|
emitNestStyle(level, message, optionalParams) {
|
|
331
|
-
const payload = {
|
|
425
|
+
const payload = {};
|
|
426
|
+
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
332
427
|
const msg = typeof message === "string" ? message : String(message);
|
|
333
428
|
switch (level) {
|
|
334
429
|
case "info":
|
|
@@ -351,7 +446,8 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
351
446
|
};
|
|
352
447
|
exports.PinoLoggerService = __decorateClass([
|
|
353
448
|
common.Injectable(),
|
|
354
|
-
__decorateParam(0, common.Inject(LOGGER_PINO_INSTANCE_TOKEN))
|
|
449
|
+
__decorateParam(0, common.Inject(LOGGER_PINO_INSTANCE_TOKEN)),
|
|
450
|
+
__decorateParam(1, common.Inject(LOGGER_REDACTOR_TOKEN))
|
|
355
451
|
], exports.PinoLoggerService);
|
|
356
452
|
|
|
357
453
|
// src/server/decorators/inject-logger.provider.ts
|
|
@@ -401,26 +497,6 @@ exports.DefaultStdoutDestination = __decorateClass([
|
|
|
401
497
|
common.Injectable()
|
|
402
498
|
], exports.DefaultStdoutDestination);
|
|
403
499
|
|
|
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
500
|
// src/server/utils/normalize-url.util.ts
|
|
425
501
|
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
502
|
var ULID_REGEX = /\/[0-9A-HJKMNP-TV-Z]{26}(?![0-9A-HJKMNP-TV-Z])/g;
|
|
@@ -561,11 +637,39 @@ exports.HttpExceptionFilter = __decorateClass([
|
|
|
561
637
|
common.Catch(),
|
|
562
638
|
__decorateParam(0, common.Inject(exports.PinoLoggerService))
|
|
563
639
|
], exports.HttpExceptionFilter);
|
|
564
|
-
|
|
640
|
+
|
|
641
|
+
// src/server/utils/http-log-state.util.ts
|
|
565
642
|
var HTTP_REDIRECT_MIN = 300;
|
|
566
643
|
var HTTP_CLIENT_ERROR_MIN = 400;
|
|
567
644
|
var HTTP_SERVER_ERROR_MIN = 500;
|
|
568
645
|
var UNKNOWN_USER_AGENT = "unknown";
|
|
646
|
+
var RECORDER_ACTIVE = /* @__PURE__ */ Symbol("bymax-one/nest-logger:recorder");
|
|
647
|
+
var RECORDED_ERROR = /* @__PURE__ */ Symbol("bymax-one/nest-logger:error");
|
|
648
|
+
function markRecorderActive(req) {
|
|
649
|
+
Reflect.set(req, RECORDER_ACTIVE, true);
|
|
650
|
+
}
|
|
651
|
+
function isRecorderActive(req) {
|
|
652
|
+
return Reflect.get(req, RECORDER_ACTIVE) === true;
|
|
653
|
+
}
|
|
654
|
+
function recordError(req, error) {
|
|
655
|
+
Reflect.set(req, RECORDED_ERROR, { error });
|
|
656
|
+
}
|
|
657
|
+
function readRecordedError(req) {
|
|
658
|
+
return Reflect.get(req, RECORDED_ERROR);
|
|
659
|
+
}
|
|
660
|
+
function toError(thrown) {
|
|
661
|
+
return thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
662
|
+
}
|
|
663
|
+
function readUserId(req) {
|
|
664
|
+
return req.user?.sub ?? req.user?.id;
|
|
665
|
+
}
|
|
666
|
+
function readUserAgent(req) {
|
|
667
|
+
const raw = req.headers["user-agent"];
|
|
668
|
+
return typeof raw === "string" ? raw : UNKNOWN_USER_AGENT;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/server/interceptors/http-logging.interceptor.ts
|
|
672
|
+
var HTTP_SUCCESS_MIN = 200;
|
|
569
673
|
exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
|
|
570
674
|
/**
|
|
571
675
|
* @param logger - The structured logger the lifecycle entries are written to.
|
|
@@ -599,13 +703,15 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
|
|
|
599
703
|
const http = context.switchToHttp();
|
|
600
704
|
const req = http.getRequest();
|
|
601
705
|
const res = http.getResponse();
|
|
706
|
+
if (isRecorderActive(req)) {
|
|
707
|
+
return this.recordOnly(req, next);
|
|
708
|
+
}
|
|
602
709
|
const { method, url, ip } = req;
|
|
603
710
|
if (this.isExcluded(stripQueryString(url))) {
|
|
604
711
|
return next.handle();
|
|
605
712
|
}
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const userId = req.user?.sub ?? req.user?.id;
|
|
713
|
+
const userAgent = readUserAgent(req);
|
|
714
|
+
const userId = readUserId(req);
|
|
609
715
|
const normalizedUrl = normalizeUrl(url);
|
|
610
716
|
const start = Date.now();
|
|
611
717
|
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
|
|
@@ -628,6 +734,28 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
|
|
|
628
734
|
})
|
|
629
735
|
);
|
|
630
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Hand the lifecycle to the access-log middleware, recording only the thrown
|
|
739
|
+
* error for its terminal entry.
|
|
740
|
+
*
|
|
741
|
+
* The interceptor cannot be the recorder: it never runs for a request a guard
|
|
742
|
+
* rejected or one that matched no route. It also cannot judge DELIVERY — at the
|
|
743
|
+
* moment it completes, the response has not been flushed yet, which is why an
|
|
744
|
+
* aborted request used to be logged as the 200 the handler intended. What it
|
|
745
|
+
* uniquely has is the thrown value, so that is all it contributes.
|
|
746
|
+
*
|
|
747
|
+
* @param req - The request whose lifecycle the middleware owns.
|
|
748
|
+
* @param next - The downstream call handler.
|
|
749
|
+
* @returns The response stream, unmodified and with exceptions re-thrown.
|
|
750
|
+
*/
|
|
751
|
+
recordOnly(req, next) {
|
|
752
|
+
return next.handle().pipe(
|
|
753
|
+
rxjs.catchError((err) => {
|
|
754
|
+
recordError(req, toError(err));
|
|
755
|
+
return rxjs.throwError(() => err);
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
}
|
|
631
759
|
/**
|
|
632
760
|
* Log a non-throwing completion: SUCCESS for 2xx, REDIRECT for 3xx, nothing
|
|
633
761
|
* for any other status (those are surfaced via the error path instead).
|
|
@@ -686,12 +814,12 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
|
|
|
686
814
|
}
|
|
687
815
|
return;
|
|
688
816
|
}
|
|
689
|
-
this.logger.errorStructured(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
);
|
|
817
|
+
this.logger.errorStructured(RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR, toError(err), userId, {
|
|
818
|
+
method,
|
|
819
|
+
url,
|
|
820
|
+
statusCode: common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
821
|
+
duration
|
|
822
|
+
});
|
|
695
823
|
}
|
|
696
824
|
};
|
|
697
825
|
exports.HttpLoggingInterceptor = __decorateClass([
|
|
@@ -726,48 +854,6 @@ var {
|
|
|
726
854
|
// disabled" without a dead nullish branch.
|
|
727
855
|
global: extras.isGlobal !== false
|
|
728
856
|
})).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
857
|
|
|
772
858
|
// src/server/constants/default-redact-paths.constants.ts
|
|
773
859
|
var REDACT_MAX_DEPTH = 4;
|
|
@@ -810,7 +896,20 @@ var REDACT_COMMON_FIELDS = [
|
|
|
810
896
|
"signingSecret",
|
|
811
897
|
"privateKey",
|
|
812
898
|
// Conservative PII (1) — consumer can disable if logging email is justified
|
|
813
|
-
"email"
|
|
899
|
+
"email",
|
|
900
|
+
// Credential-bearing HTTP header names (5). These also appear in
|
|
901
|
+
// {@link REDACT_ABSOLUTE_PATHS}, pinned to the `req.headers` / `res.headers`
|
|
902
|
+
// shapes — which covered ONLY those two shapes. A headers bag logged under any
|
|
903
|
+
// other key (`logger.info(k, m, u, { headers: req.headers })`, one of the most
|
|
904
|
+
// common things written while debugging an integration) wrote the bearer token
|
|
905
|
+
// in clear. Listed here as bare names, they are caught wherever they appear.
|
|
906
|
+
// Node lower-cases inbound header names, so the lower-case spelling is the one
|
|
907
|
+
// that can actually reach a log.
|
|
908
|
+
"authorization",
|
|
909
|
+
"cookie",
|
|
910
|
+
"set-cookie",
|
|
911
|
+
"x-api-key",
|
|
912
|
+
"x-auth-token"
|
|
814
913
|
];
|
|
815
914
|
var REDACT_ABSOLUTE_PATHS = [
|
|
816
915
|
"req.headers.authorization",
|
|
@@ -824,6 +923,48 @@ var DEFAULT_REDACT_PATHS = [
|
|
|
824
923
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
825
924
|
...REDACT_ABSOLUTE_PATHS
|
|
826
925
|
];
|
|
926
|
+
function detectOtelTraceApi() {
|
|
927
|
+
try {
|
|
928
|
+
const requireFromCwd = module$1.createRequire(path.join(process.cwd(), "noop.cjs"));
|
|
929
|
+
const mod = requireFromCwd("@opentelemetry/api");
|
|
930
|
+
return mod.trace;
|
|
931
|
+
} catch {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function isValidTraceId(traceId) {
|
|
936
|
+
return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
937
|
+
}
|
|
938
|
+
function isValidSpanId(spanId) {
|
|
939
|
+
return /^[0-9a-f]{16}$/.test(spanId) && !/^0+$/.test(spanId);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/server/mixins/trace-context.mixin.ts
|
|
943
|
+
function createTraceContextMixin(logContext, opts) {
|
|
944
|
+
const traceApi = opts.shouldAutoInjectTraceContext ? detectOtelTraceApi() : void 0;
|
|
945
|
+
return function mixin(_mergeObject, _level, _logger) {
|
|
946
|
+
const merged = {};
|
|
947
|
+
const store = logContext.getStore();
|
|
948
|
+
if (store) {
|
|
949
|
+
Object.assign(merged, store);
|
|
950
|
+
}
|
|
951
|
+
if (traceApi) {
|
|
952
|
+
const span = traceApi.getActiveSpan();
|
|
953
|
+
if (span) {
|
|
954
|
+
const ctx = span.spanContext();
|
|
955
|
+
if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
|
|
956
|
+
Reflect.set(merged, opts.traceIdField, ctx.traceId);
|
|
957
|
+
Reflect.set(merged, opts.spanIdField, ctx.spanId);
|
|
958
|
+
if (typeof ctx.traceFlags === "number") {
|
|
959
|
+
const flagsByte = ctx.traceFlags & 255;
|
|
960
|
+
Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return merged;
|
|
966
|
+
};
|
|
967
|
+
}
|
|
827
968
|
|
|
828
969
|
// src/server/utils/compile-redact-paths.util.ts
|
|
829
970
|
function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
@@ -872,6 +1013,120 @@ function destinationToStream(destination) {
|
|
|
872
1013
|
});
|
|
873
1014
|
}
|
|
874
1015
|
|
|
1016
|
+
// src/server/utils/redact-by-name.util.ts
|
|
1017
|
+
var REDACT_MAX_TRAVERSAL_DEPTH = 100;
|
|
1018
|
+
var REDACT_DEPTH_EXCEEDED = "[REDACTION_DEPTH_EXCEEDED]";
|
|
1019
|
+
var REDACT_CIRCULAR = "[Circular]";
|
|
1020
|
+
function resolveToJson(value) {
|
|
1021
|
+
let getter;
|
|
1022
|
+
let candidate;
|
|
1023
|
+
let current = value;
|
|
1024
|
+
while (current !== null) {
|
|
1025
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, "toJSON");
|
|
1026
|
+
if (descriptor !== void 0) {
|
|
1027
|
+
getter = descriptor.get;
|
|
1028
|
+
candidate = getter === void 0 ? descriptor.value : getter.call(value);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
current = Object.getPrototypeOf(current);
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
method: typeof candidate === "function" ? candidate : void 0,
|
|
1035
|
+
fromAccessor: getter !== void 0
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function cloneError(error) {
|
|
1039
|
+
const clone = Object.create(
|
|
1040
|
+
Object.getPrototypeOf(error),
|
|
1041
|
+
Object.getOwnPropertyDescriptors(error)
|
|
1042
|
+
);
|
|
1043
|
+
const { stack } = error;
|
|
1044
|
+
if (typeof stack === "string") {
|
|
1045
|
+
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
1046
|
+
}
|
|
1047
|
+
return clone;
|
|
1048
|
+
}
|
|
1049
|
+
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1050
|
+
var WRITE_FAILED = new Error();
|
|
1051
|
+
function defineOwn(target, key, value) {
|
|
1052
|
+
if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
|
|
1053
|
+
throw WRITE_FAILED;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1057
|
+
const length = value.length;
|
|
1058
|
+
const copy = [];
|
|
1059
|
+
for (let index = 0; index < length; index++) {
|
|
1060
|
+
Reflect.set(
|
|
1061
|
+
copy,
|
|
1062
|
+
index,
|
|
1063
|
+
walk(Reflect.get(value, index), sensitive, censor, depth2 + 1, ancestors)
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
return copy;
|
|
1067
|
+
}
|
|
1068
|
+
function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
|
|
1069
|
+
if (value === null) {
|
|
1070
|
+
return value;
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof value !== "object" && typeof value !== "function") {
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
if (depth2 > REDACT_MAX_TRAVERSAL_DEPTH) {
|
|
1076
|
+
return REDACT_DEPTH_EXCEEDED;
|
|
1077
|
+
}
|
|
1078
|
+
if (ancestors.has(value)) {
|
|
1079
|
+
return REDACT_CIRCULAR;
|
|
1080
|
+
}
|
|
1081
|
+
const resolvedToJson = resolveToJson(value);
|
|
1082
|
+
if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
|
|
1083
|
+
return value;
|
|
1084
|
+
}
|
|
1085
|
+
ancestors.add(value);
|
|
1086
|
+
try {
|
|
1087
|
+
const toJson = honorToJson ? resolvedToJson.method : void 0;
|
|
1088
|
+
if (toJson !== void 0) {
|
|
1089
|
+
if (Object.keys(value).some((key) => sensitive.has(key.toLowerCase()))) {
|
|
1090
|
+
return censor;
|
|
1091
|
+
}
|
|
1092
|
+
return walk(toJson.call(value), sensitive, censor, depth2 + 1, ancestors);
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof value === "function") {
|
|
1095
|
+
return value;
|
|
1096
|
+
}
|
|
1097
|
+
if (Array.isArray(value)) {
|
|
1098
|
+
return walkArray(value, sensitive, censor, depth2, ancestors);
|
|
1099
|
+
}
|
|
1100
|
+
const source = value;
|
|
1101
|
+
const keys = Object.keys(source);
|
|
1102
|
+
const copy = value instanceof Error ? cloneError(value) : {};
|
|
1103
|
+
for (const key of keys) {
|
|
1104
|
+
const current = Reflect.get(source, key);
|
|
1105
|
+
defineOwn(
|
|
1106
|
+
copy,
|
|
1107
|
+
key,
|
|
1108
|
+
sensitive.has(key.toLowerCase()) ? censor : walk(current, sensitive, censor, depth2 + 1, ancestors)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
return copy;
|
|
1112
|
+
} finally {
|
|
1113
|
+
ancestors.delete(value);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function createNameRedactor(fieldNames, censor) {
|
|
1117
|
+
const sensitive = new Set(fieldNames.map((name) => name.toLowerCase()));
|
|
1118
|
+
return (value, isRecordRoot = false) => {
|
|
1119
|
+
try {
|
|
1120
|
+
return walk(value, sensitive, censor, 0, /* @__PURE__ */ new Set(), !isRecordRoot);
|
|
1121
|
+
} catch {
|
|
1122
|
+
return {
|
|
1123
|
+
_redactionFailed: true,
|
|
1124
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
875
1130
|
// src/server/utils/truncate-large-entries.ts
|
|
876
1131
|
var PREVIEW_LENGTH = 200;
|
|
877
1132
|
function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
@@ -900,25 +1155,56 @@ function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
|
900
1155
|
}
|
|
901
1156
|
|
|
902
1157
|
// src/server/pino-factory.ts
|
|
1158
|
+
var PATH_SEGMENT = /\[\s*['"]([^'"]+)['"]\s*\]|([^.[\]]+)/g;
|
|
1159
|
+
var INDEX_SEGMENT = /^\d+$/;
|
|
1160
|
+
function leafNameOf(path) {
|
|
1161
|
+
let leaf;
|
|
1162
|
+
for (const match of path.matchAll(PATH_SEGMENT)) {
|
|
1163
|
+
const quoted = match[1];
|
|
1164
|
+
const name = quoted ?? (match[2] !== void 0 && INDEX_SEGMENT.test(match[2]) ? void 0 : match[2]);
|
|
1165
|
+
if (name !== void 0 && name !== "*") {
|
|
1166
|
+
leaf = name;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return leaf;
|
|
1170
|
+
}
|
|
1171
|
+
var PASS_THROUGH = (value) => value;
|
|
1172
|
+
function resolveNameRedactor(options) {
|
|
1173
|
+
if (options.redactStrategy !== "names") {
|
|
1174
|
+
return PASS_THROUGH;
|
|
1175
|
+
}
|
|
1176
|
+
const names = [
|
|
1177
|
+
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1178
|
+
...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
|
|
1179
|
+
];
|
|
1180
|
+
if (names.length === 0) {
|
|
1181
|
+
return PASS_THROUGH;
|
|
1182
|
+
}
|
|
1183
|
+
return createNameRedactor(names, options.redactCensor);
|
|
1184
|
+
}
|
|
1185
|
+
function resolveRedactOption(options) {
|
|
1186
|
+
const paths = options.redactStrategy === "paths" ? compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact) : [...new Set(options.redactPaths)];
|
|
1187
|
+
if (paths.length === 0) {
|
|
1188
|
+
return void 0;
|
|
1189
|
+
}
|
|
1190
|
+
return { paths, censor: options.redactCensor };
|
|
1191
|
+
}
|
|
903
1192
|
function buildPinoInstance(options, logContext, destinations) {
|
|
904
1193
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1194
|
+
const redact = resolveNameRedactor(options);
|
|
1195
|
+
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
905
1196
|
const serializers = {
|
|
906
|
-
err:
|
|
1197
|
+
err: bound(pino__default.default.stdSerializers.err),
|
|
907
1198
|
...Object.fromEntries(
|
|
908
1199
|
Object.entries(options.serializers).map(
|
|
909
|
-
([key, serializer]) => [
|
|
910
|
-
key,
|
|
911
|
-
createSizeBoundedSerializer(serializer, maxBytes)
|
|
912
|
-
]
|
|
1200
|
+
([key, serializer]) => [key, bound(serializer)]
|
|
913
1201
|
)
|
|
914
1202
|
)
|
|
915
1203
|
};
|
|
1204
|
+
const redactOption = resolveRedactOption(options);
|
|
916
1205
|
const pinoOptions = {
|
|
917
1206
|
level: options.level,
|
|
918
|
-
redact:
|
|
919
|
-
paths: compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact),
|
|
920
|
-
censor: options.redactCensor
|
|
921
|
-
},
|
|
1207
|
+
...redactOption === void 0 ? {} : { redact: redactOption },
|
|
922
1208
|
base: { service: options.service },
|
|
923
1209
|
// Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
|
|
924
1210
|
timestamp: () => `,"time":"${options.timestamp()}"`,
|
|
@@ -926,10 +1212,42 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
926
1212
|
// Emit the level as a string label instead of the numeric code — easier
|
|
927
1213
|
// for log aggregators. Trace context is injected via `mixin`, never via
|
|
928
1214
|
// `formatters.log` (which cannot see ambient ALS / OTel state).
|
|
929
|
-
level: (label) => ({ level: label })
|
|
1215
|
+
level: (label) => ({ level: label }),
|
|
1216
|
+
// `base` bindings never reach `formatters.log` — Pino pre-serializes them
|
|
1217
|
+
// into the instance's `chindings`, the same reason `child()` has to redact
|
|
1218
|
+
// its own. That matters because `service` is CONSUMER-supplied and
|
|
1219
|
+
// `applyDefaults` keeps whatever it was given: a `{ name, version, apiKey }`
|
|
1220
|
+
// went out in clear, where the path expansion had covered it via `*.*.apiKey`.
|
|
1221
|
+
// Redacting here rather than trimming to the two declared fields keeps a
|
|
1222
|
+
// consumer's extra metadata instead of silently dropping it. Runs ONCE, at
|
|
1223
|
+
// logger construction — no per-entry cost. `true`: bindings are a record
|
|
1224
|
+
// root, iterated rather than serialized.
|
|
1225
|
+
bindings: (bindings) => redact(bindings, true),
|
|
1226
|
+
// The name-walk redactor runs here, on the merged record (mixin output +
|
|
1227
|
+
// the caller's object), which is exactly the surface a caller controls.
|
|
1228
|
+
// `base` and child bindings are NOT visible at this hook — they are
|
|
1229
|
+
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1230
|
+
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1231
|
+
log: (record) => redact(record, true)
|
|
930
1232
|
},
|
|
931
1233
|
serializers,
|
|
932
|
-
mixin: createTraceContextMixin(logContext, options.otel)
|
|
1234
|
+
mixin: createTraceContextMixin(logContext, options.otel),
|
|
1235
|
+
// `formatters.log` is NOT the first code to touch the caller's object: Pino
|
|
1236
|
+
// merges the mixin result with it first, and the default strategy's
|
|
1237
|
+
// `Object.assign` invokes every own getter. A throwing getter therefore
|
|
1238
|
+
// crashed the log call before the redactor could return its fail-closed
|
|
1239
|
+
// envelope. Owning the merge moves that boundary inside the guarantee.
|
|
1240
|
+
mixinMergeStrategy: (mergeObject, mixinObject) => {
|
|
1241
|
+
try {
|
|
1242
|
+
return Object.assign({}, mixinObject, mergeObject);
|
|
1243
|
+
} catch {
|
|
1244
|
+
return {
|
|
1245
|
+
...mixinObject,
|
|
1246
|
+
_redactionFailed: true,
|
|
1247
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
933
1251
|
};
|
|
934
1252
|
const streams = destinations.map((destination) => ({
|
|
935
1253
|
level: destination.minLevel ?? options.level,
|
|
@@ -945,9 +1263,10 @@ var DestinationRegistry = class {
|
|
|
945
1263
|
* @param logger - Structured logger used to report init failures. Injected by
|
|
946
1264
|
* token explicitly for the same bundling reason.
|
|
947
1265
|
*/
|
|
948
|
-
constructor(registered, logger) {
|
|
1266
|
+
constructor(registered, logger, options) {
|
|
949
1267
|
this.registered = registered;
|
|
950
1268
|
this.logger = logger;
|
|
1269
|
+
this.options = options;
|
|
951
1270
|
/**
|
|
952
1271
|
* Destinations that initialized successfully, in registration order.
|
|
953
1272
|
*
|
|
@@ -979,6 +1298,35 @@ var DestinationRegistry = class {
|
|
|
979
1298
|
);
|
|
980
1299
|
}
|
|
981
1300
|
}
|
|
1301
|
+
this.announceBootstrap();
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1305
|
+
* initialized.
|
|
1306
|
+
*
|
|
1307
|
+
* Ordering is the whole point of doing this here rather than from an eagerly
|
|
1308
|
+
* instantiated provider factory: a provider factory runs while Nest is still
|
|
1309
|
+
* building the graph, before `onModuleInit`, so a sink that only accepts
|
|
1310
|
+
* writes once its own `onInit()` has run would drop these entries — including
|
|
1311
|
+
* `LOGGER_BOOTSTRAP_WARNING`, which exists precisely so a security review can
|
|
1312
|
+
* see that PII redaction was turned off. A signal that a custom destination
|
|
1313
|
+
* can silently swallow is not an audit trail. Emitting from the registry that
|
|
1314
|
+
* owns destination initialization makes the ordering structural instead of a
|
|
1315
|
+
* cross-provider hook-order assumption.
|
|
1316
|
+
*/
|
|
1317
|
+
announceBootstrap() {
|
|
1318
|
+
this.logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
|
|
1319
|
+
if (this.options.shouldDisableDefaultRedact) {
|
|
1320
|
+
this.logger.warnStructured(
|
|
1321
|
+
RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_WARNING,
|
|
1322
|
+
"Default PII redaction is DISABLED \u2014 sensitive fields will be logged verbatim unless every one of them is listed in options.redactPaths",
|
|
1323
|
+
void 0,
|
|
1324
|
+
{
|
|
1325
|
+
shouldDisableDefaultRedact: true,
|
|
1326
|
+
redactPathCount: this.options.redactPaths.length
|
|
1327
|
+
}
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
982
1330
|
}
|
|
983
1331
|
/**
|
|
984
1332
|
* Flush and close active destinations in REVERSE registration order, so a
|
|
@@ -987,6 +1335,15 @@ var DestinationRegistry = class {
|
|
|
987
1335
|
* point in the shutdown sequence.
|
|
988
1336
|
*/
|
|
989
1337
|
async onApplicationShutdown() {
|
|
1338
|
+
this.logger.info(
|
|
1339
|
+
RESERVED_LOG_KEYS.LOGGER_SHUTDOWN_OK,
|
|
1340
|
+
"BymaxLoggerModule shutting down",
|
|
1341
|
+
void 0,
|
|
1342
|
+
{ destinations: this.active.length }
|
|
1343
|
+
);
|
|
1344
|
+
await new Promise((resolve) => {
|
|
1345
|
+
setImmediate(resolve);
|
|
1346
|
+
});
|
|
990
1347
|
for (const destination of [...this.active].reverse()) {
|
|
991
1348
|
try {
|
|
992
1349
|
await destination.onShutdown?.();
|
|
@@ -1011,9 +1368,10 @@ var DestinationRegistry = class {
|
|
|
1011
1368
|
DestinationRegistry = __decorateClass([
|
|
1012
1369
|
common.Injectable(),
|
|
1013
1370
|
__decorateParam(0, common.Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1014
|
-
__decorateParam(1, common.Inject(exports.PinoLoggerService))
|
|
1371
|
+
__decorateParam(1, common.Inject(exports.PinoLoggerService)),
|
|
1372
|
+
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
|
|
1015
1373
|
], DestinationRegistry);
|
|
1016
|
-
var FORBIDDEN_CONTEXT_KEYS =
|
|
1374
|
+
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1017
1375
|
exports.LogContextService = class LogContextService {
|
|
1018
1376
|
constructor() {
|
|
1019
1377
|
this.als = new async_hooks.AsyncLocalStorage();
|
|
@@ -1099,24 +1457,21 @@ exports.LogContextService = __decorateClass([
|
|
|
1099
1457
|
], exports.LogContextService);
|
|
1100
1458
|
|
|
1101
1459
|
// src/server/logger.module.ts
|
|
1102
|
-
var LOGGER_BOOTSTRAP_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_BOOTSTRAP");
|
|
1103
1460
|
function resolveDestinations(options) {
|
|
1104
1461
|
return options.destinations.length > 0 ? options.destinations : [new exports.DefaultStdoutDestination()];
|
|
1105
1462
|
}
|
|
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
1463
|
function buildCommonProviders() {
|
|
1117
1464
|
return [
|
|
1118
1465
|
exports.LogContextService,
|
|
1119
1466
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: exports.LogContextService },
|
|
1467
|
+
{
|
|
1468
|
+
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1469
|
+
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
1470
|
+
// Pino pre-serializes child bindings before `formatters.log` can see them.
|
|
1471
|
+
provide: LOGGER_REDACTOR_TOKEN,
|
|
1472
|
+
useFactory: (options) => resolveNameRedactor(options),
|
|
1473
|
+
inject: [LOGGER_OPTIONS_TOKEN]
|
|
1474
|
+
},
|
|
1120
1475
|
{
|
|
1121
1476
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1122
1477
|
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
@@ -1132,8 +1487,7 @@ function buildCommonProviders() {
|
|
|
1132
1487
|
// exported — consumers never inject it directly.
|
|
1133
1488
|
DestinationRegistry,
|
|
1134
1489
|
// One child-logger provider per context discovered from @InjectLogger(context).
|
|
1135
|
-
...collectContextLoggerProviders()
|
|
1136
|
-
bootstrapProvider()
|
|
1490
|
+
...collectContextLoggerProviders()
|
|
1137
1491
|
];
|
|
1138
1492
|
}
|
|
1139
1493
|
function asyncHttpInterceptorProvider() {
|
|
@@ -1153,6 +1507,7 @@ function augmentLoggerModule(base, providers) {
|
|
|
1153
1507
|
LOGGER_OPTIONS_TOKEN,
|
|
1154
1508
|
LOGGER_PINO_INSTANCE_TOKEN,
|
|
1155
1509
|
LOGGER_DESTINATIONS_TOKEN,
|
|
1510
|
+
LOGGER_REDACTOR_TOKEN,
|
|
1156
1511
|
LOG_CONTEXT_TOKEN,
|
|
1157
1512
|
exports.LogContextService,
|
|
1158
1513
|
exports.PinoLoggerService,
|
|
@@ -1343,6 +1698,119 @@ var PrettyDevDestination = class {
|
|
|
1343
1698
|
});
|
|
1344
1699
|
}
|
|
1345
1700
|
};
|
|
1701
|
+
var HttpAccessLogMiddleware = class {
|
|
1702
|
+
/**
|
|
1703
|
+
* @param logger - The structured logger the access entries are written to.
|
|
1704
|
+
* Injected by token explicitly: the package is bundled without
|
|
1705
|
+
* `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
|
|
1706
|
+
* @param options - Resolved module options (read for `http.excludePaths`).
|
|
1707
|
+
*/
|
|
1708
|
+
constructor(logger, options) {
|
|
1709
|
+
this.logger = logger;
|
|
1710
|
+
this.isEnabled = options.http.isEnabled;
|
|
1711
|
+
this.excludePaths = options.http.excludePaths;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Emit START, arm the terminal entry on `'close'`, and continue the chain.
|
|
1715
|
+
*
|
|
1716
|
+
* @param req - The incoming request (Express satisfies the contract).
|
|
1717
|
+
* @param res - The outgoing response (Express satisfies the contract).
|
|
1718
|
+
* @param next - The next handler in the chain.
|
|
1719
|
+
*/
|
|
1720
|
+
use(req, res, next) {
|
|
1721
|
+
if (!this.isEnabled) {
|
|
1722
|
+
next();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const target = req.originalUrl ?? req.url;
|
|
1726
|
+
const path = stripQueryString(target);
|
|
1727
|
+
if (this.excludePaths.some((pattern) => pattern.test(path))) {
|
|
1728
|
+
next();
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
markRecorderActive(req);
|
|
1732
|
+
const method = req.method;
|
|
1733
|
+
const url = normalizeUrl(target);
|
|
1734
|
+
const start = Date.now();
|
|
1735
|
+
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${url}`, readUserId(req), {
|
|
1736
|
+
method,
|
|
1737
|
+
url,
|
|
1738
|
+
// Query string stripped: it may carry secrets (reset tokens, OAuth codes)
|
|
1739
|
+
// that no key-name redaction can scrub out of a string VALUE.
|
|
1740
|
+
fullUrl: path,
|
|
1741
|
+
// Conventional for an access log and personal data under GDPR/LGPD. Its
|
|
1742
|
+
// fidelity is the app's to establish: behind a proxy it is only the real
|
|
1743
|
+
// client when the app configures `trust proxy`, and a value the app has not
|
|
1744
|
+
// established is attacker-chosen. Suppress with `redactPaths: ['ip']`.
|
|
1745
|
+
ip: req.ip,
|
|
1746
|
+
userAgent: readUserAgent(req)
|
|
1747
|
+
});
|
|
1748
|
+
res.on(
|
|
1749
|
+
"close",
|
|
1750
|
+
async_hooks.AsyncResource.bind(() => {
|
|
1751
|
+
this.logTerminal(req, res, method, url, Date.now() - start);
|
|
1752
|
+
})
|
|
1753
|
+
);
|
|
1754
|
+
next();
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Emit the terminal entry for a closed request.
|
|
1758
|
+
*
|
|
1759
|
+
* `userId` is read HERE rather than at START because authentication runs in a
|
|
1760
|
+
* guard, downstream of this middleware — at START there is no principal yet.
|
|
1761
|
+
*
|
|
1762
|
+
* @param req - The request, carrying any error the interceptor recorded.
|
|
1763
|
+
* @param res - The closed response.
|
|
1764
|
+
* @param method - The HTTP method.
|
|
1765
|
+
* @param url - The normalized request URL.
|
|
1766
|
+
* @param duration - Elapsed milliseconds.
|
|
1767
|
+
*/
|
|
1768
|
+
logTerminal(req, res, method, url, duration) {
|
|
1769
|
+
const statusCode = res.statusCode;
|
|
1770
|
+
const userId = readUserId(req);
|
|
1771
|
+
const meta = { method, url, statusCode, duration };
|
|
1772
|
+
if (!res.writableFinished) {
|
|
1773
|
+
this.logger.warnStructured(
|
|
1774
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_ABORTED,
|
|
1775
|
+
`${method} ${url} \u2192 ${statusCode} not delivered (${duration}ms)`,
|
|
1776
|
+
userId,
|
|
1777
|
+
meta
|
|
1778
|
+
);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (statusCode >= HTTP_SERVER_ERROR_MIN) {
|
|
1782
|
+
const recorded = readRecordedError(req);
|
|
1783
|
+
this.logger.errorStructured(
|
|
1784
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR,
|
|
1785
|
+
recorded?.error ?? new Error(`${method} ${url} \u2192 ${statusCode}`),
|
|
1786
|
+
userId,
|
|
1787
|
+
meta
|
|
1788
|
+
);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (statusCode >= HTTP_CLIENT_ERROR_MIN) {
|
|
1792
|
+
const recorded = readRecordedError(req);
|
|
1793
|
+
this.logger.warnStructured(
|
|
1794
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_CLIENT_ERROR,
|
|
1795
|
+
`${method} ${url} \u2192 ${statusCode}`,
|
|
1796
|
+
userId,
|
|
1797
|
+
recorded === void 0 ? meta : { ...meta, errorMessage: recorded.error.message }
|
|
1798
|
+
);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
this.logger.info(
|
|
1802
|
+
statusCode >= HTTP_REDIRECT_MIN ? RESERVED_LOG_KEYS.HTTP_REQUEST_REDIRECT : RESERVED_LOG_KEYS.HTTP_REQUEST_SUCCESS,
|
|
1803
|
+
`${method} ${url} \u2192 ${statusCode} (${duration}ms)`,
|
|
1804
|
+
userId,
|
|
1805
|
+
meta
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
HttpAccessLogMiddleware = __decorateClass([
|
|
1810
|
+
common.Injectable(),
|
|
1811
|
+
__decorateParam(0, common.Inject(exports.PinoLoggerService)),
|
|
1812
|
+
__decorateParam(1, common.Inject(LOGGER_OPTIONS_TOKEN))
|
|
1813
|
+
], HttpAccessLogMiddleware);
|
|
1346
1814
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1347
1815
|
var MAX_CORRELATION_ID_LENGTH = 256;
|
|
1348
1816
|
var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
|
|
@@ -1393,9 +1861,9 @@ exports.RequestIdMiddleware = __decorateClass([
|
|
|
1393
1861
|
], exports.RequestIdMiddleware);
|
|
1394
1862
|
|
|
1395
1863
|
// src/server/middlewares/apply-request-id-middleware.ts
|
|
1396
|
-
var ALL_ROUTES = "
|
|
1864
|
+
var ALL_ROUTES = "/";
|
|
1397
1865
|
function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
|
|
1398
|
-
consumer.apply(exports.RequestIdMiddleware).forRoutes(routes);
|
|
1866
|
+
consumer.apply(exports.RequestIdMiddleware, HttpAccessLogMiddleware).forRoutes(routes);
|
|
1399
1867
|
}
|
|
1400
1868
|
var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
|
|
1401
1869
|
function InjectLogger(context) {
|
|
@@ -1468,6 +1936,9 @@ exports.LOG_CONTEXT_TOKEN = LOG_CONTEXT_TOKEN;
|
|
|
1468
1936
|
exports.LOG_KEYS_CONVENTION_REGEX = LOG_KEYS_CONVENTION_REGEX;
|
|
1469
1937
|
exports.LogContext = LogContext;
|
|
1470
1938
|
exports.LogPerformance = LogPerformance;
|
|
1939
|
+
exports.PINO_LEVEL_NAMES = PINO_LEVEL_NAMES;
|
|
1940
|
+
exports.PINO_LEVEL_NUMBERS = PINO_LEVEL_NUMBERS;
|
|
1471
1941
|
exports.PrettyDevDestination = PrettyDevDestination;
|
|
1472
1942
|
exports.RESERVED_LOG_KEYS = RESERVED_LOG_KEYS;
|
|
1943
|
+
exports.RESERVED_LOG_KEYS_NOT_EMITTED = RESERVED_LOG_KEYS_NOT_EMITTED;
|
|
1473
1944
|
exports.applyRequestIdMiddleware = applyRequestIdMiddleware;
|