@bymax-one/nest-logger 1.4.5 → 1.4.7

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 CHANGED
@@ -11,6 +11,98 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.4.7] - 2026-11-01
15
+
16
+ ### Fixed
17
+
18
+ - **`errorStructured` keeps its `logKey` when the `Error` cannot be read.** A hostile `Error` (a Proxy whose
19
+ `ownKeys` throws, an own getter that throws, a `stack` or `message` getter that throws) made the whole
20
+ record fail the name redaction, so the entry arrived as `LOGGER_REDACTION_FAILED` and lost the caller's
21
+ `logKey`, `userId` and metadata. The `Error` node is now isolated: when it cannot be walked it is replaced
22
+ by its sanitized form and walked again, so the name redaction still applies to whatever can be read. An
23
+ unreadable `stack` or `cause` no longer degrades the node to `SanitizeFailed`; it keeps `name` and
24
+ `message` and carries `_errorFieldsIncomplete: true`. A hostile value that is not an `Error` still fails
25
+ the whole record, as before.
26
+ - **A stale comment in `HttpExceptionFilter` is corrected.** It said the cause chain was dropped at
27
+ `errorStructured`; the entry keeps the cause chain and `AggregateError.errors`, and the response body
28
+ that rides along as an own field is name-redacted. A new spec pins both.
29
+
30
+ #### Apply to a derived backend
31
+
32
+ Nothing to change in code. What can differ at run time:
33
+
34
+ - An alert on `LOGGER_REDACTION_FAILED` that fired for an unreadable `Error` stops firing for it. The event
35
+ now arrives under its own `logKey`, with the `err` node carrying `_errorFieldsIncomplete: true`. To find
36
+ these entries going forward, filter on `err._errorFieldsIncomplete: true` or `err.type: 'SanitizeFailed'`. `LOGGER_REDACTION_FAILED` is
37
+ left for a record that really cannot be read, such as hostile metadata.
38
+ - An error whose `stack` or `cause` getter throws used to reach the log as `{ type: 'SanitizeFailed' }`; it
39
+ now keeps its `type` and `message` and carries the marker. Code that matched `SanitizeFailed` for that
40
+ case should match the marker instead.
41
+
42
+ ### Tests
43
+
44
+ - An `Error` that cannot be read has a failing-first spec at the redactor and through the real Pino
45
+ pipeline, including a readable `password` next to a throwing getter and a hostile `message` getter, with
46
+ no clear secret in the line. Two existing cases that pinned the old degradation (`SanitizeFailed` for a
47
+ throwing `stack` getter, the record-level envelope for a hostile `Error`) now pin the new behavior.
48
+
49
+ ## [1.4.6] - 2026-11-01
50
+
51
+ ### Fixed
52
+
53
+ Five places swallowed a failed side effect with no trace. Each now leaves one.
54
+
55
+ - **A destination failure whose cause cannot be read is no longer dropped.** When a sink's `onInit`,
56
+ `onRegistryReady` or `write` failed with a value that has no text form (a null-prototype object, a
57
+ throwing `toString`, an `Error` with throwing `name` or `message` getters), the `stderr` line was not
58
+ written at all and the sink stopped receiving entries unannounced. The line is now written with
59
+ `err: { type: 'UnknownError', message: 'unreadable value' }`.
60
+ - **`PinoUnexpectedErrorSink` announces a report it had to drop.** When logging the report threw, or
61
+ the report could not be read, the `HTTP_UNEXPECTED_ERROR` entry, the only record of an unhandled
62
+ exception, vanished silently. One `HTTP_UNEXPECTED_ERROR_DROPPED` line now goes to `stderr`, outside
63
+ the sink set that may be what failed. The response is unaffected.
64
+ - **`PrettyDevDestination` reports the real failure.** Every failure of `import('pino-pretty')` or of
65
+ `build()` was rethrown as "pino-pretty is not installed" with the original error discarded. Only a
66
+ failure to resolve `pino-pretty` itself keeps that message; anything else throws
67
+ `[PrettyDevDestination] pino-pretty failed to start: <message>`, and the original error is the
68
+ `cause` in both cases.
69
+ - **An error node says when its own fields are missing.** When an error's own properties could not be
70
+ read (a Proxy whose `ownKeys` throws, a getter that throws), none was copied and nothing said so. The
71
+ node now carries `_errorFieldsIncomplete: true`.
72
+ - **A broken `@opentelemetry/api` install is reported once.** Every failure to load it was folded into
73
+ "not installed", so correlation could be off with no sign. A missing module stays silent, as
74
+ designed; one that is installed but throws while loading, or exports no usable `trace` namespace, writes one
75
+ `LOGGER_OTEL_API_BROKEN` line to `stderr`.
76
+
77
+ #### Apply to a derived backend
78
+
79
+ Nothing to change in code. What can differ at run time:
80
+
81
+ - `stderr` can carry two lines it never carried: `HTTP_UNEXPECTED_ERROR_DROPPED` and
82
+ `LOGGER_OTEL_API_BROKEN`, both NDJSON like the destination-failure lines. Add them to whatever
83
+ alerts on the library's `stderr` codes. `RESERVED_LOG_KEYS` gains these two keys, so an application
84
+ event must not reuse them.
85
+ - A host that already logs an event under `HTTP_UNEXPECTED_ERROR_DROPPED` or `LOGGER_OTEL_API_BROKEN`
86
+ sees no change in that entry: nothing is renamed, rejected or dropped, and its `event.name` is
87
+ derived from the key as for any other (`http.unexpected.error.dropped`, `logger.otel.api.broken`).
88
+ What changes is that, where stdout and stderr reach the same place, an alert or filter on either
89
+ key now matches the library's `stderr` line and the host's own entries alike. Rename the host event.
90
+ - `LOGGER_DESTINATION_INIT_FAILED`, `LOGGER_DESTINATION_WRITE_FAILED` and the hook-failure line now
91
+ appear for failures that were silent before. A deployment that saw none was either healthy or
92
+ losing entries without knowing.
93
+ - Code that matches the text "pino-pretty is not installed" on a `PrettyDevDestination` init error now
94
+ matches only a genuine resolution failure; match `failed to start` for the rest, or read `cause`.
95
+ - Log processing that reads error nodes can meet `_errorFieldsIncomplete: true`, on an error whose own
96
+ fields could not be read.
97
+
98
+ ### Tests
99
+
100
+ - A failure whose cause cannot be read, a dropped unexpected-error report, a `pino-pretty` failure of
101
+ each kind, an error whose own fields cannot be enumerated and a broken `@opentelemetry/api` each have
102
+ a failing-first spec. The fixtures that simulated a missing `pino-pretty` or `@opentelemetry/api` now
103
+ carry the resolution code Node attaches to the real error, because the code is what tells an absent
104
+ module from a broken one.
105
+
14
106
  ## [1.4.5] - 2026-11-01
15
107
 
16
108
  ### Documentation
@@ -2219,7 +2311,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
2219
2311
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
2220
2312
  `release.yml`, Dependabot, and issue templates
2221
2313
 
