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