@bymax-one/nest-logger 1.2.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 +138 -1
- package/README.md +171 -13
- package/dist/server/index.cjs +315 -40
- package/dist/server/index.d.cts +127 -13
- package/dist/server/index.d.ts +127 -13
- package/dist/server/index.mjs +327 -49
- package/dist/shared/index.d.cts +102 -3
- package/dist/shared/index.d.ts +102 -3
- package/package.json +1 -1
package/dist/server/index.cjs
CHANGED
|
@@ -65,6 +65,14 @@ function applyDefaults(options) {
|
|
|
65
65
|
shouldUseAsNestLogger: options.shouldUseAsNestLogger ?? true,
|
|
66
66
|
redactPaths: Object.freeze([...options.redactPaths ?? []]),
|
|
67
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",
|
|
68
76
|
redactCensor: options.redactCensor ?? "[REDACTED]",
|
|
69
77
|
shouldDisableDefaultRedact: options.shouldDisableDefaultRedact ?? false,
|
|
70
78
|
destinations: Object.freeze([...options.destinations ?? []]),
|
|
@@ -134,6 +142,27 @@ function validateOptions(options) {
|
|
|
134
142
|
)}`
|
|
135
143
|
);
|
|
136
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
|
+
}
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
// src/server/constants/injection-tokens.constants.ts
|
|
@@ -183,6 +212,23 @@ var PROTOTYPE_POLLUTING_KEYS = /* @__PURE__ */ new Set([
|
|
|
183
212
|
"prototype"
|
|
184
213
|
]);
|
|
185
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
|
+
|
|
186
232
|
// src/server/services/pino-logger.service.ts
|
|
187
233
|
var OWNED_PAYLOAD_KEYS = ["logKey", "userId", "context"];
|
|
188
234
|
function withoutOwnedKeys(metadata) {
|
|
@@ -206,7 +252,7 @@ function withoutOwnedKeys(metadata) {
|
|
|
206
252
|
}
|
|
207
253
|
function safeErrorMessage(error) {
|
|
208
254
|
try {
|
|
209
|
-
return String(error.message);
|
|
255
|
+
return toSingleLineMessage(String(error.message));
|
|
210
256
|
} catch {
|
|
211
257
|
return "Unreadable error message";
|
|
212
258
|
}
|
|
@@ -248,7 +294,7 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
248
294
|
*/
|
|
249
295
|
error(message, ...optionalParams) {
|
|
250
296
|
if (message instanceof Error) {
|
|
251
|
-
const payload2 = { err:
|
|
297
|
+
const payload2 = { err: message };
|
|
252
298
|
assignIfDefined(payload2, "context", this.resolveContext(optionalParams));
|
|
253
299
|
this.pino.error(payload2, safeErrorMessage(message));
|
|
254
300
|
return;
|
|
@@ -258,7 +304,10 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
258
304
|
const payload = {};
|
|
259
305
|
assignIfDefined(payload, "context", context);
|
|
260
306
|
assignIfDefined(payload, "stack", stack);
|
|
261
|
-
this.pino.error(
|
|
307
|
+
this.pino.error(
|
|
308
|
+
payload,
|
|
309
|
+
toSingleLineMessage(typeof message === "string" ? message : String(message))
|
|
310
|
+
);
|
|
262
311
|
}
|
|
263
312
|
/**
|
|
264
313
|
* NestJS `warn` (warn level).
|
|
@@ -335,7 +384,12 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
335
384
|
const payload = {
|
|
336
385
|
...withoutOwnedKeys(metadata),
|
|
337
386
|
logKey,
|
|
338
|
-
|
|
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
|
|
339
393
|
};
|
|
340
394
|
assignIfDefined(payload, "userId", userId);
|
|
341
395
|
assignIfDefined(payload, "context", this.context);
|
|
@@ -387,23 +441,6 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
387
441
|
onApplicationShutdown() {
|
|
388
442
|
}
|
|
389
443
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
390
|
-
/**
|
|
391
|
-
* Serialize an Error into a plain, log-safe object matching Pino's built-in
|
|
392
|
-
* `err` serializer shape (`type`, `message`, `stack`).
|
|
393
|
-
*
|
|
394
|
-
* Reads `error.name` (NOT `error.constructor.name`) for `type`: native
|
|
395
|
-
* subclasses set `name` to their class name, and a sanitized plain-object error
|
|
396
|
-
* (from `sanitizeError`) carries the real class name on `name` — whereas its
|
|
397
|
-
* `constructor.name` would be the unhelpful `'Object'`, breaking `err.type`
|
|
398
|
-
* dashboards for every exception logged through `HttpExceptionFilter`.
|
|
399
|
-
*/
|
|
400
|
-
serializeError(error) {
|
|
401
|
-
try {
|
|
402
|
-
return { type: error.name, message: error.message, stack: error.stack };
|
|
403
|
-
} catch {
|
|
404
|
-
return { type: "SanitizeFailed", message: "Failed to read the thrown value" };
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
444
|
/** Resolve the context: last string param wins, else the instance context. */
|
|
408
445
|
resolveContext(optionalParams) {
|
|
409
446
|
const last = optionalParams[optionalParams.length - 1];
|
|
@@ -414,17 +451,18 @@ exports.PinoLoggerService = class PinoLoggerService {
|
|
|
414
451
|
const payload = { ...withoutOwnedKeys(metadata), logKey };
|
|
415
452
|
assignIfDefined(payload, "userId", userId);
|
|
416
453
|
assignIfDefined(payload, "context", this.context);
|
|
454
|
+
const line = toSingleLineMessage(message);
|
|
417
455
|
if (level === "info") {
|
|
418
|
-
this.pino.info(payload,
|
|
456
|
+
this.pino.info(payload, line);
|
|
419
457
|
} else {
|
|
420
|
-
this.pino.warn(payload,
|
|
458
|
+
this.pino.warn(payload, line);
|
|
421
459
|
}
|
|
422
460
|
}
|
|
423
461
|
/** Emit a NestJS-style variadic log, dispatching by level without computed indexing. */
|
|
424
462
|
emitNestStyle(level, message, optionalParams) {
|
|
425
463
|
const payload = {};
|
|
426
464
|
assignIfDefined(payload, "context", this.resolveContext(optionalParams));
|
|
427
|
-
const msg = typeof message === "string" ? message : String(message);
|
|
465
|
+
const msg = toSingleLineMessage(typeof message === "string" ? message : String(message));
|
|
428
466
|
switch (level) {
|
|
429
467
|
case "info":
|
|
430
468
|
this.pino.info(payload, msg);
|
|
@@ -519,9 +557,16 @@ function sanitizeError(err, options) {
|
|
|
519
557
|
const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
|
|
520
558
|
return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
|
|
521
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
|
+
}
|
|
522
567
|
function toSanitizedError(value, seen, depth2, maxDepth) {
|
|
523
568
|
try {
|
|
524
|
-
if (!(value
|
|
569
|
+
if (!isErrorLike(value)) {
|
|
525
570
|
return { name: "UnknownError", message: String(value) };
|
|
526
571
|
}
|
|
527
572
|
seen.add(value);
|
|
@@ -559,7 +604,9 @@ function readAggregatedErrors(value) {
|
|
|
559
604
|
return Array.isArray(candidate) ? candidate : void 0;
|
|
560
605
|
}
|
|
561
606
|
function scrubStack(stack) {
|
|
562
|
-
return
|
|
607
|
+
return escapeControlCharacters(
|
|
608
|
+
stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n")
|
|
609
|
+
);
|
|
563
610
|
}
|
|
564
611
|
function isObject(value) {
|
|
565
612
|
return typeof value === "object" && value !== null;
|
|
@@ -923,15 +970,18 @@ var DEFAULT_REDACT_PATHS = [
|
|
|
923
970
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
924
971
|
...REDACT_ABSOLUTE_PATHS
|
|
925
972
|
];
|
|
926
|
-
function
|
|
973
|
+
function loadTraceApiFrom(anchor) {
|
|
927
974
|
try {
|
|
928
|
-
const
|
|
929
|
-
const mod =
|
|
975
|
+
const requireFrom = module$1.createRequire(anchor());
|
|
976
|
+
const mod = requireFrom("@opentelemetry/api");
|
|
930
977
|
return mod.trace;
|
|
931
978
|
} catch {
|
|
932
979
|
return void 0;
|
|
933
980
|
}
|
|
934
981
|
}
|
|
982
|
+
function detectOtelTraceApi() {
|
|
983
|
+
return loadTraceApiFrom(() => __filename) ?? loadTraceApiFrom(() => path.join(process.cwd(), "noop.cjs"));
|
|
984
|
+
}
|
|
935
985
|
function isValidTraceId(traceId) {
|
|
936
986
|
return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
937
987
|
}
|
|
@@ -1127,6 +1177,127 @@ function createNameRedactor(fieldNames, censor) {
|
|
|
1127
1177
|
};
|
|
1128
1178
|
}
|
|
1129
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
|
+
|
|
1130
1301
|
// src/server/utils/truncate-large-entries.ts
|
|
1131
1302
|
var PREVIEW_LENGTH = 200;
|
|
1132
1303
|
function createSizeBoundedSerializer(baseSerializer, maxBytes) {
|
|
@@ -1168,6 +1339,85 @@ function leafNameOf(path) {
|
|
|
1168
1339
|
}
|
|
1169
1340
|
return leaf;
|
|
1170
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
|
+
}
|
|
1171
1421
|
var PASS_THROUGH = (value) => value;
|
|
1172
1422
|
function resolveNameRedactor(options) {
|
|
1173
1423
|
if (options.redactStrategy !== "names") {
|
|
@@ -1194,7 +1444,7 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1194
1444
|
const redact = resolveNameRedactor(options);
|
|
1195
1445
|
const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
|
|
1196
1446
|
const serializers = {
|
|
1197
|
-
err: bound(
|
|
1447
|
+
err: bound(serializeErrorValue),
|
|
1198
1448
|
...Object.fromEntries(
|
|
1199
1449
|
Object.entries(options.serializers).map(
|
|
1200
1450
|
([key, serializer]) => [key, bound(serializer)]
|
|
@@ -1205,7 +1455,14 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1205
1455
|
const pinoOptions = {
|
|
1206
1456
|
level: options.level,
|
|
1207
1457
|
...redactOption === void 0 ? {} : { redact: redactOption },
|
|
1208
|
-
|
|
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
|
+
),
|
|
1209
1466
|
// Pino requires the timestamp fn to emit the `,"time":"..."` fragment.
|
|
1210
1467
|
timestamp: () => `,"time":"${options.timestamp()}"`,
|
|
1211
1468
|
formatters: {
|
|
@@ -1228,7 +1485,10 @@ function buildPinoInstance(options, logContext, destinations) {
|
|
|
1228
1485
|
// `base` and child bindings are NOT visible at this hook — they are
|
|
1229
1486
|
// library-owned (`service`, the `@InjectLogger` context) and carry nothing
|
|
1230
1487
|
// sensitive, which `pino-factory.spec.ts` pins.
|
|
1231
|
-
log: (record) =>
|
|
1488
|
+
log: (record) => withSemconvException(
|
|
1489
|
+
withEventName(redact(record, true), options.eventNameField),
|
|
1490
|
+
options.errorFormat
|
|
1491
|
+
)
|
|
1232
1492
|
},
|
|
1233
1493
|
serializers,
|
|
1234
1494
|
mixin: createTraceContextMixin(logContext, options.otel),
|
|
@@ -1327,6 +1587,14 @@ var DestinationRegistry = class {
|
|
|
1327
1587
|
}
|
|
1328
1588
|
);
|
|
1329
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
|
+
}
|
|
1330
1598
|
}
|
|
1331
1599
|
/**
|
|
1332
1600
|
* Flush and close active destinations in REVERSE registration order, so a
|
|
@@ -1705,8 +1973,9 @@ var HttpAccessLogMiddleware = class {
|
|
|
1705
1973
|
* `emitDecoratorMetadata`, so implicit class-type injection would not resolve.
|
|
1706
1974
|
* @param options - Resolved module options (read for `http.excludePaths`).
|
|
1707
1975
|
*/
|
|
1708
|
-
constructor(logger, options) {
|
|
1976
|
+
constructor(logger, logContext, options) {
|
|
1709
1977
|
this.logger = logger;
|
|
1978
|
+
this.logContext = logContext;
|
|
1710
1979
|
this.isEnabled = options.http.isEnabled;
|
|
1711
1980
|
this.excludePaths = options.http.excludePaths;
|
|
1712
1981
|
}
|
|
@@ -1745,12 +2014,17 @@ var HttpAccessLogMiddleware = class {
|
|
|
1745
2014
|
ip: req.ip,
|
|
1746
2015
|
userAgent: readUserAgent(req)
|
|
1747
2016
|
});
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
+
});
|
|
1754
2028
|
next();
|
|
1755
2029
|
}
|
|
1756
2030
|
/**
|
|
@@ -1809,7 +2083,8 @@ var HttpAccessLogMiddleware = class {
|
|
|
1809
2083
|
HttpAccessLogMiddleware = __decorateClass([
|
|
1810
2084
|
common.Injectable(),
|
|
1811
2085
|
__decorateParam(0, common.Inject(exports.PinoLoggerService)),
|
|
1812
|
-
__decorateParam(1, common.Inject(
|
|
2086
|
+
__decorateParam(1, common.Inject(exports.LogContextService)),
|
|
2087
|
+
__decorateParam(2, common.Inject(LOGGER_OPTIONS_TOKEN))
|
|
1813
2088
|
], HttpAccessLogMiddleware);
|
|
1814
2089
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1815
2090
|
var MAX_CORRELATION_ID_LENGTH = 256;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -96,6 +96,79 @@ interface ServiceMetadata {
|
|
|
96
96
|
readonly name: string;
|
|
97
97
|
/** OTel attribute `service.version` — semver, commit SHA, or build tag. */
|
|
98
98
|
readonly version: string;
|
|
99
|
+
/**
|
|
100
|
+
* OTel attribute `service.namespace` (**Stable**) — the group the service
|
|
101
|
+
* belongs to, e.g. a team, a bounded context or a tenant of the platform.
|
|
102
|
+
* Two services may share a `name` as long as their namespaces differ.
|
|
103
|
+
*/
|
|
104
|
+
readonly namespace?: string;
|
|
105
|
+
/**
|
|
106
|
+
* OTel attribute `service.instance.id` (**Stable**) — identifies ONE running
|
|
107
|
+
* instance, so the triplet (namespace, name, instanceId) is globally unique.
|
|
108
|
+
*
|
|
109
|
+
* The library never generates this. A value minted here would be the logger's
|
|
110
|
+
* rather than the OpenTelemetry Resource's, so logs and traces would disagree
|
|
111
|
+
* about which instance served a request, and it would change on every restart
|
|
112
|
+
* while looking authoritative. Supply it from the platform — the Kubernetes
|
|
113
|
+
* pod UID, the ECS task ARN, the VM instance id — or through
|
|
114
|
+
* `OTEL_RESOURCE_ATTRIBUTES`, which is the same source the SDK reads.
|
|
115
|
+
*
|
|
116
|
+
* Treat it as HIGH cardinality: fine as a log field, not as a metric label.
|
|
117
|
+
*/
|
|
118
|
+
readonly instanceId?: string;
|
|
119
|
+
/**
|
|
120
|
+
* OTel attribute `deployment.environment.name` (**Stable**) — `production`,
|
|
121
|
+
* `staging`, and so on. Falls back to `NODE_ENV` when omitted.
|
|
122
|
+
*
|
|
123
|
+
* The older `deployment.environment` spelling is **Deprecated** in the
|
|
124
|
+
* semantic conventions and is never emitted.
|
|
125
|
+
*/
|
|
126
|
+
readonly environment?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The `service` block as it appears in an EMITTED record, under the default
|
|
130
|
+
* `resourceFormat: 'nested'`.
|
|
131
|
+
*
|
|
132
|
+
* Distinct from {@link ServiceMetadata} on purpose, and the distinction is the
|
|
133
|
+
* kind of defect this library has already been bitten by once: a published type
|
|
134
|
+
* that describes something the runtime never emits. `ServiceMetadata` is what a
|
|
135
|
+
* consumer CONFIGURES — flat option names like `instanceId` and `environment`.
|
|
136
|
+
* What comes out is the flattened OTel attribute path: `service.instance.id`
|
|
137
|
+
* nests as `service: { instance: { id } }`, and the environment is not under
|
|
138
|
+
* `service` at all, it is `deployment.environment.name`. Typing the emitted
|
|
139
|
+
* record with the configuration interface would advertise `entry.service.instanceId`
|
|
140
|
+
* to every consumer, and that property does not exist on any entry.
|
|
141
|
+
*
|
|
142
|
+
* Under `resourceFormat: 'flat'` these values appear as dotted top-level keys
|
|
143
|
+
* (`"service.instance.id"`), which {@link LogEntry}'s index signature already
|
|
144
|
+
* admits — a flat record simply has no `service` object.
|
|
145
|
+
*/
|
|
146
|
+
interface EmittedServiceResource {
|
|
147
|
+
/** OTel `service.name`. */
|
|
148
|
+
readonly name: string;
|
|
149
|
+
/** OTel `service.version`. */
|
|
150
|
+
readonly version: string;
|
|
151
|
+
/** OTel `service.namespace`, when resolved. */
|
|
152
|
+
readonly namespace?: string;
|
|
153
|
+
/** OTel `service.instance.id`, nested as the attribute path spells it. */
|
|
154
|
+
readonly instance?: {
|
|
155
|
+
readonly id: string;
|
|
156
|
+
};
|
|
157
|
+
/** Consumer-supplied extras carried through from the `service` option. */
|
|
158
|
+
readonly [key: string]: unknown;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The `deployment` block as it appears in an EMITTED record under
|
|
162
|
+
* `resourceFormat: 'nested'`, carrying `deployment.environment.name`.
|
|
163
|
+
*
|
|
164
|
+
* Absent entirely when no source resolved an environment. The deprecated
|
|
165
|
+
* `deployment.environment` spelling is never emitted.
|
|
166
|
+
*/
|
|
167
|
+
interface EmittedDeploymentResource {
|
|
168
|
+
/** OTel `deployment.environment.name`, flattened from its attribute path. */
|
|
169
|
+
readonly environment: {
|
|
170
|
+
readonly name: string;
|
|
171
|
+
};
|
|
99
172
|
}
|
|
100
173
|
|
|
101
174
|
/**
|
|
@@ -191,6 +264,52 @@ interface BymaxLoggerModuleOptions {
|
|
|
191
264
|
* path-matching semantics; expect it to be removed in a future major.
|
|
192
265
|
*/
|
|
193
266
|
redactStrategy?: 'names' | 'paths';
|
|
267
|
+
/**
|
|
268
|
+
* Shape of the resource/service identity on every entry. Default `'nested'`.
|
|
269
|
+
*
|
|
270
|
+
* - `'nested'` — `{ service: { name, version, namespace, instance: { id } },
|
|
271
|
+
* deployment: { environment: { name } } }`. The historical shape, extended;
|
|
272
|
+
* no existing query breaks.
|
|
273
|
+
* - `'flat'` — the dotted OTel attribute names verbatim (`service.name`,
|
|
274
|
+
* `service.instance.id`, `deployment.environment.name`), which is what a
|
|
275
|
+
* collector mapping log fields onto resource attributes reads directly.
|
|
276
|
+
*
|
|
277
|
+
* Both carry the same values; only the key shape differs.
|
|
278
|
+
*/
|
|
279
|
+
resourceFormat?: 'nested' | 'flat';
|
|
280
|
+
/**
|
|
281
|
+
* Field carrying the machine-readable event name, mirroring `logKey`.
|
|
282
|
+
* Default `'event.name'`; `false` emits nothing.
|
|
283
|
+
*
|
|
284
|
+
* `logKey` stays exactly as it is — this is additive, never a rename.
|
|
285
|
+
*
|
|
286
|
+
* The value is meant to be mapped onto the **`EventName`** field of the
|
|
287
|
+
* OpenTelemetry LogRecord, which is Stable in the Logs Data Model. Note that
|
|
288
|
+
* the same-named `event.name` *attribute* is Deprecated in the semantic
|
|
289
|
+
* conventions precisely because the value belongs in that top-level field
|
|
290
|
+
* instead; the key here is the JSON carrier, not an OTLP attribute. The name is
|
|
291
|
+
* configurable so a pipeline that maps a different key can use it.
|
|
292
|
+
*
|
|
293
|
+
* Keep event names LOW cardinality — `payment.failed`, not
|
|
294
|
+
* `payment.failed.918231781`. Identifiers belong in their own fields.
|
|
295
|
+
*/
|
|
296
|
+
eventNameField?: string | false;
|
|
297
|
+
/**
|
|
298
|
+
* Shape of the error fields on an entry. Default `'pino'`.
|
|
299
|
+
*
|
|
300
|
+
* - `'pino'` — only the legacy `err` object (`type`, `message`, `stack`,
|
|
301
|
+
* plus the `cause` chain). What every existing query reads.
|
|
302
|
+
* - `'semconv'` — only the OpenTelemetry attributes: `exception.type`,
|
|
303
|
+
* `exception.message`, `exception.stacktrace` and `error.type`. The `err`
|
|
304
|
+
* object is removed, so this is an explicit migration, never a default.
|
|
305
|
+
* - `'both'` — both shapes on the same entry. Recommended while moving
|
|
306
|
+
* dashboards over: nothing breaks, and the new fields are already there.
|
|
307
|
+
*
|
|
308
|
+
* All four attributes are **Stable** in Semantic Conventions v1.44.0.
|
|
309
|
+
* `error.type` carries the error's class name and is LOW cardinality by
|
|
310
|
+
* construction — never a message, never an identifier.
|
|
311
|
+
*/
|
|
312
|
+
errorFormat?: 'pino' | 'semconv' | 'both';
|
|
194
313
|
/** Censor string written in place of redacted values. Default: `[REDACTED]`. */
|
|
195
314
|
redactCensor?: string;
|
|
196
315
|
/** Disable default redact paths (use with caution). Default: false. */
|
|
@@ -598,17 +717,6 @@ declare class PinoLoggerService implements LoggerService, OnApplicationShutdown
|
|
|
598
717
|
* this service has no additional teardown work.
|
|
599
718
|
*/
|
|
600
719
|
onApplicationShutdown(): void;
|
|
601
|
-
/**
|
|
602
|
-
* Serialize an Error into a plain, log-safe object matching Pino's built-in
|
|
603
|
-
* `err` serializer shape (`type`, `message`, `stack`).
|
|
604
|
-
*
|
|
605
|
-
* Reads `error.name` (NOT `error.constructor.name`) for `type`: native
|
|
606
|
-
* subclasses set `name` to their class name, and a sanitized plain-object error
|
|
607
|
-
* (from `sanitizeError`) carries the real class name on `name` — whereas its
|
|
608
|
-
* `constructor.name` would be the unhelpful `'Object'`, breaking `err.type`
|
|
609
|
-
* dashboards for every exception logged through `HttpExceptionFilter`.
|
|
610
|
-
*/
|
|
611
|
-
private serializeError;
|
|
612
720
|
/** Resolve the context: last string param wins, else the instance context. */
|
|
613
721
|
private resolveContext;
|
|
614
722
|
/** Emit a structured payload at info or warn level. */
|
|
@@ -1179,8 +1287,14 @@ interface LogEntry {
|
|
|
1179
1287
|
msg: string;
|
|
1180
1288
|
/** Convention key — `MODULE_ACTION_RESULT` format. */
|
|
1181
1289
|
logKey?: string;
|
|
1182
|
-
/**
|
|
1183
|
-
|
|
1290
|
+
/**
|
|
1291
|
+
* Service identity as EMITTED — see {@link EmittedServiceResource}. Not the
|
|
1292
|
+
* configuration shape: `service.instance.id` nests, and the environment lives
|
|
1293
|
+
* under `deployment`, not here.
|
|
1294
|
+
*/
|
|
1295
|
+
service?: EmittedServiceResource;
|
|
1296
|
+
/** Deployment identity as emitted — see {@link EmittedDeploymentResource}. */
|
|
1297
|
+
deployment?: EmittedDeploymentResource;
|
|
1184
1298
|
/** Optional NestJS context (typically the emitting class name). */
|
|
1185
1299
|
context?: string;
|
|
1186
1300
|
/** Optional correlation IDs propagated via AsyncLocalStorage. */
|