2222
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.5...HEAD
2314
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.4.7...HEAD
2315
+ [1.4.7]: https://github.com/bymaxone/nest-logger/compare/v1.4.6...v1.4.7
2316
+ [1.4.6]: https://github.com/bymaxone/nest-logger/compare/v1.4.5...v1.4.6
2223
2317
  [1.4.5]: https://github.com/bymaxone/nest-logger/compare/v1.4.4...v1.4.5
2224
2318
  [1.4.4]: https://github.com/bymaxone/nest-logger/compare/v1.4.3...v1.4.4
2225
2319
  [1.4.3]: https://github.com/bymaxone/nest-logger/compare/v1.4.2...v1.4.3
package/README.md CHANGED
@@ -179,7 +179,7 @@ pnpm add @opentelemetry/api @opentelemetry/sdk-node
179
179
  > packaging.
180
180
 
181
181
  > [!NOTE]
182
- > `@opentelemetry/api` is resolved lazily when the Pino instance is built. When it is absent the trace mixin silently steps aside — the logger never fails to start, and never warns, over an optional peer.
182
+ > `@opentelemetry/api` is resolved lazily when the Pino instance is built. When it is absent the trace mixin silently steps aside — the logger never fails to start, and never warns, over an optional peer. A copy that is installed but broken is different: it is reported once on `stderr` as `LOGGER_OTEL_API_BROKEN`, because correlation would otherwise be off with no sign.
183
183
 
184
184
  > [!NOTE]
185
185
  > The published declarations depend on no HTTP framework's types. The HTTP interceptor, the exception filter and the request-id middleware are typed by structural contracts (`LoggableRequest`, `LoggableResponse`, `NextHandler`) that an Express request and response satisfy as-is, so nothing extra is needed even when you compile with `skipLibCheck: false`. The emitted `.d.ts` imports only `@nestjs/common`, `pino` and `rxjs`.
@@ -727,6 +727,10 @@ any other serializer output.
727
727
  An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
728
728
  Pino's standard serializer did.
729
729
 
730
+ When those properties cannot be read (a Proxy whose `ownKeys` throws, or a getter that throws), none of them is copied and the error node carries `_errorFieldsIncomplete: true`, so an entry without its `code` is never mistaken for a complete one.
731
+
732
+ An `Error` that cannot be read at all does not cost the record its `logKey`, `userId` or metadata: the node is replaced by its sanitized form, which keeps `name` and `message` and carries the same marker unless `name` or `message` itself is unreadable, in which case it is `{ type: 'SanitizeFailed' }`. Filter on `err._errorFieldsIncomplete: true` or `SanitizeFailed` to find these entries.
733
+
730
734
  ### 7. Context propagation with `LogContextService`
731
735
 
