@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.
@@ -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: this.serializeError(message) };
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(payload, typeof message === "string" ? message : String(message));
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
- err: this.serializeError(error)
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, message);
453
+ this.pino.info(payload, line);
413
454
  } else {
414
- this.pino.warn(payload, message);
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 instanceof Error)) {
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 stack.split("\n").filter((line) => !line.includes("node_modules")).join("\n");
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(path) {
686
- return this.excludePaths.some((pattern) => pattern.test(path));
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 detectOtelTraceApi() {
970
+ function loadTraceApiFrom(anchor) {
921
971
  try {
922
- const requireFromCwd = createRequire(join(process.cwd(), "noop.cjs"));
923
- const mod = requireFromCwd("@opentelemetry/api");
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(path) {
1328
+ function leafNameOf(path2) {
1155
1329
  let leaf;
1156
- for (const match of path.matchAll(PATH_SEGMENT)) {
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((path) => leafNameOf(path) ?? [])
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(pino.stdSerializers.err),
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
- base: { service: options.service },
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) => redact(record, true)
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 path = stripQueryString(target);
1721
- if (this.excludePaths.some((pattern) => pattern.test(path))) {
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: path,
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
- res.on(
1743
- "close",
1744
- AsyncResource.bind(() => {
1745
- this.logTerminal(req, res, method, url, Date.now() - start);
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(LOGGER_OPTIONS_TOKEN))
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;