@bymax-one/nest-logger 1.2.0 → 1.2.2
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 +160 -1
- package/README.md +238 -19
- package/dist/server/index.cjs +315 -40
- package/dist/server/index.d.cts +164 -14
- package/dist/server/index.d.ts +164 -14
- package/dist/server/index.mjs +327 -49
- package/dist/shared/index.d.cts +113 -3
- package/dist/shared/index.d.ts +113 -3
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
9
|
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
|
|
9
10
|
import { randomUUID } from 'crypto';
|
|
@@ -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 = {
|
|
@@ -59,6 +62,14 @@ function applyDefaults(options) {
|
|
|
59
62
|
shouldUseAsNestLogger: options.shouldUseAsNestLogger ?? true,
|
|
60
63
|
redactPaths: Object.freeze([...options.redactPaths ?? []]),
|
|
61
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",
|
|
62
73
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
63
74
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
64
75
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
@@ -128,6 +139,27 @@ function validateOptions(options) {
|
|
|
128
139
|
)}`
|
|
129
140
|
);
|
|
130
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
|
+
}
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
// src/server/constants/injection-tokens.constants.ts
|
|
@@ -177,6 +209,23 @@ var PROTOTYPE_POLLUTING_KEYS = /* @__PURE__ */ new Set([
|
|
|
177
209
|
"prototype"
|
|
178
210
|
]);
|
|
179
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
|
+
|
|
180
229
|
// src/server/services/pino-logger.service.ts
|
|
181
230
|
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
182
231
|
function withoutOwnedKeys(metadata) {
|
|
@@ -200,7 +249,7 @@ function withoutOwnedKeys(metadata) {
|
|
|
200
249
|
}
|
|
201
250
|
function safeErrorMessage(error) {
|
|
202
251
|
try {
|
|
203
|
-
return String(error.message);
|
|
252
|
+
return toSingleLineMessage(String(error.message));
|
|
204
253
|
} catch {
|
|
205
254
|
return "Unreadable error message";
|
|
206
255
|
}
|
|
@@ -242,7 +291,7 @@ var PinoLoggerService = class {
|
|
|
242
291
|
*/
|
|
243
292
|
error(message, ...optionalParams) {
|
|
244
293
|
if (message instanceof Error) {
|
|
245
|
-
const payload2 = { err:
|
|
294
|
+
const payload2 = { err: message };
|
|
246
295
|
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
247
296
|
this.pino.error(payload2, safeErrorMessage(message));
|
|
248
297
|
return;
|
|
@@ -252,7 +301,10 @@ var PinoLoggerService = class {
|
|
|
252
301
|
const payload = {};
|
|
253
302
|
assignIfDefined(payload, "context", context);
|
|
254
303
|
assignIfDefined(payload, "stack", stack);
|
|
255
|
-
this.pino.error(
|
|
304
|
+
this.pino.error(
|
|
305
|
+
payload,
|
|
306
|
+
toSingleLineMessage(typeof message === "string" ? message : String(message))
|
|
307
|
+
);
|
|
256
308
|
}
|
|
257
309
|
/**
|
|
258
310
|
* NestJS `warn` (warn level).
|
|
@@ -329,7 +381,12 @@ var PinoLoggerService = class {
|
|
|
329
381
|
const payload = {
|
|
330
382
|
...withoutOwnedKeys(metadata),
|
|
331
383
|
logKey,
|
|
332
|
-
|
|
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
|
|
333
390
|
};
|
|
334
391
|
assignIfDefined(payload, "userId", userId);
|
|
335
392
|
assignIfDefined(payload, "context", this.context);
|
|
@@ -381,23 +438,6 @@ var PinoLoggerService = class {
|
|
|
381
438
|
onApplicationShutdown() {
|
|
382
439
|
}
|
|
383
440
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
384
|
-
/**
|
|
385
|
-
* Serialize an Error into a plain, log-safe object matching Pino's built-in
|
|
386
|
-
* `err` serializer shape (`type`, `message`, `stack`).
|
|
387
|
-
*
|
|
388
|
-
* Reads `error.name` (NOT `error.constructor.name`) for `type`: native
|
|
389
|
-
* subclasses set `name` to their class name, and a sanitized plain-object error
|
|
390
|
-
* (from `sanitizeError`) carries the real class name on `name` — whereas its
|
|
391
|
-
* `constructor.name` would be the unhelpful `'Object'`, breaking `err.type`
|
|
392
|
-
* dashboards for every exception logged through `HttpExceptionFilter`.
|
|
393
|
-
*/
|
|
394
|
-
serializeError(error) {
|
|
395
|
-
try {
|
|
396
|
-
return { type: error.name, message: error.message, stack: error.stack };
|
|
397
|
-
} catch {
|
|
398
|
-
return { type: "SanitizeFailed", message: "Failed to read the thrown value" };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
441
|
/** Resolve the context: last string param wins, else the instance context. */
|
|
402
442
|
resolveContext(optionalParams) {
|
|
403
443
|
const last = optionalParams[optionalParams.length - 1];
|
|
@@ -408,17 +448,18 @@ var PinoLoggerService = class {
|
|
|
408
448
|
const payload = { ...withoutOwnedKeys(metadata), logKey };
|
|
409
449
|
assignIfDefined(payload, "userId", userId);
|
|
410
450
|
assignIfDefined(payload, "context", this.context);
|
|
451
|
+
const line = toSingleLineMessage(message);
|
|
411
452
|
if (level === "info") {
|
|
412
|
-
this.pino.info(payload,
|
|
453
|
+
this.pino.info(payload, line);
|
|
413
454
|
} else {
|
|
414
|
-
this.pino.warn(payload,
|
|
455
|
+
this.pino.warn(payload, line);
|
|
415
456
|
}
|
|
416
457
|
}
|
|
417
458
|
/** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
|
|
418
459
|
emitNestStyle(level, message, optionalParams) {
|
|
419
460
|
const payload = {};
|
|
420
461
|
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
421
|
-
const msg = typeof message === "string" ? message : String(message);
|
|
462
|
+
const msg = toSingleLineMessage(typeof message === "string" ? message : String(message));
|
|
422
463
|
switch (level) {
|
|
423
464
|
case "info":
|
|
424
465
|
this.pino.info(payload, msg);
|
|
@@ -513,9 +554,16 @@ function sanitizeError(err, options) {
|
|
|
513
554
|
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
514
555
|
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
515
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
|
+
}
|
|
516
564
|
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
517
565
|
try {
|
|
518
|
-
if (!(value
|
|
566
|
+
if (!isErrorLike(value)) {
|
|
519
567
|
return { name: "UnknownError", message: String(value) };
|
|
520
568
|
}
|
|
521
569
|
seen.add(value);
|
|
@@ -553,7 +601,9 @@ function readAggregatedErrors(value) {
|
|
|
553
601
|
return Array.isArray(candidate) ? candidate : void 0;
|
|
554
602
|
}
|
|
555
603
|
function scrubStack(stack) {
|
|
556
|
-
return
|
|
604
|
+
return escapeControlCharacters(
|
|
605
|
+
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
606
|
+
);
|
|
557
607
|
}
|
|
558
608
|
function isObject(value) {
|
|
559
609
|
return typeof value === "object" && value !== null;
|
|
@@ -682,8 +732,8 @@ var HttpLoggingInterceptor = class {
|
|
|
682
732
|
* @param path - The query-stripped request path.
|
|
683
733
|
* @returns `true` when the path matches a configured exclude pattern.
|
|
684
734
|
*/
|
|
685
|
-
isExcluded(
|
|
686
|
-
return this.excludePaths.some((pattern) => pattern.test(
|
|
735
|
+
isExcluded(path2) {
|
|
736
|
+
return this.excludePaths.some((pattern) => pattern.test(path2));
|
|
687
737
|
}
|
|
688
738
|
/**
|
|
689
739
|
* Emit the START entry, then attach success/redirect/error logging to the
|
|
@@ -917,15 +967,18 @@ var DEFAULT_REDACT_PATHS = [
|
|
|
917
967
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
918
968
|
...REDACT_ABSOLUTE_PATHS
|
|
919
969
|
];
|
|
920
|
-
function
|
|
970
|
+
function loadTraceApiFrom(anchor) {
|
|
921
971
|
try {
|
|
922
|
-
const
|
|
923
|
-
const mod =
|
|
972
|
+
const requireFrom = createRequire(anchor());
|
|
973
|
+
const mod = requireFrom("@opentelemetry/api");
|
|
924
974
|
return mod.trace;
|
|
925
975
|
} catch {
|
|
926
976
|
return void 0;
|
|
927
977
|
}
|
|
928
978
|
}
|
|
979
|
+
function detectOtelTraceApi() {
|
|
980
|
+
return loadTraceApiFrom(() => __filename$1) ?? loadTraceApiFrom(() => join(process.cwd(), "noop.cjs"));
|
|
981
|
+
}
|
|
929
982
|
function isValidTraceId(traceId) {
|
|
930
983
|
return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
931
984
|
}
|
|
@@ -1121,6 +1174,127 @@ function createNameRedactor(fieldNames, censor) {
|
|
|
1121
1174
|
};
|
|
1122
1175
|
}
|
|
1123
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
|
+
|
|
1124
1298
|
// src/server/utils/truncate-large-entries.ts
|
|
1125
1299
|
var PREVIEW_LENGTH = 200;
|
|
1126
1300
|
function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
@@ -1151,9 +1325,9 @@ function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
|
1151
1325
|
// src/server/pino-factory.ts
|
|
1152
1326
|
var PATH_SEGMENT = /\[\s*['"]([^'"]+)['"]\s*\]|([^.[\]]+)/g;
|
|
1153
1327
|
var INDEX_SEGMENT = /^\d+$/;
|
|
1154
|
-
function leafNameOf(
|
|
1328
|
+
function leafNameOf(path2) {
|
|
1155
1329
|
let leaf;
|
|
1156
|
-
for (const match of
|
|
1330
|
+
for (const match of path2.matchAll(PATH_SEGMENT)) {
|
|
1157
1331
|
const quoted = match[1];
|
|
1158
1332
|
const name = quoted ?? (match[2] !== void 0 && INDEX_SEGMENT.test(match[2]) ? void 0 : match[2]);
|
|
1159
1333
|
if (name !== void 0 && name !== "*") {
|
|
@@ -1162,6 +1336,85 @@ function leafNameOf(path) {
|
|
|
1162
1336
|
}
|
|
1163
1337
|
return leaf;
|
|
1164
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
|
+
}
|
|
1165
1418
|
var PASS_THROUGH = (value) => value;
|
|
1166
1419
|
function resolveNameRedactor(options) {
|
|
1167
1420
|
if (options.redactStrategy !== "names") {
|
|
@@ -1169,7 +1422,7 @@ function resolveNameRedactor(options) {
|
|
|
1169
1422
|
}
|
|
1170
1423
|
const names = [
|
|
1171
1424
|
...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
|
|
1172
|
-
...options.redactPaths.flatMap((
|
|
1425
|
+
...options.redactPaths.flatMap((path2) => leafNameOf(path2) ?? [])
|
|
1173
1426
|
];
|
|
1174
1427
|
if (names.length === 0) {
|
|
1175
1428
|
return PASS_THROUGH;
|
|
@@ -1188,7 +1441,7 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1188
1441
|
const redact = resolveNameRedactor(options);
|
|
1189
1442
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
1190
1443
|
const serializers = {
|
|
1191
|
-
err: bound(
|
|
1444
|
+
err: bound(serializeErrorValue),
|
|
1192
1445
|
...Object.fromEntries(
|
|
1193
1446
|
Object.entries(options.serializers).map(
|
|
1194
1447
|
([key, serializer]) => [key, bound(serializer)]
|
|
@@ -1199,7 +1452,14 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1199
1452
|
const pinoOptions = {
|
|
1200
1453
|
level: options.level,
|
|
1201
1454
|
...redactOption === void 0 ? {} : { redact: redactOption },
|
|
1202
|
-
|
|
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
|
+
),
|
|
1203
1463
|
// Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
|
|
1204
1464
|
timestamp: () => `,"time":"${options.timestamp()}"`,
|
|
1205
1465
|
formatters: {
|
|
@@ -1222,7 +1482,10 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1222
1482
|
// `base` and child bindings are NOT visible at this hook — they are
|
|
1223
1483
|
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1224
1484
|
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1225
|
-
log: (record) =>
|
|
1485
|
+
log: (record) => withSemconvException(
|
|
1486
|
+
withEventName(redact(record, true), options.eventNameField),
|
|
1487
|
+
options.errorFormat
|
|
1488
|
+
)
|
|
1226
1489
|
},
|
|
1227
1490
|
serializers,
|
|
1228
1491
|
mixin: createTraceContextMixin(logContext, options.otel),
|
|
@@ -1321,6 +1584,14 @@ var DestinationRegistry = class {
|
|
|
1321
1584
|
}
|
|
1322
1585
|
);
|
|
1323
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
|
+
}
|
|
1324
1595
|
}
|
|
1325
1596
|
/**
|
|
1326
1597
|
* Flush and close active destinations in REVERSE registration order, so a
|
|
@@ -1699,8 +1970,9 @@ var HttpAccessLogMiddleware = class {
|
|
|
1699
1970
|
* `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
|
|
1700
1971
|
* @param options - Resolved module options (read for `http.excludePaths`).
|
|
1701
1972
|
*/
|
|
1702
|
-
constructor(logger, options) {
|
|
1973
|
+
constructor(logger, logContext, options) {
|
|
1703
1974
|
this.logger = logger;
|
|
1975
|
+
this.logContext = logContext;
|
|
1704
1976
|
this.isEnabled = options.http.isEnabled;
|
|
1705
1977
|
this.excludePaths = options.http.excludePaths;
|
|
1706
1978
|
}
|
|
@@ -1717,8 +1989,8 @@ var HttpAccessLogMiddleware = class {
|
|
|
1717
1989
|
return;
|
|
1718
1990
|
}
|
|
1719
1991
|
const target = req.originalUrl ?? req.url;
|
|
1720
|
-
const
|
|
1721
|
-
if (this.excludePaths.some((pattern) => pattern.test(
|
|
1992
|
+
const path2 = stripQueryString(target);
|
|
1993
|
+
if (this.excludePaths.some((pattern) => pattern.test(path2))) {
|
|
1722
1994
|
next();
|
|
1723
1995
|
return;
|
|
1724
1996
|
}
|
|
@@ -1731,7 +2003,7 @@ var HttpAccessLogMiddleware = class {
|
|
|
1731
2003
|
url,
|
|
1732
2004
|
// Query string stripped: it may carry secrets (reset tokens, OAuth codes)
|
|
1733
2005
|
// that no key-name redaction can scrub out of a string VALUE.
|
|
1734
|
-
fullUrl:
|
|
2006
|
+
fullUrl: path2,
|
|
1735
2007
|
// Conventional for an access log and personal data under GDPR/LGPD. Its
|
|
1736
2008
|
// fidelity is the app's to establish: behind a proxy it is only the real
|
|
1737
2009
|
// client when the app configures `trust proxy`, and a value the app has not
|
|
@@ -1739,12 +2011,17 @@ var HttpAccessLogMiddleware = class {
|
|
|
1739
2011
|
ip: req.ip,
|
|
1740
2012
|
userAgent: readUserAgent(req)
|
|
1741
2013
|
});
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
+
});
|
|
1748
2025
|
next();
|
|
1749
2026
|
}
|
|
1750
2027
|
/**
|
|
@@ -1803,7 +2080,8 @@ var HttpAccessLogMiddleware = class {
|
|
|
1803
2080
|
HttpAccessLogMiddleware = __decorateClass([
|
|
1804
2081
|
Injectable(),
|
|
1805
2082
|
__decorateParam(0, Inject(PinoLoggerService)),
|
|
1806
|
-
__decorateParam(1, Inject(
|
|
2083
|
+
__decorateParam(1, Inject(LogContextService)),
|
|
2084
|
+
__decorateParam(2, Inject(LOGGER_OPTIONS_TOKEN))
|
|
1807
2085
|
], HttpAccessLogMiddleware);
|
|
1808
2086
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1809
2087
|
var MAX_CORRELATION_ID_LENGTH = 256;
|