732
736
  ```typescript
@@ -843,7 +847,7 @@ function assertValidLogKey(key: string) {
843
847
 
844
848
  The following keys are used internally by the library — do not reuse them in application code:
845
849
 
846
- `LOGGER_BOOTSTRAP_OK` · `LOGGER_BOOTSTRAP_WARNING` · `LOGGER_SHUTDOWN_OK` · `HTTP_REQUEST_START` · `HTTP_REQUEST_SUCCESS` · `HTTP_REQUEST_REDIRECT` · `HTTP_REQUEST_CLIENT_ERROR` · `HTTP_REQUEST_SERVER_ERROR` · `HTTP_REQUEST_ABORTED` · `HTTP_REQUEST_COMPLETED` · `HTTP_EXCEPTION_HANDLED` · `HTTP_EXCEPTION_UNHANDLED` · `HTTP_UNEXPECTED_ERROR` · `METHOD_EXECUTION` · `METHOD_SLOW_EXECUTION` · `LOGGER_DESTINATION_INIT_FAILED` · `LOGGER_DESTINATION_WRITE_FAILED` · `LOGGER_ENTRY_TRUNCATED` · `LOGGER_REDACTION_FAILED`
850
+ `LOGGER_BOOTSTRAP_OK` · `LOGGER_BOOTSTRAP_WARNING` · `LOGGER_SHUTDOWN_OK` · `HTTP_REQUEST_START` · `HTTP_REQUEST_SUCCESS` · `HTTP_REQUEST_REDIRECT` · `HTTP_REQUEST_CLIENT_ERROR` · `HTTP_REQUEST_SERVER_ERROR` · `HTTP_REQUEST_ABORTED` · `HTTP_REQUEST_COMPLETED` · `HTTP_EXCEPTION_HANDLED` · `HTTP_EXCEPTION_UNHANDLED` · `HTTP_UNEXPECTED_ERROR` · `HTTP_UNEXPECTED_ERROR_DROPPED` · `METHOD_EXECUTION` · `METHOD_SLOW_EXECUTION` · `LOGGER_DESTINATION_INIT_FAILED` · `LOGGER_DESTINATION_WRITE_FAILED` · `LOGGER_ENTRY_TRUNCATED` · `LOGGER_REDACTION_FAILED` · `LOGGER_OTEL_API_BROKEN`
847
851
 
848
852
  All reserved keys are exported as the `RESERVED_LOG_KEYS` constant from `@bymax-one/nest-logger/shared`.
849
853
 
@@ -1579,22 +1583,25 @@ Two kinds of failure, and they surface differently. A **thrown `Error`** carries
1579
1583
  | `[BymaxLoggerModule] options.` | `forRoot` / `forRootAsync`, at module setup | any option fails validation: a missing `service`, a `level` outside the set, a bad type | Fix the option the message names; it prints the path and what it received |
1580
1584
  | `[BymaxLoggerModule] useNestLogger(app) called but` | `BymaxLoggerModule.useNestLogger` | the module was never imported into the application | Import `BymaxLoggerModule` before calling it |
1581
1585
  | `[BymaxLoggerModule] applyAccessLog(app) called but` | `applyAccessLog` | the module was never imported into the application | Import `BymaxLoggerModule` before calling it |
1582
- | `[PrettyDevDestination] pino-pretty is not installed` | `PrettyDevDestination.onInit` | the destination is registered and the optional peer is missing | Install `pino-pretty` or drop the destination |
1586
+ | `[PrettyDevDestination] pino-pretty is not installed` | `PrettyDevDestination.onInit` | the destination is registered and resolving `pino-pretty` itself fails | Install `pino-pretty` or drop the destination |
1587
+ | `[PrettyDevDestination] pino-pretty failed to start` | `PrettyDevDestination.onInit` | `pino-pretty` is found but importing it or building the transform throws | Read the message and the error's `cause`: it is the real failure |
1583
1588
  | `[PrettyDevDestination] refused in production` | `PrettyDevDestination.onInit` | `NODE_ENV` is exactly `production` | Use `DefaultStdoutDestination` in production |
1584
1589
  | `[LogContextService] set() called outside run() scope` | `LogContextService.set` | called with no active `run()` | Wrap the work in `logContext.run({ ... }, () => ...)` |
1585
1590
  | `[LogContextService] Refusing to set unsafe context key` | `LogContextService.set` | the key is `__proto__`, `constructor` or `prototype` | Use an ordinary key |
1586
1591
 
1587
- The two `PrettyDevDestination` errors are thrown inside `onInit`, which the registry contains: the boot continues and the failure is reported as `LOGGER_DESTINATION_INIT_FAILED` below.
1592
+ The `PrettyDevDestination` errors are thrown inside `onInit`, which the registry contains: the boot continues and the failure is reported as `LOGGER_DESTINATION_INIT_FAILED` below.
1588
1593
 
1589
1594
  ### Log keys
1590
1595
 
1591
- | Code | Surfaces as | When | Action |
1592
- | --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
1593
- | `LOGGER_DESTINATION_INIT_FAILED` | NDJSON line on `stderr` | `destination.onInit()` throws or rejects | Sink is removed from the write fan-out and shutdown; boot continues |
1594
- | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
1595
- | `LOGGER_BOOTSTRAP_WARNING` | log entry with `reason: 'OTEL_API_UNAVAILABLE'` | `otel.shouldAutoInjectTraceContext` is on and `@opentelemetry/api` cannot be resolved | Install `@opentelemetry/api`, or turn the option off; until then `traceId` and `spanId` are absent |
1596
- | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | a serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
1597
- | `LOGGER_REDACTION_FAILED` | `_logKey` in the envelope, with `_redactionFailed: true` | a record cannot be walked by the redactor (a throwing getter, a hostile proxy) | The record is dropped whole, never partly emitted; the message and ambient context stay. Find the value that broke the walk |
1596
+ | Code | Surfaces as | When | Action |
1597
+ | --------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
1598
+ | `LOGGER_DESTINATION_INIT_FAILED` | NDJSON line on `stderr` | `destination.onInit()` throws or rejects | Sink is removed from the write fan-out and shutdown; boot continues |
1599
+ | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
1600
+ | `HTTP_UNEXPECTED_ERROR_DROPPED` | NDJSON line on `stderr` | `PinoUnexpectedErrorSink` could not log a report (the logger threw, or the report could not be read) | The report is dropped; the line names why. The response is unaffected |
1601
+ | `LOGGER_OTEL_API_BROKEN` | NDJSON line on `stderr`, once | `@opentelemetry/api` is installed but throws while loading, or exports no usable `trace` namespace | Reinstall it, or turn `otel.shouldAutoInjectTraceContext` off; until then `traceId` and `spanId` are absent |
1602
+ | `LOGGER_BOOTSTRAP_WARNING` | log entry with `reason: 'OTEL_API_UNAVAILABLE'` | `otel.shouldAutoInjectTraceContext` is on and `@opentelemetry/api` cannot be resolved | Install `@opentelemetry/api`, or turn the option off; until then `traceId` and `spanId` are absent |
1603
+ | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | a serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
1604
+ | `LOGGER_REDACTION_FAILED` | `_logKey` in the envelope, with `_redactionFailed: true` | a record cannot be walked by the redactor (a throwing getter, a hostile proxy in the metadata) | The record is dropped whole, never partly emitted; the message and ambient context stay. Find the value that broke the walk |
1598
1605
 
1599
1606
  ---
1600
1607
 
@@ -325,12 +325,14 @@ var RESERVED_LOG_KEYS = Object.freeze({
325
325
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
326
326
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
327
327
  HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
328
+ HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED",
328
329
  METHOD_EXECUTION: "METHOD_EXECUTION",
329
330
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
330
331
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
331
332
  LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
332
333
  LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
333
- LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
334
+ LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED",
335
+ LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN"
334
336
  });
335
337
  var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
336
338
  /**
@@ -397,12 +399,14 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
397
399
  }
398
400
  seen.add(value);
399
401
  const result = { name: value.name, message: value.message };
400
- if (value.stack !== void 0) {
401
- result.stack = scrubStack(value.stack);
402
+ const stack = readOptional(() => value.stack, result);
403
+ if (stack !== void 0) {
404
+ result.stack = scrubStack(stack);
402
405
  }
403
406
  const isAtDepthLimit = depth2 >= maxDepth;
404
- if (value.cause !== void 0) {
405
- result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
407
+ const cause = readOptional(() => value.cause, result);
408
+ if (cause !== void 0) {
409
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
406
410
  }
407
411
  const aggregated = readAggregatedErrors(value);
408
412
  if (aggregated !== void 0) {
@@ -429,6 +433,18 @@ function assignOwnErrorFields(target, source) {
429
433
  }
430
434
  }
431
435
  } catch {
436
+ markIncomplete(target);
437
+ }
438
+ }
439
+ function markIncomplete(target) {
440
+ Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
441
+ }
442
+ function readOptional(read, node) {
443
+ try {
444
+ return read();
445
+ } catch {
446
+ markIncomplete(node);
447
+ return void 0;
432
448
  }
433
449
  }
434
450
  function toSanitizedChild(value, seen, depth2, maxDepth) {
@@ -1210,17 +1226,114 @@ var DEFAULT_REDACT_PATHS = [
1210
1226
  ...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
1211
1227
  ...REDACT_ABSOLUTE_PATHS
1212
1228
  ];
1229
+
1230
+ // src/server/utils/report-destination-failure.util.ts
1231
+ function reportDestinationFailure(logKey, name, cause, msg) {
1232
+ writeStderrSafely(
1233
+ JSON.stringify({
1234
+ level: "error",
1235
+ logKey,
1236
+ destination: toSingleLineMessage(name),
1237
+ err: describeCause(cause),
1238
+ msg: toSingleLineMessage(msg)
1239
+ }) + "\n"
1240
+ );
1241
+ }
1242
+ var UNREADABLE_CAUSE = { type: "UnknownError", message: "unreadable value" };
1243
+ function describeCause(cause) {
1244
+ try {
1245
+ return cause instanceof Error ? {
1246
+ type: toSingleLineMessage(String(cause.name)),
1247
+ message: toSingleLineMessage(String(cause.message))
1248
+ } : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
1249
+ } catch {
1250
+ return UNREADABLE_CAUSE;
1251
+ }
1252
+ }
1253
+ function safeDestinationName(destination) {
1254
+ try {
1255
+ return String(destination.name);
1256
+ } catch {
1257
+ return "unknown";
1258
+ }
1259
+ }
1260
+ function isLogLevel(value) {
1261
+ return LOG_LEVEL_PRIORITY.includes(value);
1262
+ }
1263
+ var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
1264
+ function safeMinLevel(destination) {
1265
+ if (resolvedMinLevels.has(destination)) {
1266
+ return resolvedMinLevels.get(destination);
1267
+ }
1268
+ let resolved;
1269
+ try {
1270
+ const read = destination.minLevel;
1271
+ resolved = isLogLevel(read) ? read : void 0;
1272
+ } catch {
1273
+ }
1274
+ resolvedMinLevels.set(destination, resolved);
1275
+ return resolved;
1276
+ }
1277
+
1278
+ // src/server/utils/otel-detector.ts
1279
+ var brokenInstallWarned = false;
1280
+ function isOtelApiMissing(cause) {
1281
+ try {
1282
+ const { code, message } = cause;
1283
+ return code === "MODULE_NOT_FOUND" && /^Cannot find module '@opentelemetry\/api'/.test(String(message));
1284
+ } catch {
1285
+ return false;
1286
+ }
1287
+ }
1213
1288
  function loadTraceApiFrom(anchor) {
1289
+ let from;
1214
1290
  try {
1215
- const requireFrom = module$1.createRequire(anchor());
1216
- const mod = requireFrom("@opentelemetry/api");
1217
- return mod.trace;
1291
+ from = anchor();
1218
1292
  } catch {
1219
- return void 0;
1293
+ return { api: void 0 };
1294
+ }
1295
+ try {
1296
+ const mod = module$1.createRequire(from)("@opentelemetry/api");
1297
+ const trace = typeof mod === "object" && mod !== null || typeof mod === "function" ? mod.trace : void 0;
1298
+ if (typeof trace?.getActiveSpan !== "function") {
1299
+ throw new TypeError("@opentelemetry/api resolved but exports no usable `trace` namespace");
1300
+ }
1301
+ return { api: trace };
1302
+ } catch (cause) {
1303
+ return isOtelApiMissing(cause) ? { api: void 0 } : { api: void 0, broken: { cause } };
1220
1304
  }
1221
1305
  }
1222
- function detectOtelTraceApi() {
1223
- return loadTraceApiFrom(() => __filename) ?? loadTraceApiFrom(() => path.join(process.cwd(), "noop.cjs"));
1306
+ function warnBrokenInstallOnce(cause) {
1307
+ if (brokenInstallWarned) {
1308
+ return;
1309
+ }
1310
+ brokenInstallWarned = true;
1311
+ writeStderrSafely(
1312
+ JSON.stringify({
1313
+ level: "warn",
1314
+ logKey: RESERVED_LOG_KEYS.LOGGER_OTEL_API_BROKEN,
1315
+ msg: "@opentelemetry/api is installed but could not be loaded, so traceId and spanId are absent from every entry. Reinstall it, or set otel.shouldAutoInjectTraceContext to false.",
1316
+ err: describeCause(cause)
1317
+ }) + "\n"
1318
+ );
1319
+ }
1320
+ var DEFAULT_ANCHORS = [
1321
+ () => __filename,
1322
+ () => path.join(process.cwd(), "noop.cjs")
1323
+ ];
1324
+ function detectOtelTraceApi(anchors = DEFAULT_ANCHORS) {
1325
+ let broken;
1326
+ for (const anchor of anchors) {
1327
+ const attempt = loadTraceApiFrom(anchor);
1328
+ if (attempt.api !== void 0) {
1329
+ return attempt.api;
1330
+ }
1331
+ broken ??= attempt.broken;
1332
+ }
1333
+ if (broken !== void 0) {
1334
+ warnBrokenInstallOnce(broken.cause);
1335
+ }
1336
+ return void 0;
1224
1337
  }
1225
1338
  function isValidTraceId(traceId) {
1226
1339
  return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
@@ -1239,17 +1352,21 @@ function createTraceContextMixin(logContext, opts) {
1239
1352
  Object.assign(merged, store);
1240
1353
  }
1241
1354
  if (traceApi) {
1242
- const span = traceApi.getActiveSpan();
1243
- if (span) {
1244
- const ctx = span.spanContext();
1245
- if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
1246
- Reflect.set(merged, opts.traceIdField, ctx.traceId);
1247
- Reflect.set(merged, opts.spanIdField, ctx.spanId);
1248
- if (typeof ctx.traceFlags === "number") {
1249
- const flagsByte = ctx.traceFlags & 255;
1250
- Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
1355
+ try {
1356
+ const span = traceApi.getActiveSpan();
1357
+ if (span) {
1358
+ const { traceId, spanId, traceFlags } = span.spanContext();
1359
+ if (isValidTraceId(traceId) && isValidSpanId(spanId)) {
1360
+ const flags = typeof traceFlags === "number" ? (traceFlags & 255).toString(16).padStart(2, "0") : void 0;
1361
+ Reflect.set(merged, opts.traceIdField, traceId);
1362
+ Reflect.set(merged, opts.spanIdField, spanId);
1363
+ if (flags !== void 0) {
1364
+ Reflect.set(merged, opts.traceFlagsField, flags);
1365
+ }
1251
1366
  }
1252
1367
  }
1368
+ } catch (cause) {
1369
+ warnBrokenInstallOnce(cause);
1253
1370
  }
1254
1371
  }
1255
1372
  return merged;
@@ -1261,52 +1378,6 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
1261
1378
  const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
1262
1379
  return Array.from(new Set(sources.flat()));
1263
1380
  }
1264
-
1265
- // src/server/utils/report-destination-failure.util.ts
1266
- function reportDestinationFailure(logKey, name, cause, msg) {
1267
- try {
1268
- const err = cause instanceof Error ? {
1269
- type: toSingleLineMessage(String(cause.name)),
1270
- message: toSingleLineMessage(String(cause.message))
1271
- } : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
1272
- writeStderrSafely(
1273
- JSON.stringify({
1274
- level: "error",
1275
- logKey,
1276
- destination: toSingleLineMessage(name),
1277
- err,
1278
- msg: toSingleLineMessage(msg)
1279
- }) + "\n"
1280
- );
1281
- } catch {
1282
- }
1283
- }
1284
- function safeDestinationName(destination) {
1285
- try {
1286
- return String(destination.name);
1287
- } catch {
1288
- return "unknown";
1289
- }
1290
- }
1291
- function isLogLevel(value) {
1292
- return LOG_LEVEL_PRIORITY.includes(value);
1293
- }
1294
- var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
1295
- function safeMinLevel(destination) {
1296
- if (resolvedMinLevels.has(destination)) {
1297
- return resolvedMinLevels.get(destination);
1298
- }
1299
- let resolved;
1300
- try {
1301
- const read = destination.minLevel;
1302
- resolved = isLogLevel(read) ? read : void 0;
1303
- } catch {
1304
- }
1305
- resolvedMinLevels.set(destination, resolved);
1306
- return resolved;
1307
- }
1308
-
1309
- // src/server/utils/destination-to-stream.ts
1310
1381
  function reportWriteFailure(destination, cause) {
1311
1382
  const name = safeDestinationName(destination);
1312
1383
  reportDestinationFailure(
@@ -1427,6 +1498,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
1427
1498
  if (ancestors.has(value)) {
1428
1499
  return REDACT_CIRCULAR;
1429
1500
  }
1501
+ if (value instanceof Error) {
1502
+ return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
1503
+ }
1504
+ return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
1505
+ }
1506
+ function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
1507
+ try {
1508
+ return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
1509
+ } catch {
1510
+ return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
1511
+ }
1512
+ }
1513
+ function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
1430
1514
  const resolvedToJson = resolveToJson(value);
1431
1515
  if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
1432
1516
  return value;
@@ -2562,6 +2646,22 @@ var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
2562
2646
  function writeRawToStdout(payload) {
2563
2647
  writeStdoutSafely(payload);
2564
2648
  }
2649
+ var RESOLUTION_FAILURE_CODES = ["MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"];
2650
+ function isPinoPrettyMissing(cause) {
2651
+ try {
2652
+ const { code, message } = cause;
2653
+ return RESOLUTION_FAILURE_CODES.includes(code) && /^Cannot find (?:module|package) 'pino-pretty'/.test(String(message));
2654
+ } catch {
2655
+ return false;
2656
+ }
2657
+ }
2658
+ function describeFailure(cause) {
2659
+ try {
2660
+ return toSingleLineMessage(cause instanceof Error ? cause.message : String(cause));
2661
+ } catch {
2662
+ return "unreadable value";
2663
+ }
2664
+ }
2565
2665
  var PrettyDevDestination = class {
2566
2666
  /**
2567
2667
  * @param opts.minLevel — Optional minimum level filter applied by the
@@ -2634,10 +2734,11 @@ var PrettyDevDestination = class {
2634
2734
  // and the last-resort rescue.
2635
2735
  destination: process.stdout
2636
2736
  });
2637
- } catch {
2737
+ } catch (cause) {
2638
2738
  this.initFailed = true;
2639
2739
  throw new Error(
2640
- "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
2740
+ isPinoPrettyMissing(cause) ? "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`." : `[PrettyDevDestination] pino-pretty failed to start: ${describeFailure(cause)}`,
2741
+ { cause }
2641
2742
  );
