@zudojs/logger 1.2.0 → 1.3.0

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.
Files changed (22) hide show
  1. package/README.md +23 -4
  2. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.js +62 -17
  3. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.d.ts +6 -1
  4. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +8 -3
  5. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.js +23 -2
  6. package/dist/loggerEntry/loggerEntryCreate.js +2 -1
  7. package/dist/loggerEntry/loggerEntryHelpers/index.d.ts +1 -1
  8. package/dist/loggerEntry/loggerEntryHelpers/index.js +1 -1
  9. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +18 -5
  10. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +66 -21
  11. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.js +2 -1
  12. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.d.ts +5 -0
  13. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js +43 -12
  14. package/dist/loggerFactory/loggerFactory.core.d.ts +7 -0
  15. package/dist/loggerFactory/loggerFactory.core.js +10 -0
  16. package/dist/loggerFormatter/loggerFormatter.core.js +3 -2
  17. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.js +26 -13
  18. package/dist/loggerLevel/loggerLevel.type.js +3 -2
  19. package/dist/loggerManager/loggerManager.core.d.ts +10 -0
  20. package/dist/loggerManager/loggerManager.core.js +24 -7
  21. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +9 -0
  22. package/package.json +2 -2
package/README.md CHANGED
@@ -55,7 +55,11 @@ that entry, and a failure from a timer-triggered flush is rethrown by the
55
55
  next `flush()` or `close()`.
56
56
 
57
57
  `transportTimeout` (default 10s) bounds every transport write, so a
58
- transport that stops responding cannot hang `flush()` or `close()`.
58
+ transport that stops responding cannot hang `flush()` or `close()`. A
59
+ write that exceeds it fails with `LoggerTimeoutError`, carrying
60
+ `transportName` and `timeout`; other write failures are reported as
61
+ `LoggerTransportError` with `transportName` set, and formatter failures
62
+ as `LoggerFormatterError` with `formatterName` set.
59
63
 
60
64
  `throwTransportErrors` (default `false`) rethrows transport and formatter
61
65
  failures instead of dropping them. A synchronous transport throws from the
