@bymax-one/nest-logger 1.0.8 → 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 +320 -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.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import pino from 'pino';
|
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { Writable } from 'stream';
|
|
8
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
8
|
+
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
|
|
9
9
|
import { randomUUID } from 'crypto';
|
|
10
10
|
|
|
11
11
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -58,6 +58,7 @@ function applyDefaults(options) {
|
|
|
58
58
|
isGlobal: options.isGlobal ?? true,
|
|
59
59
|
shouldUseAsNestLogger: options.shouldUseAsNestLogger ?? true,
|
|
60
60
|
redactPaths: Object.freeze([...options.redactPaths ?? []]),
|
|
61
|
+
redactStrategy: options.redactStrategy ?? "names",
|
|
61
62
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
62
63
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
63
64
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
@@ -83,7 +84,7 @@ var PINO_LEVEL_NUMBERS = Object.freeze({
|
|
|
83
84
|
debug: 20,
|
|
84
85
|
trace: 10
|
|
85
86
|
});
|
|
86
|
-
Object.freeze(
|
|
87
|
+
var PINO_LEVEL_NAMES = Object.freeze(
|
|
87
88
|
Object.fromEntries(
|
|
88
89
|
Object.entries(PINO_LEVEL_NUMBERS).map(([name, num]) => [num, name])
|
|
89
90
|
// keys of PINO_LEVEL_NUMBERS are always LogLevel
|
|
@@ -120,16 +121,105 @@ function validateOptions(options) {
|
|
|
120
121
|
if (options.maxEntrySizeBytes !== void 0 && options.maxEntrySizeBytes <= 0) {
|
|
121
122
|
throw new Error("[BymaxLoggerModule] options.maxEntrySizeBytes must be > 0");
|
|
122
123
|
}
|
|
124
|
+
if (options.redactStrategy !== void 0 && options.redactStrategy !== "names" && options.redactStrategy !== "paths") {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[BymaxLoggerModule] options.redactStrategy must be 'names' or 'paths'. Got: ${String(
|
|
127
|
+
options.redactStrategy
|
|
128
|
+
)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
// src/server/constants/injection-tokens.constants.ts
|
|
126
134
|
var LOGGER_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_OPTIONS");
|
|
127
135
|
var LOGGER_PINO_INSTANCE_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_PINO_INSTANCE");
|
|
128
136
|
var LOGGER_DESTINATIONS_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_DESTINATIONS");
|
|
137
|
+
var LOGGER_REDACTOR_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_REDACTOR");
|
|
129
138
|
var LOG_CONTEXT_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_LOG_CONTEXT");
|
|
139
|
+
|
|
140
|
+
// src/shared/constants/reserved-log-keys.constants.ts
|
|
141
|
+
var RESERVED_LOG_KEYS = Object.freeze({
|
|
142
|
+
LOGGER_BOOTSTRAP_OK: "LOGGER_BOOTSTRAP_OK",
|
|
143
|
+
LOGGER_BOOTSTRAP_WARNING: "LOGGER_BOOTSTRAP_WARNING",
|
|
144
|
+
LOGGER_SHUTDOWN_OK: "LOGGER_SHUTDOWN_OK",
|
|
145
|
+
HTTP_REQUEST_START: "HTTP_REQUEST_START",
|
|
146
|
+
HTTP_REQUEST_SUCCESS: "HTTP_REQUEST_SUCCESS",
|
|
147
|
+
HTTP_REQUEST_REDIRECT: "HTTP_REQUEST_REDIRECT",
|
|
148
|
+
HTTP_REQUEST_CLIENT_ERROR: "HTTP_REQUEST_CLIENT_ERROR",
|
|
149
|
+
HTTP_REQUEST_SERVER_ERROR: "HTTP_REQUEST_SERVER_ERROR",
|
|
150
|
+
HTTP_REQUEST_COMPLETED: "HTTP_REQUEST_COMPLETED",
|
|
151
|
+
HTTP_REQUEST_ABORTED: "HTTP_REQUEST_ABORTED",
|
|
152
|
+
HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
|
|
153
|
+
HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
|
|
154
|
+
METHOD_EXECUTION: "METHOD_EXECUTION",
|
|
155
|
+
METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
|
|
156
|
+
LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
|
|
157
|
+
LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
|
|
158
|
+
LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
|
|
159
|
+
LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
|
|
160
|
+
});
|
|
161
|
+
var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
|
|
162
|
+
/**
|
|
163
|
+
* Superseded before it was ever used. The request lifecycle terminates in one
|
|
164
|
+
* of `HTTP_REQUEST_SUCCESS` / `_REDIRECT` / `_CLIENT_ERROR` / `_SERVER_ERROR`,
|
|
165
|
+
* each carrying the same `duration` a generic "completed" entry would. Emitting
|
|
166
|
+
* it as well would double the terminal access-log volume to say nothing new.
|
|
167
|
+
* It stays claimed so an application cannot repurpose a name that reads like a
|
|
168
|
+
* library event.
|
|
169
|
+
*/
|
|
170
|
+
HTTP_REQUEST_COMPLETED: "reserved: superseded by the four status-specific terminal keys"
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/server/constants/prototype-polluting-keys.constants.ts
|
|
174
|
+
var PROTOTYPE_POLLUTING_KEYS = /* @__PURE__ */ new Set([
|
|
175
|
+
"__proto__",
|
|
176
|
+
"constructor",
|
|
177
|
+
"prototype"
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// src/server/services/pino-logger.service.ts
|
|
181
|
+
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
182
|
+
function withoutOwnedKeys(metadata) {
|
|
183
|
+
if (metadata === void 0) {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const safe = {};
|
|
188
|
+
for (const key of Object.keys(metadata)) {
|
|
189
|
+
if (!OWNED_PAYLOAD_KEYS.includes(key) && !PROTOTYPE_POLLUTING_KEYS.has(key)) {
|
|
190
|
+
Reflect.set(safe, key, Reflect.get(metadata, key));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return safe;
|
|
194
|
+
} catch {
|
|
195
|
+
return {
|
|
196
|
+
_redactionFailed: true,
|
|
197
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function safeErrorMessage(error) {
|
|
202
|
+
try {
|
|
203
|
+
return String(error.message);
|
|
204
|
+
} catch {
|
|
205
|
+
return "Unreadable error message";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function assignIfDefined(payload, key, value) {
|
|
209
|
+
if (value !== void 0) {
|
|
210
|
+
Reflect.defineProperty(payload, key, { value, enumerable: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
130
213
|
var PinoLoggerService = class {
|
|
131
|
-
|
|
214
|
+
/**
|
|
215
|
+
* @param pino - The wrapped Pino instance.
|
|
216
|
+
* @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
|
|
217
|
+
* Defaults to the identity so a hand-built instance (benchmarks, unit tests)
|
|
218
|
+
* still works; the module always injects the configured one.
|
|
219
|
+
*/
|
|
220
|
+
constructor(pino2, redact = (value) => value) {
|
|
132
221
|
this.pino = pino2;
|
|
222
|
+
this.redact = redact;
|
|
133
223
|
}
|
|
134
224
|
// ─── NestJS LoggerService variadic interface ──────────────────────────────
|
|
135
225
|
/**
|
|
@@ -152,18 +242,16 @@ var PinoLoggerService = class {
|
|
|
152
242
|
*/
|
|
153
243
|
error(message, ...optionalParams) {
|
|
154
244
|
if (message instanceof Error) {
|
|
155
|
-
this.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
245
|
+
const payload2 = { err: this.serializeError(message) };
|
|
246
|
+
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
247
|
+
this.pino.error(payload2, safeErrorMessage(message));
|
|
159
248
|
return;
|
|
160
249
|
}
|
|
161
250
|
const stack = typeof optionalParams[0] === "string" ? optionalParams[0] : void 0;
|
|
162
251
|
const context = typeof optionalParams[1] === "string" ? optionalParams[1] : this.context;
|
|
163
|
-
const payload = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
252
|
+
const payload = {};
|
|
253
|
+
assignIfDefined(payload, "context", context);
|
|
254
|
+
assignIfDefined(payload, "stack", stack);
|
|
167
255
|
this.pino.error(payload, typeof message === "string" ? message : String(message));
|
|
168
256
|
}
|
|
169
257
|
/**
|
|
@@ -238,10 +326,14 @@ var PinoLoggerService = class {
|
|
|
238
326
|
* @param metadata - Optional extra structured fields.
|
|
239
327
|
*/
|
|
240
328
|
errorStructured(logKey, error, userId, metadata) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
329
|
+
const payload = {
|
|
330
|
+
...withoutOwnedKeys(metadata),
|
|
331
|
+
logKey,
|
|
332
|
+
err: this.serializeError(error)
|
|
333
|
+
};
|
|
334
|
+
assignIfDefined(payload, "userId", userId);
|
|
335
|
+
assignIfDefined(payload, "context", this.context);
|
|
336
|
+
this.pino.error(payload, safeErrorMessage(error));
|
|
245
337
|
}
|
|
246
338
|
// ─── Helpers / escape hatches ─────────────────────────────────────────────
|
|
247
339
|
/**
|
|
@@ -274,7 +366,8 @@ var PinoLoggerService = class {
|
|
|
274
366
|
* @returns A new `PinoLoggerService` wrapping the Pino child.
|
|
275
367
|
*/
|
|
276
368
|
child(bindings) {
|
|
277
|
-
const
|
|
369
|
+
const safeBindings = this.redact(bindings, true);
|
|
370
|
+
const childService = new PinoLoggerService(this.pino.child(safeBindings), this.redact);
|
|
278
371
|
if (this.context !== void 0) {
|
|
279
372
|
childService.setContext(this.context);
|
|
280
373
|
}
|
|
@@ -299,7 +392,11 @@ var PinoLoggerService = class {
|
|
|
299
392
|
* dashboards for every exception logged through `HttpExceptionFilter`.
|
|
300
393
|
*/
|
|
301
394
|
serializeError(error) {
|
|
302
|
-
|
|
395
|
+
try {
|
|
396
|
+
return { type: error.name, message: error.message, stack: error.stack };
|
|
397
|
+
} catch {
|
|
398
|
+
return { type: "SanitizeFailed", message: "Failed to read the thrown value" };
|
|
399
|
+
}
|
|
303
400
|
}
|
|
304
401
|
/** Resolve the context: last string param wins, else the instance context. */
|
|
305
402
|
resolveContext(optionalParams) {
|
|
@@ -308,12 +405,9 @@ var PinoLoggerService = class {
|
|
|
308
405
|
}
|
|
309
406
|
/** Emit a structured payload at info or warn level. */
|
|
310
407
|
emitStructured(level, logKey, message, userId, metadata) {
|
|
311
|
-
const payload = {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
userId,
|
|
315
|
-
context: this.context
|
|
316
|
-
};
|
|
408
|
+
const payload = { ...withoutOwnedKeys(metadata), logKey };
|
|
409
|
+
assignIfDefined(payload, "userId", userId);
|
|
410
|
+
assignIfDefined(payload, "context", this.context);
|
|
317
411
|
if (level === "info") {
|
|
318
412
|
this.pino.info(payload, message);
|
|
319
413
|
} else {
|
|
@@ -322,7 +416,8 @@ var PinoLoggerService = class {
|
|
|
322
416
|
}
|
|
323
417
|
/** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
|
|
324
418
|
emitNestStyle(level, message, optionalParams) {
|
|
325
|
-
const payload = {
|
|
419
|
+
const payload = {};
|
|
420
|
+
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
326
421
|
const msg = typeof message === "string" ? message : String(message);
|
|
327
422
|
switch (level) {
|
|
328
423
|
case "info":
|
|
@@ -345,7 +440,8 @@ var PinoLoggerService = class {
|
|
|
345
440
|
};
|
|
346
441
|
PinoLoggerService = __decorateClass([
|
|
347
442
|
Injectable(),
|
|
348
|
-
__decorateParam(0, Inject(LOGGER_PINO_INSTANCE_TOKEN))
|
|
443
|
+
__decorateParam(0, Inject(LOGGER_PINO_INSTANCE_TOKEN)),
|
|
444
|
+
__decorateParam(1, Inject(LOGGER_REDACTOR_TOKEN))
|
|
349
445
|
], PinoLoggerService);
|
|
350
446
|
|
|
351
447
|
// src/server/decorators/inject-logger.provider.ts
|
|
@@ -395,26 +491,6 @@ DefaultStdoutDestination = __decorateClass([
|
|
|
395
491
|
Injectable()
|
|
396
492
|
], DefaultStdoutDestination);
|
|
397
493
|
|
|
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
494
|
// src/server/utils/normalize-url.util.ts
|
|
419
495
|
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
496
|
var ULID_REGEX = /\/[0-9A-HJKMNP-TV-Z]{26}(?![0-9A-HJKMNP-TV-Z])/g;
|
|
@@ -555,11 +631,39 @@ HttpExceptionFilter = __decorateClass([
|
|
|
555
631
|
Catch(),
|
|
556
632
|
__decorateParam(0, Inject(PinoLoggerService))
|
|
557
633
|
], HttpExceptionFilter);
|
|
558
|
-
|
|
634
|
+
|
|
635
|
+
// src/server/utils/http-log-state.util.ts
|
|
559
636
|
var HTTP_REDIRECT_MIN = 300;
|
|
560
637
|
var HTTP_CLIENT_ERROR_MIN = 400;
|
|
561
638
|
var HTTP_SERVER_ERROR_MIN = 500;
|
|
562
639
|
var UNKNOWN_USER_AGENT = "unknown";
|
|
640
|
+
var RECORDER_ACTIVE = /* @__PURE__ */ Symbol("bymax-one/nest-logger:recorder");
|
|
641
|
+
var RECORDED_ERROR = /* @__PURE__ */ Symbol("bymax-one/nest-logger:error");
|
|
642
|
+
function markRecorderActive(req) {
|
|
643
|
+
Reflect.set(req, RECORDER_ACTIVE, true);
|
|
644
|
+
}
|
|
645
|
+
function isRecorderActive(req) {
|
|
646
|
+
return Reflect.get(req, RECORDER_ACTIVE) === true;
|
|
647
|
+
}
|
|
648
|
+
function recordError(req, error) {
|
|
649
|
+
Reflect.set(req, RECORDED_ERROR, { error });
|
|
650
|
+
}
|
|
651
|
+
function readRecordedError(req) {
|
|
652
|
+
return Reflect.get(req, RECORDED_ERROR);
|
|
653
|
+
}
|
|
654
|
+
function toError(thrown) {
|
|
655
|
+
return thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
656
|
+
}
|
|
657
|
+
function readUserId(req) {
|
|
658
|
+
return req.user?.sub ?? req.user?.id;
|
|
659
|
+
}
|
|
660
|
+
function readUserAgent(req) {
|
|
661
|
+
const raw = req.headers["user-agent"];
|
|
662
|
+
return typeof raw === "string" ? raw : UNKNOWN_USER_AGENT;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/server/interceptors/http-logging.interceptor.ts
|
|
666
|
+
var HTTP_SUCCESS_MIN = 200;
|
|
563
667
|
var HttpLoggingInterceptor = class {
|
|
564
668
|
/**
|
|
565
669
|
* @param logger - The structured logger the lifecycle entries are written to.
|
|
@@ -593,13 +697,15 @@ var HttpLoggingInterceptor = class {
|
|
|
593
697
|
const http = context.switchToHttp();
|
|
594
698
|
const req = http.getRequest();
|
|
595
699
|
const res = http.getResponse();
|
|
700
|
+
if (isRecorderActive(req)) {
|
|
701
|
+
return this.recordOnly(req, next);
|
|
702
|
+
}
|
|
596
703
|
const { method, url, ip } = req;
|
|
597
704
|
if (this.isExcluded(stripQueryString(url))) {
|
|
598
705
|
return next.handle();
|
|
599
706
|
}
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const userId = req.user?.sub ?? req.user?.id;
|
|
707
|
+
const userAgent = readUserAgent(req);
|
|
708
|
+
const userId = readUserId(req);
|
|
603
709
|
const normalizedUrl = normalizeUrl(url);
|
|
604
710
|
const start = Date.now();
|
|
605
711
|
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
|
|
@@ -622,6 +728,28 @@ var HttpLoggingInterceptor = class {
|
|
|
622
728
|
})
|
|
623
729
|
);
|
|
624
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Hand the lifecycle to the access-log middleware, recording only the thrown
|
|
733
|
+
* error for its terminal entry.
|
|
734
|
+
*
|
|
735
|
+
* The interceptor cannot be the recorder: it never runs for a request a guard
|
|
736
|
+
* rejected or one that matched no route. It also cannot judge DELIVERY — at the
|
|
737
|
+
* moment it completes, the response has not been flushed yet, which is why an
|
|
738
|
+
* aborted request used to be logged as the 200 the handler intended. What it
|
|
739
|
+
* uniquely has is the thrown value, so that is all it contributes.
|
|
740
|
+
*
|
|
741
|
+
* @param req - The request whose lifecycle the middleware owns.
|
|
742
|
+
* @param next - The downstream call handler.
|
|
743
|
+
* @returns The response stream, unmodified and with exceptions re-thrown.
|
|
744
|
+
*/
|
|
745
|
+
recordOnly(req, next) {
|
|
746
|
+
return next.handle().pipe(
|
|
747
|
+
catchError((err) => {
|
|
748
|
+
recordError(req, toError(err));
|
|
749
|
+
return throwError(() => err);
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
}
|
|
625
753
|
/**
|
|
626
754
|
* Log a non-throwing completion: SUCCESS for 2xx, REDIRECT for 3xx, nothing
|
|
627
755
|
* for any other status (those are surfaced via the error path instead).
|
|
@@ -680,12 +808,12 @@ var HttpLoggingInterceptor = class {
|
|
|
680
808
|
}
|
|
681
809
|
return;
|
|
682
810
|
}
|
|
683
|
-
this.logger.errorStructured(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
811
|
+
this.logger.errorStructured(RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR, toError(err), userId, {
|
|
812
|
+
method,
|
|
813
|
+
url,
|
|
814
|
+
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
815
|
+
duration
|
|
816
|
+
});
|
|
689
817
|
}
|
|
690
818
|
};
|
|
691
819
|
HttpLoggingInterceptor = __decorateClass([
|
|
@@ -720,48 +848,6 @@ var {
|
|
|
720
848
|
// disabled" without a dead nullish branch.
|
|
721
849
|
global: extras.isGlobal !== false
|
|
722
850
|
})).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
851
|
|
|
766
852
|
// src/server/constants/default-redact-paths.constants.ts
|
|
767
853
|
var REDACT_MAX_DEPTH = 4;
|
|
@@ -804,7 +890,20 @@ var REDACT_COMMON_FIELDS = [
|
|
|
804
890
|
"signingSecret",
|
|
805
891
|
"privateKey",
|
|
806
892
|
// Conservative PII (1) — consumer can disable if logging email is justified
|
|
807
|
-
"email"
|
|
893
|
+
"email",
|
|
894
|
+
// Credential-bearing HTTP header names (5). These also appear in
|
|
895
|
+
// {@link REDACT_ABSOLUTE_PATHS}, pinned to the `req.headers` / `res.headers`
|
|
896
|
+
// shapes — which covered ONLY those two shapes. A headers bag logged under any
|
|
897
|
+
// other key (`logger.info(k, m, u, { headers: req.headers })`, one of the most
|
|
898
|
+
// common things written while debugging an integration) wrote the bearer token
|
|
899
|
+
// in clear. Listed here as bare names, they are caught wherever they appear.
|
|
900
|
+
// Node lower-cases inbound header names, so the lower-case spelling is the one
|
|
901
|
+
// that can actually reach a log.
|
|
902
|
+
"authorization",
|
|
903
|
+
"cookie",
|
|
904
|
+
"set-cookie",
|
|
905
|
+
"x-api-key",
|
|
906
|
+
"x-auth-token"
|
|
808
907
|
];
|
|
809
908
|
var REDACT_ABSOLUTE_PATHS = [
|
|
810
909
|
"req.headers.authorization",
|
|
@@ -818,6 +917,48 @@ var DEFAULT_REDACT_PATHS = [
|
|
|
818
917
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
819
918
|
...REDACT_ABSOLUTE_PATHS
|
|
820
919
|
];
|
|
920
|
+
function detectOtelTraceApi() {
|
|
921
|
+
try {
|
|
922
|
+
const requireFromCwd = createRequire(join(process.cwd(), "noop.cjs"));
|
|
923
|
+
const mod = requireFromCwd("@opentelemetry/api");
|
|
924
|
+
return mod.trace;
|
|
925
|
+
} catch {
|
|
926
|
+
return void 0;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function isValidTraceId(traceId) {
|
|
930
|
+
return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
931
|
+
}
|
|
932
|
+
function isValidSpanId(spanId) {
|
|
933
|
+
return /^[0-9a-f]{16}$/.test(spanId) && !/^0+$/.test(spanId);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/server/mixins/trace-context.mixin.ts
|
|
937
|
+
function createTraceContextMixin(logContext, opts) {
|
|
938
|
+
const traceApi = opts.shouldAutoInjectTraceContext ? detectOtelTraceApi() : void 0;
|
|
939
|
+
return function mixin(_mergeObject, _level, _logger) {
|
|
940
|
+
const merged = {};
|
|
941
|
+
const store = logContext.getStore();
|
|
942
|
+
if (store) {
|
|
943
|
+
Object.assign(merged, store);
|
|
944
|
+
}
|
|
945
|
+
if (traceApi) {
|
|
946
|
+
const span = traceApi.getActiveSpan();
|
|
947
|
+
if (span) {
|
|
948
|
+
const ctx = span.spanContext();
|
|
949
|
+
if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
|
|
950
|
+
Reflect.set(merged, opts.traceIdField, ctx.traceId);
|
|
951
|
+
Reflect.set(merged, opts.spanIdField, ctx.spanId);
|
|
952
|
+
if (typeof ctx.traceFlags === "number") {
|
|
953
|
+
const flagsByte = ctx.traceFlags & 255;
|
|
954
|
+
Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return merged;
|
|
960
|
+
};
|
|
961
|
+
}
|
|
821
962
|
|
|
822
963
|
// src/server/utils/compile-redact-paths.util.ts
|
|
823
964
|
function compileRedactPaths(extraPaths, shouldSkipDefaults) {
|
|
@@ -866,6 +1007,120 @@ function destinationToStream(destination) {
|
|
|
866
1007
|
});
|
|
867
1008
|
}
|
|
868
1009
|
|
|
1010
|
+
// src/server/utils/redact-by-name.util.ts
|
|
1011
|
+
var REDACT_MAX_TRAVERSAL_DEPTH = 100;
|
|
1012
|
+
var REDACT_DEPTH_EXCEEDED = "[REDACTION_DEPTH_EXCEEDED]";
|
|
1013
|
+
var REDACT_CIRCULAR = "[Circular]";
|
|
1014
|
+
function resolveToJson(value) {
|
|
1015
|
+
let getter;
|
|
1016
|
+
let candidate;
|
|
1017
|
+
let current = value;
|
|
1018
|
+
while (current !== null) {
|
|
1019
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, "toJSON");
|
|
1020
|
+
if (descriptor !== void 0) {
|
|
1021
|
+
getter = descriptor.get;
|
|
1022
|
+
candidate = getter === void 0 ? descriptor.value : getter.call(value);
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
current = Object.getPrototypeOf(current);
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
method: typeof candidate === "function" ? candidate : void 0,
|
|
1029
|
+
fromAccessor: getter !== void 0
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function cloneError(error) {
|
|
1033
|
+
const clone = Object.create(
|
|
1034
|
+
Object.getPrototypeOf(error),
|
|
1035
|
+
Object.getOwnPropertyDescriptors(error)
|
|
1036
|
+
);
|
|
1037
|
+
const { stack } = error;
|
|
1038
|
+
if (typeof stack === "string") {
|
|
1039
|
+
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
1040
|
+
}
|
|
1041
|
+
return clone;
|
|
1042
|
+
}
|
|
1043
|
+
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1044
|
+
var WRITE_FAILED = new Error();
|
|
1045
|
+
function defineOwn(target, key, value) {
|
|
1046
|
+
if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
|
|
1047
|
+
throw WRITE_FAILED;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1051
|
+
const length = value.length;
|
|
1052
|
+
const copy = [];
|
|
1053
|
+
for (let index = 0; index < length; index++) {
|
|
1054
|
+
Reflect.set(
|
|
1055
|
+
copy,
|
|
1056
|
+
index,
|
|
1057
|
+
walk(Reflect.get(value, index), sensitive, censor, depth2 + 1, ancestors)
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
return copy;
|
|
1061
|
+
}
|
|
1062
|
+
function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
|
|
1063
|
+
if (value === null) {
|
|
1064
|
+
return value;
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof value !== "object" && typeof value !== "function") {
|
|
1067
|
+
return value;
|
|
1068
|
+
}
|
|
1069
|
+
if (depth2 > REDACT_MAX_TRAVERSAL_DEPTH) {
|
|
1070
|
+
return REDACT_DEPTH_EXCEEDED;
|
|
1071
|
+
}
|
|
1072
|
+
if (ancestors.has(value)) {
|
|
1073
|
+
return REDACT_CIRCULAR;
|
|
1074
|
+
}
|
|
1075
|
+
const resolvedToJson = resolveToJson(value);
|
|
1076
|
+
if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
|
|
1077
|
+
return value;
|
|
1078
|
+
}
|
|
1079
|
+
ancestors.add(value);
|
|
1080
|
+
try {
|
|
1081
|
+
const toJson = honorToJson ? resolvedToJson.method : void 0;
|
|
1082
|
+
if (toJson !== void 0) {
|
|
1083
|
+
if (Object.keys(value).some((key) => sensitive.has(key.toLowerCase()))) {
|
|
1084
|
+
return censor;
|
|
1085
|
+
}
|
|
1086
|
+
return walk(toJson.call(value), sensitive, censor, depth2 + 1, ancestors);
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof value === "function") {
|
|
1089
|
+
return value;
|
|
1090
|
+
}
|
|
1091
|
+
if (Array.isArray(value)) {
|
|
1092
|
+
return walkArray(value, sensitive, censor, depth2, ancestors);
|
|
1093
|
+
}
|
|
1094
|
+
const source = value;
|
|
1095
|
+
const keys = Object.keys(source);
|
|
1096
|
+
const copy = value instanceof Error ? cloneError(value) : {};
|
|
1097
|
+
for (const key of keys) {
|
|
1098
|
+
const current = Reflect.get(source, key);
|
|
1099
|
+
defineOwn(
|
|
1100
|
+
copy,
|
|
1101
|
+
key,
|
|
1102
|
+
sensitive.has(key.toLowerCase()) ? censor : walk(current, sensitive, censor, depth2 + 1, ancestors)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return copy;
|
|
1106
|
+
} finally {
|
|
1107
|
+
ancestors.delete(value);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function createNameRedactor(fieldNames, censor) {
|
|
1111
|
+
const sensitive = new Set(fieldNames.map((name) => name.toLowerCase()));
|
|
1112
|
+
return (value, isRecordRoot = false) => {
|
|
1113
|
+
try {
|
|
1114
|
+
return walk(value, sensitive, censor, 0, /* @__PURE__ */ new Set(), !isRecordRoot);
|
|
1115
|
+
} catch {
|
|
1116
|
+
return {
|
|
1117
|
+
_redactionFailed: true,
|
|
1118
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
869
1124
|
// src/server/utils/truncate-large-entries.ts
|
|
870
1125
|
var PREVIEW_LENGTH = 200;
|
|
871
1126
|
function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
@@ -894,25 +1149,56 @@ function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
|
894
1149
|
}
|
|
895
1150
|
|
|
896
1151
|
// src/server/pino-factory.ts
|
|
1152
|
+
var PATH_SEGMENT = /\[\s*['"]([^'"]+)['"]\s*\]|([^.[\]]+)/g;
|
|
1153
|
+
var INDEX_SEGMENT = /^\d+$/;
|
|
1154
|
+
function leafNameOf(path) {
|
|
1155
|
+
let leaf;
|
|
1156
|
+
for (const match of path.matchAll(PATH_SEGMENT)) {
|
|
1157
|
+
const quoted = match[1];
|
|
1158
|
+
const name = quoted ?? (match[2] !== void 0 && INDEX_SEGMENT.test(match[2]) ? void 0 : match[2]);
|
|
1159
|
+
if (name !== void 0 && name !== "*") {
|
|
1160
|
+
leaf = name;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return leaf;
|
|
1164
|
+
}
|
|
1165
|
+
var PASS_THROUGH = (value) => value;
|
|
1166
|
+
function resolveNameRedactor(options) {
|
|
1167
|
+
if (options.redactStrategy !== "names") {
|
|
1168
|
+
return PASS_THROUGH;
|
|
1169
|
+
}
|
|
1170
|
+
const names = [
|
|
1171
|
+
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1172
|
+
...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
|
|
1173
|
+
];
|
|
1174
|
+
if (names.length === 0) {
|
|
1175
|
+
return PASS_THROUGH;
|
|
1176
|
+
}
|
|
1177
|
+
return createNameRedactor(names, options.redactCensor);
|
|
1178
|
+
}
|
|
1179
|
+
function resolveRedactOption(options) {
|
|
1180
|
+
const paths = options.redactStrategy === "paths" ? compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact) : [...new Set(options.redactPaths)];
|
|
1181
|
+
if (paths.length === 0) {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
return { paths, censor: options.redactCensor };
|
|
1185
|
+
}
|
|
897
1186
|
function buildPinoInstance(options, logContext, destinations) {
|
|
898
1187
|
const maxBytes = options.maxEntrySizeBytes;
|
|
1188
|
+
const redact = resolveNameRedactor(options);
|
|
1189
|
+
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
899
1190
|
const serializers = {
|
|
900
|
-
err:
|
|
1191
|
+
err: bound(pino.stdSerializers.err),
|
|
901
1192
|
...Object.fromEntries(
|
|
902
1193
|
Object.entries(options.serializers).map(
|
|
903
|
-
([key, serializer]) => [
|
|
904
|
-
key,
|
|
905
|
-
createSizeBoundedSerializer(serializer, maxBytes)
|
|
906
|
-
]
|
|
1194
|
+
([key, serializer]) => [key, bound(serializer)]
|
|
907
1195
|
)
|
|
908
1196
|
)
|
|
909
1197
|
};
|
|
1198
|
+
const redactOption = resolveRedactOption(options);
|
|
910
1199
|
const pinoOptions = {
|
|
911
1200
|
level: options.level,
|
|
912
|
-
redact:
|
|
913
|
-
paths: compileRedactPaths(options.redactPaths, options.shouldDisableDefaultRedact),
|
|
914
|
-
censor: options.redactCensor
|
|
915
|
-
},
|
|
1201
|
+
...redactOption === void 0 ? {} : { redact: redactOption },
|
|
916
1202
|
base: { service: options.service },
|
|
917
1203
|
// Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
|
|
918
1204
|
timestamp: () => `,"time":"${options.timestamp()}"`,
|
|
@@ -920,10 +1206,42 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
920
1206
|
// Emit the level as a string label instead of the numeric code — easier
|
|
921
1207
|
// for log aggregators. Trace context is injected via `mixin`, never via
|
|
922
1208
|
// `formatters.log` (which cannot see ambient ALS / OTel state).
|
|
923
|
-
level: (label) => ({ level: label })
|
|
1209
|
+
level: (label) => ({ level: label }),
|
|
1210
|
+
// `base` bindings never reach `formatters.log` — Pino pre-serializes them
|
|
1211
|
+
// into the instance's `chindings`, the same reason `child()` has to redact
|
|
1212
|
+
// its own. That matters because `service` is CONSUMER-supplied and
|
|
1213
|
+
// `applyDefaults` keeps whatever it was given: a `{ name, version, apiKey }`
|
|
1214
|
+
// went out in clear, where the path expansion had covered it via `*.*.apiKey`.
|
|
1215
|
+
// Redacting here rather than trimming to the two declared fields keeps a
|
|
1216
|
+
// consumer's extra metadata instead of silently dropping it. Runs ONCE, at
|
|
1217
|
+
// logger construction — no per-entry cost. `true`: bindings are a record
|
|
1218
|
+
// root, iterated rather than serialized.
|
|
1219
|
+
bindings: (bindings) => redact(bindings, true),
|
|
1220
|
+
// The name-walk redactor runs here, on the merged record (mixin output +
|
|
1221
|
+
// the caller's object), which is exactly the surface a caller controls.
|
|
1222
|
+
// `base` and child bindings are NOT visible at this hook — they are
|
|
1223
|
+
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1224
|
+
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1225
|
+
log: (record) => redact(record, true)
|
|
924
1226
|
},
|
|
925
1227
|
serializers,
|
|
926
|
-
mixin: createTraceContextMixin(logContext, options.otel)
|
|
1228
|
+
mixin: createTraceContextMixin(logContext, options.otel),
|
|
1229
|
+
// `formatters.log` is NOT the first code to touch the caller's object: Pino
|
|
1230
|
+
// merges the mixin result with it first, and the default strategy's
|
|
1231
|
+
// `Object.assign` invokes every own getter. A throwing getter therefore
|
|
1232
|
+
// crashed the log call before the redactor could return its fail-closed
|
|
1233
|
+
// envelope. Owning the merge moves that boundary inside the guarantee.
|
|
1234
|
+
mixinMergeStrategy: (mergeObject, mixinObject) => {
|
|
1235
|
+
try {
|
|
1236
|
+
return Object.assign({}, mixinObject, mergeObject);
|
|
1237
|
+
} catch {
|
|
1238
|
+
return {
|
|
1239
|
+
...mixinObject,
|
|
1240
|
+
_redactionFailed: true,
|
|
1241
|
+
_logKey: RESERVED_LOG_KEYS.LOGGER_REDACTION_FAILED
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
927
1245
|
};
|
|
928
1246
|
const streams = destinations.map((destination) => ({
|
|
929
1247
|
level: destination.minLevel ?? options.level,
|
|
@@ -939,9 +1257,10 @@ var DestinationRegistry = class {
|
|
|
939
1257
|
* @param logger - Structured logger used to report init failures. Injected by
|
|
940
1258
|
* token explicitly for the same bundling reason.
|
|
941
1259
|
*/
|
|
942
|
-
constructor(registered, logger) {
|
|
1260
|
+
constructor(registered, logger, options) {
|
|
943
1261
|
this.registered = registered;
|
|
944
1262
|
this.logger = logger;
|
|
1263
|
+
this.options = options;
|
|
945
1264
|
/**
|
|
946
1265
|
* Destinations that initialized successfully, in registration order.
|
|
947
1266
|
*
|
|
@@ -973,6 +1292,35 @@ var DestinationRegistry = class {
|
|
|
973
1292
|
);
|
|
974
1293
|
}
|
|
975
1294
|
}
|
|
1295
|
+
this.announceBootstrap();
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Emit the one-shot bootstrap entries, AFTER every destination has been
|
|
1299
|
+
* initialized.
|
|
1300
|
+
*
|
|
1301
|
+
* Ordering is the whole point of doing this here rather than from an eagerly
|
|
1302
|
+
* instantiated provider factory: a provider factory runs while Nest is still
|
|
1303
|
+
* building the graph, before `onModuleInit`, so a sink that only accepts
|
|
1304
|
+
* writes once its own `onInit()` has run would drop these entries — including
|
|
1305
|
+
* `LOGGER_BOOTSTRAP_WARNING`, which exists precisely so a security review can
|
|
1306
|
+
* see that PII redaction was turned off. A signal that a custom destination
|
|
1307
|
+
* can silently swallow is not an audit trail. Emitting from the registry that
|
|
1308
|
+
* owns destination initialization makes the ordering structural instead of a
|
|
1309
|
+
* cross-provider hook-order assumption.
|
|
1310
|
+
*/
|
|
1311
|
+
announceBootstrap() {
|
|
1312
|
+
this.logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
|
|
1313
|
+
if (this.options.shouldDisableDefaultRedact) {
|
|
1314
|
+
this.logger.warnStructured(
|
|
1315
|
+
RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_WARNING,
|
|
1316
|
+
"Default PII redaction is DISABLED \u2014 sensitive fields will be logged verbatim unless every one of them is listed in options.redactPaths",
|
|
1317
|
+
void 0,
|
|
1318
|
+
{
|
|
1319
|
+
shouldDisableDefaultRedact: true,
|
|
1320
|
+
redactPathCount: this.options.redactPaths.length
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
976
1324
|
}
|
|
977
1325
|
/**
|
|
978
1326
|
* Flush and close active destinations in REVERSE registration order, so a
|
|
@@ -981,6 +1329,15 @@ var DestinationRegistry = class {
|
|
|
981
1329
|
* point in the shutdown sequence.
|
|
982
1330
|
*/
|
|
983
1331
|
async onApplicationShutdown() {
|
|
1332
|
+
this.logger.info(
|
|
1333
|
+
RESERVED_LOG_KEYS.LOGGER_SHUTDOWN_OK,
|
|
1334
|
+
"BymaxLoggerModule shutting down",
|
|
1335
|
+
void 0,
|
|
1336
|
+
{ destinations: this.active.length }
|
|
1337
|
+
);
|
|
1338
|
+
await new Promise((resolve) => {
|
|
1339
|
+
setImmediate(resolve);
|
|
1340
|
+
});
|
|
984
1341
|
for (const destination of [...this.active].reverse()) {
|
|
985
1342
|
try {
|
|
986
1343
|
await destination.onShutdown?.();
|
|
@@ -1005,9 +1362,10 @@ var DestinationRegistry = class {
|
|
|
1005
1362
|
DestinationRegistry = __decorateClass([
|
|
1006
1363
|
Injectable(),
|
|
1007
1364
|
__decorateParam(0, Inject(LOGGER_DESTINATIONS_TOKEN)),
|
|
1008
|
-
__decorateParam(1, Inject(PinoLoggerService))
|
|
1365
|
+
__decorateParam(1, Inject(PinoLoggerService)),
|
|
1366
|
+
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN))
|
|
1009
1367
|
], DestinationRegistry);
|
|
1010
|
-
var FORBIDDEN_CONTEXT_KEYS =
|
|
1368
|
+
var FORBIDDEN_CONTEXT_KEYS = PROTOTYPE_POLLUTING_KEYS;
|
|
1011
1369
|
var LogContextService = class {
|
|
1012
1370
|
constructor() {
|
|
1013
1371
|
this.als = new AsyncLocalStorage();
|
|
@@ -1093,24 +1451,21 @@ LogContextService = __decorateClass([
|
|
|
1093
1451
|
], LogContextService);
|
|
1094
1452
|
|
|
1095
1453
|
// src/server/logger.module.ts
|
|
1096
|
-
var LOGGER_BOOTSTRAP_TOKEN = /* @__PURE__ */ Symbol("BYMAX_LOGGER_BOOTSTRAP");
|
|
1097
1454
|
function resolveDestinations(options) {
|
|
1098
1455
|
return options.destinations.length > 0 ? options.destinations : [new DefaultStdoutDestination()];
|
|
1099
1456
|
}
|
|
1100
|
-
function bootstrapProvider() {
|
|
1101
|
-
return {
|
|
1102
|
-
provide: LOGGER_BOOTSTRAP_TOKEN,
|
|
1103
|
-
useFactory: (logger) => {
|
|
1104
|
-
logger.info(RESERVED_LOG_KEYS.LOGGER_BOOTSTRAP_OK, "BymaxLoggerModule initialized");
|
|
1105
|
-
return true;
|
|
1106
|
-
},
|
|
1107
|
-
inject: [PinoLoggerService]
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
1457
|
function buildCommonProviders() {
|
|
1111
1458
|
return [
|
|
1112
1459
|
LogContextService,
|
|
1113
1460
|
{ provide: LOG_CONTEXT_TOKEN, useExisting: LogContextService },
|
|
1461
|
+
{
|
|
1462
|
+
// The DEFAULT-coverage redactor, shared by the Pino factory's hooks and by
|
|
1463
|
+
// `PinoLoggerService.child()`. The latter needs it as a provider because
|
|
1464
|
+
// Pino pre-serializes child bindings before `formatters.log` can see them.
|
|
1465
|
+
provide: LOGGER_REDACTOR_TOKEN,
|
|
1466
|
+
useFactory: (options) => resolveNameRedactor(options),
|
|
1467
|
+
inject: [LOGGER_OPTIONS_TOKEN]
|
|
1468
|
+
},
|
|
1114
1469
|
{
|
|
1115
1470
|
provide: LOGGER_PINO_INSTANCE_TOKEN,
|
|
1116
1471
|
useFactory: (options, logContext, destinations) => buildPinoInstance(options, logContext, destinations),
|
|
@@ -1126,8 +1481,7 @@ function buildCommonProviders() {
|
|
|
1126
1481
|
// exported — consumers never inject it directly.
|
|
1127
1482
|
DestinationRegistry,
|
|
1128
1483
|
// One child-logger provider per context discovered from @InjectLogger(context).
|
|
1129
|
-
...collectContextLoggerProviders()
|
|
1130
|
-
bootstrapProvider()
|
|
1484
|
+
...collectContextLoggerProviders()
|
|
1131
1485
|
];
|
|
1132
1486
|
}
|
|
1133
1487
|
function asyncHttpInterceptorProvider() {
|
|
@@ -1147,6 +1501,7 @@ function augmentLoggerModule(base, providers) {
|
|
|
1147
1501
|
LOGGER_OPTIONS_TOKEN,
|
|
1148
1502
|
LOGGER_PINO_INSTANCE_TOKEN,
|
|
1149
1503
|
LOGGER_DESTINATIONS_TOKEN,
|
|
1504
|
+
LOGGER_REDACTOR_TOKEN,
|
|
1150
1505
|
LOG_CONTEXT_TOKEN,
|
|
1151
1506
|
LogContextService,
|
|
1152
1507
|
PinoLoggerService,
|
|
@@ -1337,6 +1692,119 @@ var PrettyDevDestination = class {
|
|
|
1337
1692
|
});
|
|
1338
1693
|
}
|
|
1339
1694
|
};
|
|
1695
|
+
var HttpAccessLogMiddleware = class {
|
|
1696
|
+
/**
|
|
1697
|
+
* @param logger - The structured logger the access entries are written to.
|
|
1698
|
+
* Injected by token explicitly: the package is bundled without
|
|
1699
|
+
* `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
|
|
1700
|
+
* @param options - Resolved module options (read for `http.excludePaths`).
|
|
1701
|
+
*/
|
|
1702
|
+
constructor(logger, options) {
|
|
1703
|
+
this.logger = logger;
|
|
1704
|
+
this.isEnabled = options.http.isEnabled;
|
|
1705
|
+
this.excludePaths = options.http.excludePaths;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Emit START, arm the terminal entry on `'close'`, and continue the chain.
|
|
1709
|
+
*
|
|
1710
|
+
* @param req - The incoming request (Express satisfies the contract).
|
|
1711
|
+
* @param res - The outgoing response (Express satisfies the contract).
|
|
1712
|
+
* @param next - The next handler in the chain.
|
|
1713
|
+
*/
|
|
1714
|
+
use(req, res, next) {
|
|
1715
|
+
if (!this.isEnabled) {
|
|
1716
|
+
next();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const target = req.originalUrl ?? req.url;
|
|
1720
|
+
const path = stripQueryString(target);
|
|
1721
|
+
if (this.excludePaths.some((pattern) => pattern.test(path))) {
|
|
1722
|
+
next();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
markRecorderActive(req);
|
|
1726
|
+
const method = req.method;
|
|
1727
|
+
const url = normalizeUrl(target);
|
|
1728
|
+
const start = Date.now();
|
|
1729
|
+
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${url}`, readUserId(req), {
|
|
1730
|
+
method,
|
|
1731
|
+
url,
|
|
1732
|
+
// Query string stripped: it may carry secrets (reset tokens, OAuth codes)
|
|
1733
|
+
// that no key-name redaction can scrub out of a string VALUE.
|
|
1734
|
+
fullUrl: path,
|
|
1735
|
+
// Conventional for an access log and personal data under GDPR/LGPD. Its
|
|
1736
|
+
// fidelity is the app's to establish: behind a proxy it is only the real
|
|
1737
|
+
// client when the app configures `trust proxy`, and a value the app has not
|
|
1738
|
+
// established is attacker-chosen. Suppress with `redactPaths: ['ip']`.
|
|
1739
|
+
ip: req.ip,
|
|
1740
|
+
userAgent: readUserAgent(req)
|
|
1741
|
+
});
|
|
1742
|
+
res.on(
|
|
1743
|
+
"close",
|
|
1744
|
+
AsyncResource.bind(() => {
|
|
1745
|
+
this.logTerminal(req, res, method, url, Date.now() - start);
|
|
1746
|
+
})
|
|
1747
|
+
);
|
|
1748
|
+
next();
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Emit the terminal entry for a closed request.
|
|
1752
|
+
*
|
|
1753
|
+
* `userId` is read HERE rather than at START because authentication runs in a
|
|
1754
|
+
* guard, downstream of this middleware — at START there is no principal yet.
|
|
1755
|
+
*
|
|
1756
|
+
* @param req - The request, carrying any error the interceptor recorded.
|
|
1757
|
+
* @param res - The closed response.
|
|
1758
|
+
* @param method - The HTTP method.
|
|
1759
|
+
* @param url - The normalized request URL.
|
|
1760
|
+
* @param duration - Elapsed milliseconds.
|
|
1761
|
+
*/
|
|
1762
|
+
logTerminal(req, res, method, url, duration) {
|
|
1763
|
+
const statusCode = res.statusCode;
|
|
1764
|
+
const userId = readUserId(req);
|
|
1765
|
+
const meta = { method, url, statusCode, duration };
|
|
1766
|
+
if (!res.writableFinished) {
|
|
1767
|
+
this.logger.warnStructured(
|
|
1768
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_ABORTED,
|
|
1769
|
+
`${method} ${url} \u2192 ${statusCode} not delivered (${duration}ms)`,
|
|
1770
|
+
userId,
|
|
1771
|
+
meta
|
|
1772
|
+
);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (statusCode >= HTTP_SERVER_ERROR_MIN) {
|
|
1776
|
+
const recorded = readRecordedError(req);
|
|
1777
|
+
this.logger.errorStructured(
|
|
1778
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_SERVER_ERROR,
|
|
1779
|
+
recorded?.error ?? new Error(`${method} ${url} \u2192 ${statusCode}`),
|
|
1780
|
+
userId,
|
|
1781
|
+
meta
|
|
1782
|
+
);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (statusCode >= HTTP_CLIENT_ERROR_MIN) {
|
|
1786
|
+
const recorded = readRecordedError(req);
|
|
1787
|
+
this.logger.warnStructured(
|
|
1788
|
+
RESERVED_LOG_KEYS.HTTP_REQUEST_CLIENT_ERROR,
|
|
1789
|
+
`${method} ${url} \u2192 ${statusCode}`,
|
|
1790
|
+
userId,
|
|
1791
|
+
recorded === void 0 ? meta : { ...meta, errorMessage: recorded.error.message }
|
|
1792
|
+
);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
this.logger.info(
|
|
1796
|
+
statusCode >= HTTP_REDIRECT_MIN ? RESERVED_LOG_KEYS.HTTP_REQUEST_REDIRECT : RESERVED_LOG_KEYS.HTTP_REQUEST_SUCCESS,
|
|
1797
|
+
`${method} ${url} \u2192 ${statusCode} (${duration}ms)`,
|
|
1798
|
+
userId,
|
|
1799
|
+
meta
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
HttpAccessLogMiddleware = __decorateClass([
|
|
1804
|
+
Injectable(),
|
|
1805
|
+
__decorateParam(0, Inject(PinoLoggerService)),
|
|
1806
|
+
__decorateParam(1, Inject(LOGGER_OPTIONS_TOKEN))
|
|
1807
|
+
], HttpAccessLogMiddleware);
|
|
1340
1808
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1341
1809
|
var MAX_CORRELATION_ID_LENGTH = 256;
|
|
1342
1810
|
var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
|
|
@@ -1387,9 +1855,9 @@ RequestIdMiddleware = __decorateClass([
|
|
|
1387
1855
|
], RequestIdMiddleware);
|
|
1388
1856
|
|
|
1389
1857
|
// src/server/middlewares/apply-request-id-middleware.ts
|
|
1390
|
-
var ALL_ROUTES = "
|
|
1858
|
+
var ALL_ROUTES = "/";
|
|
1391
1859
|
function applyRequestIdMiddleware(consumer, routes = ALL_ROUTES) {
|
|
1392
|
-
consumer.apply(RequestIdMiddleware).forRoutes(routes);
|
|
1860
|
+
consumer.apply(RequestIdMiddleware, HttpAccessLogMiddleware).forRoutes(routes);
|
|
1393
1861
|
}
|
|
1394
1862
|
var INJECT_LOGGER_CONTEXT_METADATA_KEY = "bymax_logger:context";
|
|
1395
1863
|
function InjectLogger(context) {
|
|
@@ -1452,4 +1920,4 @@ function LogPerformance(thresholdMs = DEFAULT_THRESHOLD_MS) {
|
|
|
1452
1920
|
// src/shared/constants/log-keys-convention.constants.ts
|
|
1453
1921
|
var LOG_KEYS_CONVENTION_REGEX = /^[A-Z][A-Z0-9_]+_[A-Z][A-Z0-9_]+(_[A-Z][A-Z0-9_]+)?$/;
|
|
1454
1922
|
|
|
1455
|
-
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 };
|
|
1923
|
+
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, PINO_LEVEL_NAMES, PINO_LEVEL_NUMBERS, PinoLoggerService, PrettyDevDestination, RESERVED_LOG_KEYS, RESERVED_LOG_KEYS_NOT_EMITTED, RequestIdMiddleware, applyRequestIdMiddleware };
|