2642
2743
  }
2643
2744
  this.flushBuffer((payload) => {
@@ -3102,7 +3203,13 @@ exports.PinoUnexpectedErrorSink = class PinoUnexpectedErrorSink {
3102
3203
  void 0,
3103
3204
  metadata
3104
3205
  );
3105
- } catch {
3206
+ } catch (cause) {
3207
+ reportDestinationFailure(
3208
+ RESERVED_LOG_KEYS.HTTP_UNEXPECTED_ERROR_DROPPED,
3209
+ "PinoUnexpectedErrorSink",
3210
+ cause,
3211
+ "An unexpected-error report could not be logged and was dropped."
3212
+ );
3106
3213
  }
3107
3214
  }
3108
3215
  };
@@ -585,12 +585,14 @@ declare const RESERVED_LOG_KEYS: Readonly<{
585
585
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
586
586
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
587
587
  readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
588
+ readonly HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED";
588
589
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
589
590
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
590
591
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
591
592
  readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
592
593
  readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
593
594
  readonly LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED";
595
+ readonly LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN";
594
596
  }>;
595
597
  /**
596
598
  * Keys that are reserved but intentionally never emitted, each with the reason.
@@ -616,66 +618,6 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
616
618
  */
617
619
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
618
620
 
619
- /**
620
- * Name-based recursive redaction.
621
- *
622
- * Layer: server/utils — the engine behind the library's DEFAULT PII protection.
623
- * It walks a log record once and censors every value whose KEY NAME is in the
624
- * sensitive set, to a bounded depth (see {@link REDACT_MAX_TRAVERSAL_DEPTH}).
625
- *
626
- * Why this replaced path matching. The previous default compiled 140
627
- * `fast-redact` paths, 108 of them multi-level wildcards (`*.password`,
628
- * `*.*.password`, …). `fast-redact` walks every key at every listed level on
629
- * every log call, so the cost grew with the PATH COUNT, not with the payload:
630
- * measured at ~107 µs per entry (≈9.3 k logs/s) against ~0.9 µs with redaction
631
- * off. A single traversal is O(nodes) — the same order Pino's serializer already
632
- * pays. The shipped configuration measures 462 k logs/s against 9.3 k, for
633
- * byte-identical output on every payload the old paths covered.
634
- *
635
- * It is also STRICTLY SAFER than the paths it replaces:
636
- * - depth 100 instead of four, and past it the value is DROPPED rather than
637
- * passed through, so the ceiling cannot become a leak the way the old one
638
- * was (anything below the wildcard list's fourth level was emitted in clear);
639
- * - a sensitive key is caught wherever it appears, so a headers bag logged as
640
- * `{ headers: { authorization } }` is covered, not only the exact
641
- * `req.headers.authorization` shape the absolute paths pinned.
642
- *
643
- * Three value kinds need special handling, and getting any of them wrong is a
644
- * leak rather than a cosmetic issue:
645
- * - `Error` — copied through its prototype and descriptors, so it stays an
646
- * `Error` (Pino's `err` serializer keys off the instance, and `message` /
647
- * `stack` are own but NON-enumerable). Skipping errors entirely left an
648
- * error under any key WITHOUT a serializer serializing its own enumerable
649
- * properties in clear.
650
- * - anything carrying `toJSON` (`Date`, `Decimal`, Luxon, Prisma types) — the
651
- * method decides the serialized form, so the walk inspects its OUTPUT.
652
- * Skipping these let a `toJSON` SYNTHESIZE a secret straight past both hooks.
653
- * - `ArrayBuffer` views (`Buffer`, typed arrays) — indexed byte containers,
654
- * returned untouched.
655
- *
656
- * What this file cannot reach: bindings passed to `child()`. Pino pre-serializes
657
- * them into the instance's `chindings` fragment before any formatter runs, so
658
- * `PinoLoggerService.child()` applies this redactor itself.
659
- *
660
- * Contract: SNAPSHOT and NEVER-THROW. Nothing the caller passed is mutated; the
661
- * result is a fresh structure built from values read exactly ONCE. That is a
662
- * deliberate trade against an earlier copy-on-write design which returned a
663
- * clean subtree by reference: doing so left every accessor in it to be evaluated
664
- * a SECOND time by `JSON.stringify`, and a stateful getter (or `toJSON`) can
665
- * answer clean to the walk and `{ password }` to the serializer. Inspection
666
- * cannot close that window — only reading once and pinning the result can. It
667
- * costs ~26 % of the entry throughput, measured, and the walk is still ~36x
668
- * faster than the path expansion it replaced.
669
- *
670
- * Any internal failure — a throwing getter, a hostile proxy, a value the walk
671
- * cannot READ — degrades the whole record to a marked, secret-free envelope rather
672
- * than crashing the request that produced it. Writing the censor is not among those
673
- * failures: every copy is built so the write cannot fail, which is why a frozen
674
- * error keeps its fields instead of costing the record.
675
- *
676
- * @see {@link createNameRedactor}
677
- */
678
-
679
621
  /**
680
622
  * A configured redactor.
681
623
  *
@@ -585,12 +585,14 @@ declare const RESERVED_LOG_KEYS: Readonly<{
585
585
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
586
586
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
587
587
  readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
588
+ readonly HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED";
588
589
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
589
590
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
590
591
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
591
592
  readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
592
593
  readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
593
594
  readonly LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED";
595
+ readonly LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN";
594
596
  }>;
595
597
  /**
596
598
  * Keys that are reserved but intentionally never emitted, each with the reason.
@@ -616,66 +618,6 @@ declare const RESERVED_LOG_KEYS_NOT_EMITTED: Readonly<{
616
618
  */