@@ -88,8 +92,14 @@ against `DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret,
88
92
  token, jwt, bearer, auth, authorization, cookie, session, sid,
89
93
  credential, api key, private key, client secret, card number, cvv, ssn,
90
94
  pin, otp and more — so `sessionId` and `cardNumber` are redacted while
91
- `passenger` and `authorId` are not. Nested objects, arrays and getters
92
- are all covered. Passing `redact.pattern` replaces the word matcher with
95
+ `passenger` and `authorId` are not. Nested objects, arrays, `Map`,
96
+ `Set` and getters are all covered — a `Map` is redacted per key and a
97
+ `Set` becomes an array. A getter that THROWS yields `"[Unreadable]"`
98
+ for that field: the rest of the entry is logged and the read failure is
99
+ reported through the same path as transport and formatter failures
100
+ (dropped by default, rethrown with `throwTransportErrors`), so a lazy
101
+ ORM relation can never abort the caller's log statement. Passing
102
+ `redact.pattern` replaces the word matcher with
93
103
  your own RegExp (the old substring default is still exported as
94
104
  `DEFAULT_LOGGER_SECRET_PATTERN`).
95
105
 
@@ -115,7 +125,10 @@ indented so none can start at column 0 and pass for a record. The JSON formatter
115
125
 
116
126
  Metadata is normalized before serialization, so circular references
117
127
  (`"[Circular]"`), BigInt values and functions never make a formatter
118
- throw and silently drop the record.
128
+ throw and silently drop the record. Only a genuine back-edge becomes
129
+ `"[Circular]"` — the walk tracks the ancestor path, so an object
130
+ referenced from two places in one payload (`{ actor: user, target: user }`)
131
+ is logged in full both times.
119
132
 
120
133
  ## Context
121
134
 
@@ -150,6 +163,12 @@ Pass `{ colors: true }` in the formatter context to colourize the level
150
163
  tag of text output. Colour codes are emitted only around the fixed level
151
164
  name, never around user-supplied text.
152
165
 
166
+ A formatter returns either a string or an object
167
+ (`LoggerFormattedOutput`). A string becomes the payload's `message`; an
168
+ object is merged OVER the entry, so `createStructuredLoggerFormatter()`
169
+ hands the transport its structured record (with an ISO-string
170
+ `timestamp` and serialized metadata) rather than the raw entry.
171
+
153
172
  ## Use Cases
154
173
 
155
174
  - Application logging
@@ -4,8 +4,8 @@
4
4
  import { formatLoggerEntry } from "../../../loggerFormatter/loggerFormatter.core.js";
5
5
  import { createLoggerTransport, writeLoggerTransport, } from "../../../loggerTransport/loggerTransport.core.js";
6
6
  import { isLoggerTransport } from "../../../loggerTransport/loggerTransportGuard.js";
7
- import { LoggerFormatterError, LoggerTransportError, } from "../../../loggerErrors/loggerError.base.js";
8
- import { toLoggerError } from "../../../loggerErrors/loggerError.helpers.js";
7
+ import { LoggerFormatterError, LoggerTimeoutError, LoggerTransportError, } from "../../../loggerErrors/loggerError.base.js";
8
+ import { createLoggerFormatterError, createLoggerTransportError, } from "../../../loggerErrors/loggerError.helpers.js";
9
9
  /**
10
10
  * Default formatter used when no formatter is configured.
11
11
  */
@@ -33,7 +33,11 @@ async function withTransportTimeout(timeoutMs, transportName, operation) {
33
33
  let timer;
34
34
  const expiry = new Promise((_resolve, reject) => {
35
35
  timer = setTimeout(() => {
36
- reject(new LoggerTransportError(`Transport "${transportName}" did not complete within ${timeoutMs}ms.`));
36
+ // LoggerTimeoutError carries `transportName` and `timeout`, which
37
+ // the bare LoggerTransportError raised here before did not — so
38
+ // `catch (e) { if (e instanceof LoggerTimeoutError) retry() }`
39
+ // could never match.
40
+ reject(new LoggerTimeoutError(transportName, timeoutMs));
37
41
  }, timeoutMs);
38
42
  // This timer bounds a write; it is not work in its own right. Left
39
43
  // referenced it keeps the event loop alive, so a process that has
@@ -53,6 +57,50 @@ async function withTransportTimeout(timeoutMs, transportName, operation) {
53
57
  void operation.catch(() => { });
54
58
  }
55
59
  }
60
+ /**
61
+ * Builds the payload handed to a transport.
62
+ *
63
+ * `LoggerFormattedOutput` is `string | Record<string, unknown>`. A
64
+ * string replaces the message; an OBJECT is the formatted record and is
65
+ * merged over the entry, so a transport sees the formatter's fields.
66
+ * The object branch used to be computed and then discarded, which made
67
+ * `createStructuredLoggerFormatter()` pure overhead on every log call
68
+ * and handed the transport the raw, unformatted entry instead.
69
+ */
70
+ function toTransportPayload(entry, formatted) {
71
+ if (typeof formatted === "string") {
72
+ return { ...entry, message: formatted };
73
+ }
74
+ if (formatted !== null &&
75
+ typeof formatted === "object" &&
76
+ !Array.isArray(formatted)) {
77
+ return {
78
+ ...entry,
79
+ ...formatted,
80
+ };
81
+ }
82
+ return entry;
83
+ }
84
+ /** Name of the configured formatter, for error reporting. */
85
+ function resolveFormatterName(configuration) {
86
+ const formatter = configuration.formatter;
87
+ if (typeof formatter === "string") {
88
+ return formatter;
89
+ }
90
+ return formatter.name ?? "formatter";
91
+ }
92
+ /** Wraps a transport failure, keeping an already-typed logger error. */
93
+ function toTransportError(transportName, error) {
94
+ return error instanceof LoggerTransportError
95
+ ? error
96
+ : createLoggerTransportError(transportName, error);
97
+ }
98
+ /** Wraps a formatter failure, keeping an already-typed logger error. */
99
+ function toFormatterError(formatterName, error) {
100
+ return error instanceof LoggerFormatterError
101
+ ? error
102
+ : createLoggerFormatterError(formatterName, error);
103
+ }
56
104
  /**
57
105
  * Writes to a transport.
58
106
  */
@@ -61,12 +109,7 @@ async function writeTransport(configuration, transport, entry, formatted) {
61
109
  loggerName: configuration.name,
62
110
  environment: configuration.environment,
63
111
  };
64
- if (typeof formatted === "string") {
65
- const formattedEntry = { ...entry, message: formatted };
66
- await writeLoggerTransport(transport.transport, formattedEntry, transportContext);
67
- return;
68
- }
69
- await writeLoggerTransport(transport.transport, entry, transportContext);
112
+ await writeLoggerTransport(transport.transport, toTransportPayload(entry, formatted), transportContext);
70
113
  }
71
114
  /**
72
115
  * Formats an entry with the configured formatter.
@@ -94,7 +137,7 @@ function writeTransportMaybeSync(configuration, transport, entry, formatted) {
94
137
  loggerName: configuration.name,
95
138
  environment: configuration.environment,
96
139
  };
97
- const payload = typeof formatted === "string" ? { ...entry, message: formatted } : entry;
140
+ const payload = toTransportPayload(entry, formatted);
98
141
  const target = transport.transport;
99
142
  return typeof target === "function"
100
143
  ? target(payload, transportContext)
@@ -109,24 +152,24 @@ export async function dispatchEntry(configuration, entry, handleError) {
109
152
  formatted = formatEntry(configuration, entry);
110
153
  }
111
154
  catch (error) {
112
- const formatterError = new LoggerFormatterError(`Failed to format log entry: ${toLoggerError(error).message}`, { cause: error });
113
- handleError(formatterError);
155
+ handleError(toFormatterError(resolveFormatterName(configuration), error));
114
156
  return;
115
157
  }
116
158
  for (const transport of configuration.transports) {
159
+ let transportName = "transport";
117
160
  try {
118
161
  if (!isLoggerTransport(transport)) {
119
162
  continue;
120
163
  }
121
164
  const registered = createLoggerTransport(transport);
165
+ transportName = registered.name;
122
166
  if (!registered.enabled) {
123
167
  continue;
124
168
  }
125
169
  await withTransportTimeout(configuration.transportTimeout, registered.name, writeTransport(configuration, registered, entry, formatted));
126
170
  }
127
171
  catch (error) {
128
- const transportError = new LoggerTransportError(`Failed to write log entry: ${toLoggerError(error).message}`, { cause: error });
129
- handleError(transportError);
172
+ handleError(toTransportError(transportName, error));
130
173
  }
131
174
  }
132
175
  }
@@ -153,28 +196,30 @@ export function dispatchEntrySync(configuration, entry, handleError) {
153
196
  formatted = formatEntry(configuration, entry);
154
197
  }
155
198
  catch (error) {
156
- handleError(new LoggerFormatterError(`Failed to format log entry: ${toLoggerError(error).message}`, { cause: error }));
199
+ handleError(toFormatterError(resolveFormatterName(configuration), error));
157
200
  return;
158
201
  }
159
202
  const pending = [];
160
203
  for (const transport of configuration.transports) {
204
+ let transportName = "transport";
161
205
  try {
162
206
  if (!isLoggerTransport(transport)) {
163
207
  continue;
164
208
  }
165
209
  const registered = createLoggerTransport(transport);
210
+ transportName = registered.name;
166
211
  if (!registered.enabled) {
167
212
  continue;
168
213
  }
169
214
  const result = writeTransportMaybeSync(configuration, registered, entry, formatted);
170
215
  if (result instanceof Promise) {
171
216
  pending.push(withTransportTimeout(configuration.transportTimeout, registered.name, result).catch((error) => {
172
- handleError(new LoggerTransportError(`Failed to write log entry: ${toLoggerError(error).message}`, { cause: error }));
217
+ handleError(toTransportError(registered.name, error));
173
218
  }));
174
219
  }
175
220
  }
176
221
  catch (error) {
177
- handleError(new LoggerTransportError(`Failed to write log entry: ${toLoggerError(error).message}`, { cause: error }));
222
+ handleError(toTransportError(transportName, error));
178
223
  }
179
224
  }
180
225
  if (pending.length === 0) {
@@ -6,8 +6,13 @@ import type { LoggerEntry } from "../../../loggerEntry/loggerEntry.type.js";
6
6
  import type { LoggerConfiguration, LogOptions } from "../../../loggerOptions/loggerOptions.type.js";
7
7
  /**
8
8
  * Creates a normalized log entry.
9
+ *
10
+ * @param onMetadataError - Notified when a metadata property could not
11
+ * be read (a throwing getter). The field is replaced with a marker
12
+ * and the entry is still produced, so the caller's log statement
13
+ * never aborts; reporting is the caller's job.
9
14
  */
10
15
  export declare function createEntry(configuration: LoggerConfiguration, contextStorage: {
11
16
  get(): import("../../../loggerContext/loggerContext.core.js").LoggerContext | undefined;
12
- }, level: LoggerLevel, message: string, options: LogOptions): LoggerEntry;
17
+ }, level: LoggerLevel, message: string, options: LogOptions, onMetadataError?: (key: string, error: unknown) => void): LoggerEntry;
13
18
  //# sourceMappingURL=loggerCoreMethods.entry.d.ts.map
@@ -6,8 +6,13 @@ import { createSecretMatcher, redactLogValue, LOGGER_REDACTION_TOKEN, } from "..
6
6
  import { contextToLogMetadata, createLoggerContext, } from "../../../loggerContext/loggerContext.core.js";
7
7
  /**
8
8
  * Creates a normalized log entry.
9
+ *
10
+ * @param onMetadataError - Notified when a metadata property could not
11
+ * be read (a throwing getter). The field is replaced with a marker
12
+ * and the entry is still produced, so the caller's log statement
13
+ * never aborts; reporting is the caller's job.
9
14
  */
10
- export function createEntry(configuration, contextStorage, level, message, options) {
15
+ export function createEntry(configuration, contextStorage, level, message, options, onMetadataError) {
11
16
  const activeContext = configuration.inheritContext
12
17
  ? contextStorage.get()
13
18
  : undefined;
@@ -29,7 +34,7 @@ export function createEntry(configuration, contextStorage, level, message, optio
29
34
  // path can bypass it — including nested objects, arrays and getters.
30
35
  const isSecret = createSecretMatcher(configuration.redact);
31
36
  const replacement = configuration.redact.replacement ?? LOGGER_REDACTION_TOKEN;
32
- const metadata = redactLogValue(rawMetadata, isSecret, replacement);
37
+ const metadata = redactLogValue(rawMetadata, isSecret, replacement, undefined, onMetadataError);
33
38
  const context = options.context
34
39
  ? createLoggerContext({
35
40
  parent: activeContext,
@@ -46,7 +51,7 @@ export function createEntry(configuration, contextStorage, level, message, optio
46
51
  context: context
47
52
  ? {
48
53
  ...context.identifiers,
49
- metadata: redactLogValue(context.metadata, isSecret, replacement),
54
+ metadata: redactLogValue(context.metadata, isSecret, replacement, undefined, onMetadataError),
50
55
  }
51
56
  : undefined,
52
57
  source: options.source,
@@ -2,7 +2,8 @@
2
2
  * ZudojsLogger level methods.
3
3
  */
4
4
  import { LoggerLevel, shouldLog, } from "../../../loggerLevel/loggerLevel.type.js";
5
- import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
5
+ import { InvalidLoggerEntryError, LoggerConfigurationError, } from "../../../loggerErrors/loggerError.base.js";
6
+ import { toLoggerError } from "../../../loggerErrors/loggerError.helpers.js";
6
7
  import { createEntry } from "./loggerCoreMethods.entry.js";
7
8
  import { dispatchEntrySync } from "./loggerCoreMethods.dispatch.js";
8
9
  /**
@@ -17,7 +18,24 @@ export function logAtLevel(ctx, level, message, options = {}) {
17
18
  if (typeof message !== "string") {
18
19
  throw new LoggerConfigurationError("Logger message must be a string.");
19
20
  }
20
- const entry = createEntry(ctx.configuration, ctx.contextStorage, level, message, options);
21
+ // Entry construction reads caller-supplied metadata, including
22
+ // getters. A throwing accessor used to propagate straight out of
23
+ // logger.info(...) and abort the caller — and only when the level
24
+ // let the call through, so the same code was a silent no-op at one
25
+ // log level and a crash at another. The offending field becomes a
26
+ // marker and the failure is reported like every other infrastructure
27
+ // failure, AFTER the line has been dispatched.
28
+ const metadataFailures = [];
29
+ let entry;
30
+ try {
31
+ entry = createEntry(ctx.configuration, ctx.contextStorage, level, message, options, (key, error) => {
32
+ metadataFailures.push(new InvalidLoggerEntryError(`Failed to read log metadata field "${key}": ${toLoggerError(error).message}`, { cause: error }));
33
+ });
34
+ }
35
+ catch (error) {
36
+ ctx.handleInfrastructureError(new InvalidLoggerEntryError(`Failed to build log entry: ${toLoggerError(error).message}`, { cause: error }));
37
+ return;
38
+ }
21
39
  // Dispatch is asynchronous. It used to be fired and forgotten, so
22
40
  // flush() and close() could return while entries were still in
23
41
  // flight — messages were lost on process exit. Registering the
@@ -26,5 +44,8 @@ export function logAtLevel(ctx, level, message, options = {}) {
26
44
  if (dispatch) {
27
45
  ctx.trackDispatch(dispatch);
28
46
  }
47
+ for (const failure of metadataFailures) {
48
+ ctx.handleInfrastructureError(failure);
49
+ }
29
50
  }
30
51
  //# sourceMappingURL=loggerCoreMethods.level.js.map
@@ -2,6 +2,7 @@
2
2
  * Logger entry creation from input.
3
3
  */
4
4
  import { createLoggerEntryId } from "./loggerEntry.core.js";
5
+ import { InvalidLoggerEntryError } from "../loggerErrors/loggerError.base.js";
5
6
  import { loggerLevelNameFallback } from "./loggerEntryHelpers/loggerEntryHelpers.serialize.js";
6
7
  /**
7
8
  * Creates a normalized LoggerEntry.
@@ -10,7 +11,7 @@ export function createLoggerEntry(input) {
10
11
  const timestamp = input.timestamp ?? new Date();
11
12
  const timestampMs = timestamp.getTime();
12
13
  if (!Number.isFinite(timestampMs)) {
13
- throw new RangeError("Logger entry timestamp must be a valid date.");
14
+ throw new InvalidLoggerEntryError("Logger entry timestamp must be a valid date.");
14
15
  }
15
16
  const levelName = input.levelName ?? loggerLevelNameFallback(input.level);
16
17
  return Object.freeze({
@@ -6,6 +6,6 @@
6
6
  export type { LogValue, LogMetadata, LoggerSource, LoggerEntryContext, } from "./loggerEntryHelpers.types.js";
7
7
  export type { LoggerEntry, LoggerEntryInput, } from "./loggerEntryHelpers.interfaces.js";
8
8
  export { serializeLoggerEntry, serializeLoggerError, serializeLoggerValue, loggerLevelNameFallback, } from "./loggerEntryHelpers.serialize.js";
9
- export { LOGGER_REDACTION_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
9
+ export { LOGGER_REDACTION_TOKEN, LOGGER_UNREADABLE_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
10
10
  export type { LoggerRedactionOptions } from "./loggerEntryHelpers.sanitize.js";
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * Logger entry helper types and utilities.
5
5
  */
6
6
  export { serializeLoggerEntry, serializeLoggerError, serializeLoggerValue, loggerLevelNameFallback, } from "./loggerEntryHelpers.serialize.js";
7
- export { LOGGER_REDACTION_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
7
+ export { LOGGER_REDACTION_TOKEN, LOGGER_UNREADABLE_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -12,6 +12,14 @@
12
12
  * Replacement token written in place of a redacted value.
13
13
  */
14
14
  export declare const LOGGER_REDACTION_TOKEN = "[REDACTED]";
15
+ /**
16
+ * Replacement token written in place of a property whose getter threw.
17
+ *
18
+ * A throwing accessor used to propagate out of `logger.info(...)` and
19
+ * abort the caller. The field is marked instead, the rest of the entry
20
+ * is logged, and the failure is reported as an infrastructure error.
21
+ */
22
+ export declare const LOGGER_UNREADABLE_TOKEN = "[Unreadable]";
15
23
  /**
16
24
  * Legacy substring pattern for secret field names.
17
25
  *
@@ -64,10 +72,15 @@ export declare function createSecretMatcher(options?: LoggerRedactionOptions): (
64
72
  /**
65
73
  * Recursively replaces secret-named fields with a redaction token.
66
74
  *
67
- * Nesting, arrays and getters are all covered: the walk descends into
68
- * every enumerable own property, and a getter is read here — once,
69
- * before the value can reach a transport. Cycles resolve to
70
- * "[Circular]" rather than recursing forever.
75
+ * Nesting, arrays, `Map`, `Set` and getters are all covered: the walk
76
+ * descends into every enumerable own property, and a getter is read
77
+ * here — once, before the value can reach a transport. A getter that
78
+ * throws yields {@link LOGGER_UNREADABLE_TOKEN} and is reported through
79
+ * `onReadError` instead of aborting the caller's log statement.
80
+ *
81
+ * `seen` tracks the ANCESTOR PATH only (each object is unmarked as the
82
+ * walk ascends), so a back-edge resolves to "[Circular]" while an
83
+ * object merely referenced twice in one payload is logged both times.
71
84
  */
72
- export declare function redactLogValue(value: unknown, isSecret: (key: string) => boolean, replacement?: string, seen?: WeakSet<object>): unknown;
85
+ export declare function redactLogValue(value: unknown, isSecret: (key: string) => boolean, replacement?: string, seen?: WeakSet<object>, onReadError?: (key: string, error: unknown) => void): unknown;
73
86
  //# sourceMappingURL=loggerEntryHelpers.sanitize.d.ts.map
@@ -19,6 +19,14 @@ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
19
19
  * Replacement token written in place of a redacted value.
20
20
  */
21
21
  export const LOGGER_REDACTION_TOKEN = "[REDACTED]";
22
+ /**
23
+ * Replacement token written in place of a property whose getter threw.
24
+ *
25
+ * A throwing accessor used to propagate out of `logger.info(...)` and
26
+ * abort the caller. The field is marked instead, the rest of the entry
27
+ * is logged, and the failure is reported as an infrastructure error.
28
+ */
29
+ export const LOGGER_UNREADABLE_TOKEN = "[Unreadable]";
22
30
  /**
23
31
  * Legacy substring pattern for secret field names.
24
32
  *
@@ -85,15 +93,32 @@ export function createSecretMatcher(options = {}) {
85
93
  : configured;
86
94
  return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
87
95
  }
96
+ /** Defines an own, enumerable property without touching a setter. */
97
+ function defineLogProperty(target, key, value) {
98
+ // defineProperty, never assignment: a "__proto__" key coming from
99
+ // JSON.parse of untrusted input would otherwise reach the inherited
100
+ // setter and replace this object's prototype.
101
+ Object.defineProperty(target, key, {
102
+ value,
103
+ enumerable: true,
104
+ writable: true,
105
+ configurable: true,
106
+ });
107
+ }
88
108
  /**
89
109
  * Recursively replaces secret-named fields with a redaction token.
90
110
  *
91
- * Nesting, arrays and getters are all covered: the walk descends into
92
- * every enumerable own property, and a getter is read here — once,
93
- * before the value can reach a transport. Cycles resolve to
94
- * "[Circular]" rather than recursing forever.
111
+ * Nesting, arrays, `Map`, `Set` and getters are all covered: the walk
112
+ * descends into every enumerable own property, and a getter is read
113
+ * here — once, before the value can reach a transport. A getter that
114
+ * throws yields {@link LOGGER_UNREADABLE_TOKEN} and is reported through
115
+ * `onReadError` instead of aborting the caller's log statement.
116
+ *
117
+ * `seen` tracks the ANCESTOR PATH only (each object is unmarked as the
118
+ * walk ascends), so a back-edge resolves to "[Circular]" while an
119
+ * object merely referenced twice in one payload is logged both times.
95
120
  */
96
- export function redactLogValue(value, isSecret, replacement = LOGGER_REDACTION_TOKEN, seen = new WeakSet()) {
121
+ export function redactLogValue(value, isSecret, replacement = LOGGER_REDACTION_TOKEN, seen = new WeakSet(), onReadError) {
97
122
  if (value === null || typeof value !== "object") {
98
123
  return value;
99
124
  }
@@ -104,23 +129,43 @@ export function redactLogValue(value, isSecret, replacement = LOGGER_REDACTION_T
104
129
  return "[Circular]";
105
130
  }
106
131
  seen.add(value);
107
- if (Array.isArray(value)) {
108
- return value.map((item) => redactLogValue(item, isSecret, replacement, seen));
132
+ try {
133
+ const descend = (item) => redactLogValue(item, isSecret, replacement, seen, onReadError);
134
+ if (Array.isArray(value)) {
135
+ return value.map(descend);
136
+ }
137
+ if (value instanceof Set) {
138
+ return Array.from(value, descend);
139
+ }
140
+ const result = {};
141
+ if (value instanceof Map) {
142
+ for (const [key, item] of value.entries()) {
143
+ const name = typeof key === "string" ? key : String(key);
144
+ defineLogProperty(result, name, isSecret(name) ? replacement : descend(item));
145
+ }
146
+ return result;
147
+ }
148
+ for (const key of Object.keys(value)) {
149
+ if (isSecret(key)) {
150
+ // Never even read a secret-named accessor.
151
+ defineLogProperty(result, key, replacement);
152
+ continue;
153
+ }
154
+ let item;
155
+ try {
156
+ item = value[key];
157
+ }
158
+ catch (error) {
159
+ onReadError?.(key, error);
160
+ defineLogProperty(result, key, LOGGER_UNREADABLE_TOKEN);
161
+ continue;
162
+ }
163
+ defineLogProperty(result, key, descend(item));
164
+ }
165
+ return result;
109
166
  }
110
- const result = {};
111
- for (const [key, item] of Object.entries(value)) {
112
- // defineProperty, never assignment: a "__proto__" key coming from
113
- // JSON.parse of untrusted input would otherwise reach the inherited
114
- // setter and replace this object's prototype.
115
- Object.defineProperty(result, key, {
116
- value: isSecret(key)
117
- ? replacement
118
- : redactLogValue(item, isSecret, replacement, seen),
119
- enumerable: true,
120
- writable: true,
121
- configurable: true,
122
- });
167
+ finally {
168
+ seen.delete(value);
123
169
  }
124
- return result;
125
170
  }
126
171
  //# sourceMappingURL=loggerEntryHelpers.sanitize.js.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Logger entry serialization helpers.
3
3
  */
4
+ import { InvalidLoggerLevelError } from "../../loggerErrors/loggerError.base.js";
4
5
  import { serializeLoggerError, serializeLoggerValue, } from "./loggerEntryHelpers.valueSerialize.js";
5
6
  /**
6
7
  * Returns a plain serializable representation of an entry.
@@ -49,7 +50,7 @@ export function loggerLevelNameFallback(level) {
49
50
  case 5:
50
51
  return "trace";
51
52
  default:
52
- throw new RangeError(`Unknown logger level: ${String(level)}`);
53
+ throw new InvalidLoggerLevelError(level);
53
54
  }
54
55
  }
55
56
  //# sourceMappingURL=loggerEntryHelpers.serialize.js.map
@@ -11,6 +11,11 @@ export declare function serializeLoggerError(error: {
11
11
  }): Record<string, unknown>;
12
12
  /**
13
13
  * Converts arbitrary values into safer serializable values.
14
+ *
15
+ * `seen` tracks the ANCESTOR PATH only, so only a genuine back-edge
16
+ * becomes "[Circular]"; `Map` and `Set` keep their contents; and a
17
+ * property whose getter throws becomes "[Unreadable]" rather than
18
+ * taking the whole log line down.
14
19
  */
15
20
  export declare function serializeLoggerValue(value: unknown, seen?: WeakSet<object>): unknown;
16
21
  //# sourceMappingURL=loggerEntryHelpers.valueSerialize.d.ts.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Logger entry value serialization.
3
3
  */
4
+ import { LOGGER_UNREADABLE_TOKEN } from "./loggerEntryHelpers.sanitize.js";
4
5
  /**
5
6
  * Serializes an error-like object into a plain object.
6
7
  */
@@ -13,6 +14,11 @@ export function serializeLoggerError(error) {
13
14
  }
14
15
  /**
15
16
  * Converts arbitrary values into safer serializable values.
17
+ *
18
+ * `seen` tracks the ANCESTOR PATH only, so only a genuine back-edge
19
+ * becomes "[Circular]"; `Map` and `Set` keep their contents; and a
20
+ * property whose getter throws becomes "[Unreadable]" rather than
21
+ * taking the whole log line down.
16
22
  */
17
23
  export function serializeLoggerValue(value, seen = new WeakSet()) {
18
24
  if (value === null ||
@@ -44,21 +50,46 @@ export function serializeLoggerValue(value, seen = new WeakSet()) {
44
50
  return "[Circular]";
45
51
  }
46
52
  seen.add(value);
47
- if (Array.isArray(value)) {
48
- return value.map((item) => serializeLoggerValue(item, seen));
49
- }
50
- const result = {};
51
- for (const [key, item] of Object.entries(value)) {
53
+ try {
54
+ if (Array.isArray(value)) {
55
+ return value.map((item) => serializeLoggerValue(item, seen));
56
+ }
57
+ if (value instanceof Set) {
58
+ return Array.from(value, (item) => serializeLoggerValue(item, seen));
59
+ }
60
+ const result = {};
52
61
  // defineProperty, never assignment: a "__proto__" key from an
53
62
  // untrusted payload would otherwise reach the inherited setter and
54
63
  // replace the serialized object's prototype.
55
- Object.defineProperty(result, key, {
56
- value: serializeLoggerValue(item, seen),
57
- enumerable: true,
58
- writable: true,
59
- configurable: true,
60
- });
64
+ const define = (key, item) => {
65
+ Object.defineProperty(result, key, {
66
+ value: item,
67
+ enumerable: true,
68
+ writable: true,
69
+ configurable: true,
70
+ });
71
+ };
72
+ if (value instanceof Map) {
73
+ for (const [key, item] of value.entries()) {
74
+ define(typeof key === "string" ? key : String(key), serializeLoggerValue(item, seen));
75
+ }
76
+ return result;
77
+ }
78
+ for (const key of Object.keys(value)) {
79
+ let item;
80
+ try {
81
+ item = value[key];
82
+ }
83
+ catch {
84
+ define(key, LOGGER_UNREADABLE_TOKEN);
85
+ continue;
86
+ }
87
+ define(key, serializeLoggerValue(item, seen));
88
+ }
89
+ return result;
90
+ }
91
+ finally {
92
+ seen.delete(value);
61
93
  }
62
- return result;
63
94
  }
64
95
  //# sourceMappingURL=loggerEntryHelpers.valueSerialize.js.map
@@ -17,6 +17,13 @@ export declare class LoggerFactory {
17
17
  * instance is returned unless `forceNew` is enabled.
18
18
  */
19
19
  create(name?: string, options?: LoggerOptions, forceNew?: boolean): Logger;
20
+ /**
21
+ * Registers an existing logger under its own name (or `name`).
22
+ *
23
+ * An adopted logger is then covered by `flushAll()`, `disposeAll()`,
24
+ * `getAll()` and `size` exactly like one the factory created itself.
25
+ */
26
+ register(logger: Logger, name?: string): Logger;
20
27
  /** Returns an existing logger. */
21
28
  get(name: string): Logger | undefined;
22
29
  /** Gets an existing logger or creates it. */
@@ -31,6 +31,16 @@ export class LoggerFactory {
31
31
  this.loggers.set(loggerName, logger);
32
32
  return logger;
33
33
  }
34
+ /**
35
+ * Registers an existing logger under its own name (or `name`).
36
+ *
37
+ * An adopted logger is then covered by `flushAll()`, `disposeAll()`,
38
+ * `getAll()` and `size` exactly like one the factory created itself.
39
+ */
40
+ register(logger, name) {
41
+ this.loggers.set(name ?? logger.name, logger);
42
+ return logger;
43
+ }
34
44
  /** Returns an existing logger. */
35
45
  get(name) {
36
46
  return this.loggers.get(name);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Core logger formatter functions.
3
3
  */
4
+ import { LoggerFormatterNotFoundError } from "../loggerErrors/loggerError.base.js";
4
5
  import { createLoggerFormatterId, isLoggerFormatterFunction, isLoggerFormatterObject, isLoggerFormatter, } from "./loggerFormatterGuard.js";
5
6
  /**
6
7
  * Creates a function-backed formatter.
@@ -20,7 +21,7 @@ export function createLoggerFormatter(formatter, options = {}) {
20
21
  return formatter(entry, context);
21
22
  }
22
23
  if (typeof formatter === "string") {
23
- throw new Error(`Cannot format with string identifier "${formatter}" directly. Resolve the formatter first.`);
24
+ throw new LoggerFormatterNotFoundError(formatter);
24
25
  }
25
26
  return formatter.format(entry, context);
26
27
  },
@@ -34,7 +35,7 @@ export function formatLoggerEntry(formatter, entry, context = {}) {
34
35
  return formatter(entry, context);
35
36
  }
36
37
  if (typeof formatter === "string") {
37
- throw new Error(`Cannot format with string identifier "${formatter}" directly. Resolve the formatter first.`);
38
+ throw new LoggerFormatterNotFoundError(formatter);
38
39
  }
39
40
  return formatter.format(entry, context);
40
41
  }
@@ -12,29 +12,42 @@ function removeUndefinedValues(value, seen = new WeakSet()) {
12
12
  return "[Circular]";
13
13
  }
14
14
  seen.add(value);
15
- return value.map((item) => removeUndefinedValues(item, seen));
15
+ try {
16
+ return value.map((item) => removeUndefinedValues(item, seen));
17
+ }
18
+ finally {
19
+ seen.delete(value);
20
+ }
16
21
  }
17
22
  if (value && typeof value === "object" && !(value instanceof Date)) {
18
23
  // Without a cycle guard this walk recursed until the stack blew,
19
24
  // and the resulting RangeError was swallowed by dispatch — losing
20
- // the log line rather than reporting the offending value.
25
+ // the log line rather than reporting the offending value. `seen`
26
+ // tracks the ancestor path only (unmarked on ascent), so an object
27
+ // referenced twice in one payload is kept rather than collapsing
28
+ // to "[Circular]" from its second occurrence on.
21
29
  if (seen.has(value)) {
22
30
  return "[Circular]";
23
31
  }
24
32
  seen.add(value);
25
- const result = {};
26
- for (const [key, item] of Object.entries(value)) {
27
- if (item === undefined) {
28
- continue;
33
+ try {
34
+ const result = {};
35
+ for (const [key, item] of Object.entries(value)) {
36
+ if (item === undefined) {
37
+ continue;
38
+ }
39
+ Object.defineProperty(result, key, {
40
+ value: removeUndefinedValues(item, seen),
41
+ enumerable: true,
42
+ writable: true,
43
+ configurable: true,
44
+ });
29
45
  }
30
- Object.defineProperty(result, key, {
31
- value: removeUndefinedValues(item, seen),
32
- enumerable: true,
33
- writable: true,
34
- configurable: true,
35
- });
46
+ return result;
47
+ }
48
+ finally {
49
+ seen.delete(value);
36
50
  }
37
- return result;
38
51
  }
39
52
  return value;
40
53
  }
@@ -4,6 +4,7 @@
4
4
  * Lower numeric values represent more severe messages.
5
5
  * Higher numeric values represent more verbose messages.
6
6
  */
7
+ import { InvalidLoggerLevelError } from "../loggerErrors/loggerError.base.js";
7
8
  export var LoggerLevel;
8
9
  (function (LoggerLevel) {
9
10
  LoggerLevel[LoggerLevel["FATAL"] = 0] = "FATAL";
@@ -29,7 +30,7 @@ export function loggerLevelToName(level) {
29
30
  case LoggerLevel.TRACE:
30
31
  return "trace";
31
32
  default:
32
- throw new RangeError(`Unknown logger level: ${String(level)}`);
33
+ throw new InvalidLoggerLevelError(level);
33
34
  }
34
35
  }
35
36
  /** Converts a logger level name into its enum value. */
@@ -50,7 +51,7 @@ export function loggerLevelFromName(name) {
50
51
  case "trace":
51
52
  return LoggerLevel.TRACE;
52
53
  default:
53
- throw new RangeError(`Unknown logger level name: "${name}"`);
54
+ throw new InvalidLoggerLevelError(name);
54
55
  }
55
56
  }
56
57
  /** Checks whether a value is a valid LoggerLevel. */
@@ -16,6 +16,16 @@ export declare class LoggerManager {
16
16
  constructor(options?: LoggerOptions);
17
17
  /** Initializes the logger manager. Initialization is idempotent. */
18
18
  initialize(options?: LoggerOptions): Logger;
19
+ /**
20
+ * Adopts an existing logger as the manager's default logger.
21
+ *
22
+ * The logger is REGISTERED with the underlying factory, so it is
23
+ * covered by flush(), close(), getAll() and size. Assigning it to the
24
+ * private default field alone (as createLoggerManagerFromLogger used
25
+ * to) left the registry empty, making flush()/close() no-ops for the
26
+ * only logger the manager owned.
27
+ */
28
+ adopt(logger: Logger): Logger;
19
29
  /** Returns the default application logger. Lazily initializes when necessary. */
20
30
  getLogger(): Logger;
21
31
  /** Returns a named logger. Named loggers are managed by the underlying factory. */
@@ -1,3 +1,4 @@
1
+ import { LoggerDisposedError } from "../loggerErrors/loggerError.base.js";
1
2
  import { createLogger } from "../loggerCore/core/loggerCore.core.js";
2
3
  import { createDefaultLogger } from "../loggerCore/helpers/loggerCore.helper.js";
3
4
  import { createLoggerFactory } from "../loggerFactory/loggerFactory.core.js";
@@ -19,7 +20,7 @@ export class LoggerManager {
19
20
  /** Initializes the logger manager. Initialization is idempotent. */
20
21
  initialize(options = {}) {
21
22
  if (this.closed)
22
- throw new Error("LoggerManager has been closed.");
23
+ throw new LoggerDisposedError("LoggerManager");
23
24
  if (this.initialized && this.defaultLogger)
24
25
  return this.defaultLogger;
25
26
  const logger = this.factory.create(options.name ?? "zudojs", options);
@@ -27,10 +28,27 @@ export class LoggerManager {
27
28
  this.initialized = true;
28
29
  return logger;
29
30
  }
31
+ /**
32
+ * Adopts an existing logger as the manager's default logger.
33
+ *
34
+ * The logger is REGISTERED with the underlying factory, so it is
35
+ * covered by flush(), close(), getAll() and size. Assigning it to the
36
+ * private default field alone (as createLoggerManagerFromLogger used
37
+ * to) left the registry empty, making flush()/close() no-ops for the
38
+ * only logger the manager owned.
39
+ */
40
+ adopt(logger) {
41
+ if (this.closed)
42
+ throw new LoggerDisposedError("LoggerManager");
43
+ this.factory.register(logger);
44
+ this.defaultLogger = logger;
45
+ this.initialized = true;
46
+ return logger;
47
+ }
30
48
  /** Returns the default application logger. Lazily initializes when necessary. */
31
49
  getLogger() {
32
50
  if (this.closed)
33
- throw new Error("LoggerManager has been closed.");
51
+ throw new LoggerDisposedError("LoggerManager");
34
52
  if (!this.defaultLogger)
35
53
  return this.initialize();
36
54
  return this.defaultLogger;
@@ -38,13 +56,13 @@ export class LoggerManager {
38
56
  /** Returns a named logger. Named loggers are managed by the underlying factory. */
39
57
  get(name, options = {}) {
40
58
  if (this.closed)
41
- throw new Error("LoggerManager has been closed.");
59
+ throw new LoggerDisposedError("LoggerManager");
42
60
  return this.factory.getOrCreate(name, options);
43
61
  }
44
62
  /** Creates a new logger even when another logger with the same name already exists. */
45
63
  create(name, options = {}) {
46
64
  if (this.closed)
47
- throw new Error("LoggerManager has been closed.");
65
+ throw new LoggerDisposedError("LoggerManager");
48
66
  return this.factory.create(name, options, true);
49
67
  }
50
68
  /** Checks whether a named logger exists. */
@@ -99,7 +117,7 @@ export class LoggerManager {
99
117
  /** Provides direct access to the underlying factory. */
100
118
  getFactory() {
101
119
  if (this.closed)
102
- throw new Error("LoggerManager has been closed.");
120
+ throw new LoggerDisposedError("LoggerManager");
103
121
  return this.factory;
104
122
  }
105
123
  }
@@ -120,8 +138,7 @@ export function createManagedDefaultLogger(name = "zudojs") {
120
138
  /** Creates a logger manager from an existing logger. */
121
139
  export function createLoggerManagerFromLogger(logger) {
122
140
  const manager = new LoggerManager();
123
- manager["defaultLogger"] = logger;
124
- manager["initialized"] = true;
141
+ manager.adopt(logger);
125
142
  return manager;
126
143
  }
127
144
  /** Returns a logger from a manager or creates a fallback logger when no manager is supplied. */
@@ -4,6 +4,7 @@
4
4
  import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
5
5
  import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
6
6
  import { throwCollectedFailures } from "../../loggerErrors/loggerError.helpers.js";
7
+ import { LoggerTransportClosedError } from "../../loggerErrors/loggerError.base.js";
7
8
  /**
8
9
  * Creates a transport that buffers entries before forwarding
9
10
  * them to another transport.
@@ -14,6 +15,7 @@ export function createBufferedLoggerTransport(transport, options = {}) {
14
15
  const flushInterval = options.flushInterval ?? 0;
15
16
  let timer;
16
17
  let deferredFailure;
18
+ let closed = false;
17
19
  // Each entry is written on its own: one failing write used to abort the
18
20
  // loop after the whole batch had already been spliced out, losing every
19
21
  // entry behind it. Only the entries that actually failed are dropped.
@@ -80,6 +82,12 @@ export function createBufferedLoggerTransport(transport, options = {}) {
80
82
  name: options.name ?? "buffered",
81
83
  enabled: options.enabled ?? true,
82
84
  async write(entry) {
85
+ // A write after close() used to be buffered and then silently
86
+ // dropped: nothing drains the buffer again. Refuse it instead so
87
+ // the caller learns the entry was not accepted.
88
+ if (closed) {
89
+ throw new LoggerTransportClosedError(options.name ?? "buffered");
90
+ }
83
91
  buffer.push(entry);
84
92
  if (buffer.length >= maxSize) {
85
93
  await drain();
@@ -90,6 +98,7 @@ export function createBufferedLoggerTransport(transport, options = {}) {
90
98
  },
91
99
  flush,
92
100
  async close() {
101
+ closed = true;
93
102
  if (timer) {
94
103
  clearTimeout(timer);
95
104
  timer = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/logger",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Structured logging with transports, log levels, and context propagation for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -28,7 +28,7 @@
28
28
  "node": ">=24.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@zudojs/errors": "1.1.0"
31
+ "@zudojs/errors": "1.2.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "7.0.2",