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