617
619
  type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS];
618
620
 
619
- /**
620
- * Name-based recursive redaction.
621
- *
622
- * Layer: server/utils — the engine behind the library's DEFAULT PII protection.
623
- * It walks a log record once and censors every value whose KEY NAME is in the
624
- * sensitive set, to a bounded depth (see {@link REDACT_MAX_TRAVERSAL_DEPTH}).
625
- *
626
- * Why this replaced path matching. The previous default compiled 140
627
- * `fast-redact` paths, 108 of them multi-level wildcards (`*.password`,
628
- * `*.*.password`, …). `fast-redact` walks every key at every listed level on
629
- * every log call, so the cost grew with the PATH COUNT, not with the payload:
630
- * measured at ~107 µs per entry (≈9.3 k logs/s) against ~0.9 µs with redaction
631
- * off. A single traversal is O(nodes) — the same order Pino's serializer already
632
- * pays. The shipped configuration measures 462 k logs/s against 9.3 k, for
633
- * byte-identical output on every payload the old paths covered.
634
- *
635
- * It is also STRICTLY SAFER than the paths it replaces:
636
- * - depth 100 instead of four, and past it the value is DROPPED rather than
637
- * passed through, so the ceiling cannot become a leak the way the old one
638
- * was (anything below the wildcard list's fourth level was emitted in clear);
639
- * - a sensitive key is caught wherever it appears, so a headers bag logged as
640
- * `{ headers: { authorization } }` is covered, not only the exact
641
- * `req.headers.authorization` shape the absolute paths pinned.
642
- *
643
- * Three value kinds need special handling, and getting any of them wrong is a
644
- * leak rather than a cosmetic issue:
645
- * - `Error` — copied through its prototype and descriptors, so it stays an
646
- * `Error` (Pino's `err` serializer keys off the instance, and `message` /
647
- * `stack` are own but NON-enumerable). Skipping errors entirely left an
648
- * error under any key WITHOUT a serializer serializing its own enumerable
649
- * properties in clear.
650
- * - anything carrying `toJSON` (`Date`, `Decimal`, Luxon, Prisma types) — the
651
- * method decides the serialized form, so the walk inspects its OUTPUT.
652
- * Skipping these let a `toJSON` SYNTHESIZE a secret straight past both hooks.
653
- * - `ArrayBuffer` views (`Buffer`, typed arrays) — indexed byte containers,
654
- * returned untouched.
655
- *
656
- * What this file cannot reach: bindings passed to `child()`. Pino pre-serializes
657
- * them into the instance's `chindings` fragment before any formatter runs, so
658
- * `PinoLoggerService.child()` applies this redactor itself.
659
- *
660
- * Contract: SNAPSHOT and NEVER-THROW. Nothing the caller passed is mutated; the
661
- * result is a fresh structure built from values read exactly ONCE. That is a
662
- * deliberate trade against an earlier copy-on-write design which returned a
663
- * clean subtree by reference: doing so left every accessor in it to be evaluated
664
- * a SECOND time by `JSON.stringify`, and a stateful getter (or `toJSON`) can
665
- * answer clean to the walk and `{ password }` to the serializer. Inspection
666
- * cannot close that window — only reading once and pinning the result can. It
667
- * costs ~26 % of the entry throughput, measured, and the walk is still ~36x
668
- * faster than the path expansion it replaced.
669
- *
670
- * Any internal failure — a throwing getter, a hostile proxy, a value the walk
671
- * cannot READ — degrades the whole record to a marked, secret-free envelope rather
672
- * than crashing the request that produced it. Writing the censor is not among those
673
- * failures: every copy is built so the write cannot fail, which is why a frozen
674
- * error keeps its fields instead of costing the record.
675
- *
676
- * @see {@link createNameRedactor}
677
- */
678
-
679
621
  /**
680
622
  * A configured redactor.
681
623
  *
@@ -322,12 +322,14 @@ var RESERVED_LOG_KEYS = Object.freeze({
322
322
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
323
323
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
324
324
  HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
325
+ HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED",
325
326
  METHOD_EXECUTION: "METHOD_EXECUTION",
326
327
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
327
328
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
328
329
  LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
329
330
  LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
330
- LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
331
+ LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED",
332
+ LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN"
331
333
  });
332
334
  var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
333
335
  /**
@@ -394,12 +396,14 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
394
396
  }
395
397
  seen.add(value);
396
398
  const result = { name: value.name, message: value.message };
397
- if (value.stack !== void 0) {
398
- result.stack = scrubStack(value.stack);
399
+ const stack = readOptional(() => value.stack, result);
400
+ if (stack !== void 0) {
401
+ result.stack = scrubStack(stack);
399
402
  }
400
403
  const isAtDepthLimit = depth2 >= maxDepth;
401
- if (value.cause !== void 0) {
402
- result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(value.cause, seen, depth2 + 1, maxDepth);
404
+ const cause = readOptional(() => value.cause, result);
405
+ if (cause !== void 0) {
406
+ result.cause = isAtDepthLimit ? createDepthMarker() : toSanitizedChild(cause, seen, depth2 + 1, maxDepth);
403
407
  }
404
408
  const aggregated = readAggregatedErrors(value);
405
409
  if (aggregated !== void 0) {
@@ -426,6 +430,18 @@ function assignOwnErrorFields(target, source) {
426
430
  }
427
431
  }
428
432
  } catch {
433
+ markIncomplete(target);
434
+ }
435
+ }
436
+ function markIncomplete(target) {
437
+ Reflect.defineProperty(target, "_errorFieldsIncomplete", { value: true, enumerable: true });
438
+ }
439
+ function readOptional(read, node) {
440
+ try {
441
+ return read();
442
+ } catch {
443
+ markIncomplete(node);
444
+ return void 0;
429
445
  }
430
446
  }
431
447
  function toSanitizedChild(value, seen, depth2, maxDepth) {
@@ -1207,17 +1223,114 @@ var DEFAULT_REDACT_PATHS = [
1207
1223
  ...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
1208
1224
  ...REDACT_ABSOLUTE_PATHS
1209
1225
  ];
1226
+
1227
+ // src/server/utils/report-destination-failure.util.ts
1228
+ function reportDestinationFailure(logKey, name, cause, msg) {
1229
+ writeStderrSafely(
1230
+ JSON.stringify({
1231
+ level: "error",
1232
+ logKey,
1233
+ destination: toSingleLineMessage(name),
1234
+ err: describeCause(cause),
1235
+ msg: toSingleLineMessage(msg)
1236
+ }) + "\n"
1237
+ );
1238
+ }
1239
+ var UNREADABLE_CAUSE = { type: "UnknownError", message: "unreadable value" };
1240
+ function describeCause(cause) {
1241
+ try {
1242
+ return cause instanceof Error ? {
1243
+ type: toSingleLineMessage(String(cause.name)),
1244
+ message: toSingleLineMessage(String(cause.message))
1245
+ } : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
1246
+ } catch {
1247
+ return UNREADABLE_CAUSE;
1248
+ }
1249
+ }
1250
+ function safeDestinationName(destination) {
1251
+ try {
1252
+ return String(destination.name);
1253
+ } catch {
1254
+ return "unknown";
1255
+ }
1256
+ }
1257
+ function isLogLevel(value) {
1258
+ return LOG_LEVEL_PRIORITY.includes(value);
1259
+ }
1260
+ var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
1261
+ function safeMinLevel(destination) {
1262
+ if (resolvedMinLevels.has(destination)) {
1263
+ return resolvedMinLevels.get(destination);
1264
+ }
1265
+ let resolved;
1266
+ try {
1267
+ const read = destination.minLevel;
1268
+ resolved = isLogLevel(read) ? read : void 0;
1269
+ } catch {
1270
+ }
1271
+ resolvedMinLevels.set(destination, resolved);
1272
+ return resolved;
1273
+ }
1274
+
1275
+ // src/server/utils/otel-detector.ts
1276
+ var brokenInstallWarned = false;
1277
+ function isOtelApiMissing(cause) {
1278
+ try {
1279
+ const { code, message } = cause;
1280
+ return code === "MODULE_NOT_FOUND" && /^Cannot find module '@opentelemetry\/api'/.test(String(message));
1281
+ } catch {
1282
+ return false;
1283
+ }
1284
+ }
1210
1285
  function loadTraceApiFrom(anchor) {
1286
+ let from;
1211
1287
  try {
1212
- const requireFrom = createRequire(anchor());
1213
- const mod = requireFrom("@opentelemetry/api");
1214
- return mod.trace;
1288
+ from = anchor();
1215
1289
  } catch {
1216
- return void 0;
1290
+ return { api: void 0 };
1291
+ }
1292
+ try {
1293
+ const mod = createRequire(from)("@opentelemetry/api");
1294
+ const trace = typeof mod === "object" && mod !== null || typeof mod === "function" ? mod.trace : void 0;
1295
+ if (typeof trace?.getActiveSpan !== "function") {
1296
+ throw new TypeError("@opentelemetry/api resolved but exports no usable `trace` namespace");
1297
+ }
1298
+ return { api: trace };
1299
+ } catch (cause) {
1300
+ return isOtelApiMissing(cause) ? { api: void 0 } : { api: void 0, broken: { cause } };
1217
1301
  }
1218
1302
  }
1219
- function detectOtelTraceApi() {
1220
- return loadTraceApiFrom(() => __filename$1) ?? loadTraceApiFrom(() => join(process.cwd(), "noop.cjs"));
1303
+ function warnBrokenInstallOnce(cause) {
1304
+ if (brokenInstallWarned) {
1305
+ return;
1306
+ }
1307
+ brokenInstallWarned = true;
1308
+ writeStderrSafely(
1309
+ JSON.stringify({
1310
+ level: "warn",
1311
+ logKey: RESERVED_LOG_KEYS.LOGGER_OTEL_API_BROKEN,
1312
+ msg: "@opentelemetry/api is installed but could not be loaded, so traceId and spanId are absent from every entry. Reinstall it, or set otel.shouldAutoInjectTraceContext to false.",
1313
+ err: describeCause(cause)
1314
+ }) + "\n"
1315
+ );
1316
+ }
1317
+ var DEFAULT_ANCHORS = [
1318
+ () => __filename$1,
1319
+ () => join(process.cwd(), "noop.cjs")
1320
+ ];
1321
+ function detectOtelTraceApi(anchors = DEFAULT_ANCHORS) {
1322
+ let broken;
1323
+ for (const anchor of anchors) {
1324
+ const attempt = loadTraceApiFrom(anchor);
1325
+ if (attempt.api !== void 0) {
1326
+ return attempt.api;
1327
+ }
1328
+ broken ??= attempt.broken;
1329
+ }
1330
+ if (broken !== void 0) {
1331
+ warnBrokenInstallOnce(broken.cause);
1332
+ }
1333
+ return void 0;
1221
1334
  }
1222
1335
  function isValidTraceId(traceId) {
1223
1336
  return /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
@@ -1236,17 +1349,21 @@ function createTraceContextMixin(logContext, opts) {
1236
1349
  Object.assign(merged, store);
1237
1350
  }
1238
1351
  if (traceApi) {
1239
- const span = traceApi.getActiveSpan();
1240
- if (span) {
1241
- const ctx = span.spanContext();
1242
- if (isValidTraceId(ctx.traceId) && isValidSpanId(ctx.spanId)) {
1243
- Reflect.set(merged, opts.traceIdField, ctx.traceId);
1244
- Reflect.set(merged, opts.spanIdField, ctx.spanId);
1245
- if (typeof ctx.traceFlags === "number") {
1246
- const flagsByte = ctx.traceFlags & 255;
1247
- Reflect.set(merged, opts.traceFlagsField, flagsByte.toString(16).padStart(2, "0"));
1352
+ try {
1353
+ const span = traceApi.getActiveSpan();
1354
+ if (span) {
1355
+ const { traceId, spanId, traceFlags } = span.spanContext();
1356
+ if (isValidTraceId(traceId) && isValidSpanId(spanId)) {
1357
+ const flags = typeof traceFlags === "number" ? (traceFlags & 255).toString(16).padStart(2, "0") : void 0;
1358
+ Reflect.set(merged, opts.traceIdField, traceId);
1359
+ Reflect.set(merged, opts.spanIdField, spanId);
1360
+ if (flags !== void 0) {
1361
+ Reflect.set(merged, opts.traceFlagsField, flags);
1362
+ }
1248
1363
  }
1249
1364
  }
1365
+ } catch (cause) {
1366
+ warnBrokenInstallOnce(cause);
1250
1367
  }
1251
1368
  }
1252
1369
  return merged;
@@ -1258,52 +1375,6 @@ function compileRedactPaths(extraPaths, shouldSkipDefaults) {
1258
1375
  const sources = shouldSkipDefaults ? [extraPaths] : [DEFAULT_REDACT_PATHS, extraPaths];
1259
1376
  return Array.from(new Set(sources.flat()));
1260
1377
  }
1261
-
1262
- // src/server/utils/report-destination-failure.util.ts
1263
- function reportDestinationFailure(logKey, name, cause, msg) {
1264
- try {
1265
- const err = cause instanceof Error ? {
1266
- type: toSingleLineMessage(String(cause.name)),
1267
- message: toSingleLineMessage(String(cause.message))
1268
- } : { type: "UnknownError", message: toSingleLineMessage(String(cause)) };
1269
- writeStderrSafely(
1270
- JSON.stringify({
1271
- level: "error",
1272
- logKey,
1273
- destination: toSingleLineMessage(name),
1274
- err,
1275
- msg: toSingleLineMessage(msg)
1276
- }) + "\n"
1277
- );
1278
- } catch {
1279
- }
1280
- }
1281
- function safeDestinationName(destination) {
1282
- try {
1283
- return String(destination.name);
1284
- } catch {
1285
- return "unknown";
1286
- }
1287
- }
1288
- function isLogLevel(value) {
1289
- return LOG_LEVEL_PRIORITY.includes(value);
1290
- }
1291
- var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
1292
- function safeMinLevel(destination) {
1293
- if (resolvedMinLevels.has(destination)) {
1294
- return resolvedMinLevels.get(destination);
1295
- }
1296
- let resolved;
1297
- try {
1298
- const read = destination.minLevel;
1299
- resolved = isLogLevel(read) ? read : void 0;
1300
- } catch {
1301
- }
1302
- resolvedMinLevels.set(destination, resolved);
1303
- return resolved;
1304
- }
1305
-
1306
- // src/server/utils/destination-to-stream.ts
1307
1378
  function reportWriteFailure(destination, cause) {
1308
1379
  const name = safeDestinationName(destination);
1309
1380
  reportDestinationFailure(
@@ -1424,6 +1495,19 @@ function walk(value, sensitive, censor, depth2, ancestors, honorToJson = true) {
1424
1495
  if (ancestors.has(value)) {
1425
1496
  return REDACT_CIRCULAR;
1426
1497
  }
1498
+ if (value instanceof Error) {
1499
+ return walkError(value, sensitive, censor, depth2, ancestors, honorToJson);
1500
+ }
1501
+ return walkObject(value, sensitive, censor, depth2, ancestors, honorToJson);
1502
+ }
1503
+ function walkError(error, sensitive, censor, depth2, ancestors, honorToJson) {
1504
+ try {
1505
+ return walkObject(error, sensitive, censor, depth2, ancestors, honorToJson);
1506
+ } catch {
1507
+ return walk(sanitizeError(error), sensitive, censor, depth2, ancestors, honorToJson);
1508
+ }
1509
+ }
1510
+ function walkObject(value, sensitive, censor, depth2, ancestors, honorToJson) {
1427
1511
  const resolvedToJson = resolveToJson(value);
1428
1512
  if (!resolvedToJson.fromAccessor && resolvedToJson.method === BUFFER_TO_JSON) {
1429
1513
  return value;
@@ -2559,6 +2643,22 @@ var MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
2559
2643
  function writeRawToStdout(payload) {
2560
2644
  writeStdoutSafely(payload);
2561
2645
  }
2646
+ var RESOLUTION_FAILURE_CODES = ["MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"];
2647
+ function isPinoPrettyMissing(cause) {
2648
+ try {
2649
+ const { code, message } = cause;
2650
+ return RESOLUTION_FAILURE_CODES.includes(code) && /^Cannot find (?:module|package) 'pino-pretty'/.test(String(message));
2651
+ } catch {
2652
+ return false;
2653
+ }
2654
+ }
2655
+ function describeFailure(cause) {
2656
+ try {
2657
+ return toSingleLineMessage(cause instanceof Error ? cause.message : String(cause));
2658
+ } catch {
2659
+ return "unreadable value";
2660
+ }
2661
+ }
2562
2662
  var PrettyDevDestination = class {
2563
2663
  /**
2564
2664
  * @param opts.minLevel — Optional minimum level filter applied by the
@@ -2631,10 +2731,11 @@ var PrettyDevDestination = class {
2631
2731
  // and the last-resort rescue.
2632
2732
  destination: process.stdout
2633
2733
  });
2634
- } catch {
2734
+ } catch (cause) {
2635
2735
  this.initFailed = true;
2636
2736
  throw new Error(
2637
- "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
2737
+ isPinoPrettyMissing(cause) ? "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`." : `[PrettyDevDestination] pino-pretty failed to start: ${describeFailure(cause)}`,
2738
+ { cause }
2638
2739
  );
2639
2740
  }
2640
2741
  this.flushBuffer((payload) => {
@@ -3099,7 +3200,13 @@ var PinoUnexpectedErrorSink = class {
3099
3200
  void 0,
3100
3201
  metadata
3101
3202
  );
3102
- } catch {
3203
+ } catch (cause) {
3204
+ reportDestinationFailure(
3205
+ RESERVED_LOG_KEYS.HTTP_UNEXPECTED_ERROR_DROPPED,
3206
+ "PinoUnexpectedErrorSink",
3207
+ cause,
3208
+ "An unexpected-error report could not be logged and was dropped."
3209
+ );
3103
3210
  }
3104
3211
  }
3105
3212
  };
@@ -18,12 +18,14 @@ var RESERVED_LOG_KEYS = Object.freeze({
18
18
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
19
19
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
20
20
  HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
21
+ HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED",
21
22
  METHOD_EXECUTION: "METHOD_EXECUTION",
22
23
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
23
24
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
24
25
  LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
25
26
  LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
26
- LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
27
+ LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED",
28
+ LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN"
27
29
  });
28
30
  var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
29
31
  /**
@@ -239,12 +239,14 @@ declare const RESERVED_LOG_KEYS: Readonly<{
239
239
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
240
240
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
241
241
  readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
242
+ readonly HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED";
242
243
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
243
244
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
244
245
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
245
246
  readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
246
247
  readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
247
248
  readonly LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED";
249
+ readonly LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN";
248
250
  }>;
249
251
  /**
250
252
  * Keys that are reserved but intentionally never emitted, each with the reason.
@@ -239,12 +239,14 @@ declare const RESERVED_LOG_KEYS: Readonly<{
239
239
  readonly HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED";
240
240
  readonly HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED";
241
241
  readonly HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR";
242
+ readonly HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED";
242
243
  readonly METHOD_EXECUTION: "METHOD_EXECUTION";
243
244
  readonly METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION";
244
245
  readonly LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED";
245
246
  readonly LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED";
246
247
  readonly LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED";
247
248
  readonly LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED";
249
+ readonly LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN";
248
250
  }>;
249
251
  /**
250
252
  * Keys that are reserved but intentionally never emitted, each with the reason.
@@ -16,12 +16,14 @@ var RESERVED_LOG_KEYS = Object.freeze({
16
16
  HTTP_EXCEPTION_HANDLED: "HTTP_EXCEPTION_HANDLED",
17
17
  HTTP_EXCEPTION_UNHANDLED: "HTTP_EXCEPTION_UNHANDLED",
18
18
  HTTP_UNEXPECTED_ERROR: "HTTP_UNEXPECTED_ERROR",
19
+ HTTP_UNEXPECTED_ERROR_DROPPED: "HTTP_UNEXPECTED_ERROR_DROPPED",
19
20
  METHOD_EXECUTION: "METHOD_EXECUTION",
20
21
  METHOD_SLOW_EXECUTION: "METHOD_SLOW_EXECUTION",
21
22
  LOGGER_DESTINATION_INIT_FAILED: "LOGGER_DESTINATION_INIT_FAILED",
22
23
  LOGGER_DESTINATION_WRITE_FAILED: "LOGGER_DESTINATION_WRITE_FAILED",
23
24
  LOGGER_ENTRY_TRUNCATED: "LOGGER_ENTRY_TRUNCATED",
24
- LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED"
25
+ LOGGER_REDACTION_FAILED: "LOGGER_REDACTION_FAILED",
26
+ LOGGER_OTEL_API_BROKEN: "LOGGER_OTEL_API_BROKEN"
25
27
  });
26
28
  var RESERVED_LOG_KEYS_NOT_EMITTED = Object.freeze({
27
29
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  "bench": "node --expose-gc --import tsx bench/throughput.bench.ts",
58
58
  "build": "pnpm clean && tsup",
59
59
  "check:exports": "attw --pack .",
60
+ "check:api": "node scripts/check-api-surface.mjs",
60
61
  "check:docs": "node scripts/check-doc-snippets.mjs",
61
62
  "check:mutants": "node scripts/check-mutation-directives.mjs",
62
63
  "check:published": "node scripts/check-published-surface.mjs",