@zudojs/logger 1.1.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 (38) hide show
  1. package/README.md +63 -12
  2. package/dist/loggerCore/core/loggerCore.core.d.ts +7 -1
  3. package/dist/loggerCore/core/loggerCore.core.js +18 -4
  4. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.js +62 -17
  5. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.d.ts +6 -1
  6. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +8 -3
  7. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.js +23 -2
  8. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.d.ts +9 -0
  9. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js +51 -49
  10. package/dist/loggerEntry/index.d.ts +1 -0
  11. package/dist/loggerEntry/index.js +1 -0
  12. package/dist/loggerEntry/loggerEntry.secretFields.d.ts +19 -0
  13. package/dist/loggerEntry/loggerEntry.secretFields.js +90 -0
  14. package/dist/loggerEntry/loggerEntryCreate.js +2 -1
  15. package/dist/loggerEntry/loggerEntryHelpers/index.d.ts +1 -1
  16. package/dist/loggerEntry/loggerEntryHelpers/index.js +1 -1
  17. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +29 -12
  18. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +89 -31
  19. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.js +2 -1
  20. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.d.ts +5 -0
  21. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js +43 -12
  22. package/dist/loggerErrors/loggerError.helpers.d.ts +11 -0
  23. package/dist/loggerErrors/loggerError.helpers.js +21 -0
  24. package/dist/loggerFactory/loggerFactory.core.d.ts +7 -0
  25. package/dist/loggerFactory/loggerFactory.core.js +10 -0
  26. package/dist/loggerFormatter/loggerFormatter.core.js +3 -2
  27. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js +1 -1
  28. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.js +26 -13
  29. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.d.ts +6 -0
  30. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js +43 -8
  31. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.js +1 -1
  32. package/dist/loggerLevel/loggerLevel.type.js +3 -2
  33. package/dist/loggerManager/loggerManager.core.d.ts +10 -0
  34. package/dist/loggerManager/loggerManager.core.js +24 -7
  35. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +66 -10
  36. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.d.ts +9 -0
  37. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.js +29 -8
  38. package/package.json +2 -2
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Default secret-field matching for logger redaction.
3
+ *
4
+ * Field names are split into words (`x-api-key`, `api_key` and `apiKey`
5
+ * all become `api key`) and matched on whole-word runs, the same scheme
6
+ * @zudojs/observability's `isSensitiveField` uses. A raw substring test
7
+ * redacted `passenger`, `compass` and `bypassCache` (they contain `pass`)
8
+ * while missing `auth`, `sessionId`, `sid`, `jwt`, `ssn`, `cardNumber`,
9
+ * `cvv` and `otp` entirely.
10
+ */
11
+ /**
12
+ * Secret field names matched on whole words by default.
13
+ */
14
+ export const DEFAULT_LOGGER_SECRET_FIELDS = Object.freeze([
15
+ "password",
16
+ "passwd",
17
+ "passphrase",
18
+ "pwd",
19
+ "secret",
20
+ "token",
21
+ "jwt",
22
+ "bearer",
23
+ "authorization",
24
+ "auth",
25
+ "cookie",
26
+ "session",
27
+ "sid",
28
+ "credential",
29
+ "credentials",
30
+ "api_key",
31
+ "private_key",
32
+ "client_secret",
33
+ "credit_card",
34
+ "card_number",
35
+ "cvv",
36
+ "cvc",
37
+ "ssn",
38
+ "social_security",
39
+ "pin",
40
+ "otp",
41
+ ]);
42
+ /**
43
+ * Unambiguous secret words that are also matched as a substring of a
44
+ * lowercase run-together name (`userpassword`, `accesstoken`), which the
45
+ * word split alone cannot see.
46
+ */
47
+ const SUBSTRING_SECRET_WORDS = Object.freeze([
48
+ "password",
49
+ "passwd",
50
+ "secret",
51
+ "token",
52
+ "apikey",
53
+ "privatekey",
54
+ "credential",
55
+ "authorization",
56
+ "cookie",
57
+ ]);
58
+ /** Splits a field name into lowercase words. */
59
+ function splitWords(key) {
60
+ return key
61
+ .replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
62
+ .replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2")
63
+ .toLowerCase()
64
+ .split(/[^a-z0-9]+/u)
65
+ .filter(Boolean);
66
+ }
67
+ /** Normalizes a field-list entry the way word runs are joined. */
68
+ function normalizeField(field) {
69
+ return field.toLowerCase().replace(/[^a-z0-9]/gu, "");
70
+ }
71
+ /**
72
+ * Builds the default secret-field predicate from a word list.
73
+ */
74
+ export function createDefaultSecretFieldMatcher(fields = DEFAULT_LOGGER_SECRET_FIELDS) {
75
+ const targets = new Set(fields.map(normalizeField).filter(Boolean));
76
+ return (key) => {
77
+ const words = splitWords(key);
78
+ for (let start = 0; start < words.length; start += 1) {
79
+ let joined = "";
80
+ for (let end = start; end < words.length; end += 1) {
81
+ joined += words[end] ?? "";
82
+ if (targets.has(joined))
83
+ return true;
84
+ }
85
+ }
86
+ const compact = key.toLowerCase().replace(/[^a-z0-9]/gu, "");
87
+ return SUBSTRING_SECRET_WORDS.some((word) => compact.includes(word));
88
+ };
89
+ }
90
+ //# sourceMappingURL=loggerEntry.secretFields.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
@@ -13,10 +13,21 @@
13
13
  */
14
14
  export declare const LOGGER_REDACTION_TOKEN = "[REDACTED]";
15
15
  /**
16
- * Field names treated as secrets by default.
16
+ * Replacement token written in place of a property whose getter threw.
17
17
  *
18
- * Matching is case-insensitive and substring-based so `dbPassword`,
19
- * `X-Api-Key` and `refresh_token` are all covered.
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]";
23
+ /**
24
+ * Legacy substring pattern for secret field names.
25
+ *
26
+ * @deprecated No longer the default: it redacted `passenger`/`compass`
27
+ * and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
28
+ * The default is now the word-based matcher over
29
+ * `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
30
+ * back into the old behaviour.
20
31
  */
21
32
  export declare const DEFAULT_LOGGER_SECRET_PATTERN: RegExp;
22
33
  /**
@@ -32,8 +43,9 @@ export interface LoggerRedactionOptions {
32
43
  */
33
44
  readonly keys?: readonly string[];
34
45
  /**
35
- * Field-name pattern. Defaults to DEFAULT_LOGGER_SECRET_PATTERN.
36
- * Pass a pattern that never matches to rely on `keys` alone.
46
+ * Field-name pattern. When omitted, the word-based default matcher over
47
+ * `DEFAULT_LOGGER_SECRET_FIELDS` is used. Pass a pattern that never
48
+ * matches to rely on `keys` alone.
37
49
  */
38
50
  readonly pattern?: RegExp;
39
51
  /**
@@ -45,8 +57,8 @@ export interface LoggerRedactionOptions {
45
57
  * Escapes control characters that could forge log records.
46
58
  *
47
59
  * CR, LF, TAB and the ANSI escape byte become printable escapes; every
48
- * other C0 control character and DEL becomes `\xNN`. Ordinary text,
49
- * including every non-ASCII character, is returned unchanged.
60
+ * other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
61
+ * U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
50
62
  */
51
63
  export declare function escapeLogText(value: string): string;
52
64
  /**
@@ -60,10 +72,15 @@ export declare function createSecretMatcher(options?: LoggerRedactionOptions): (
60
72
  /**
61
73
  * Recursively replaces secret-named fields with a redaction token.
62
74
  *
63
- * Nesting, arrays and getters are all covered: the walk descends into
64
- * every enumerable own property, and a getter is read here — once,
65
- * before the value can reach a transport. Cycles resolve to
66
- * "[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.
67
84
  */
68
- 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;
69
86
  //# sourceMappingURL=loggerEntryHelpers.sanitize.d.ts.map
@@ -8,28 +8,41 @@
8
8
  * terminal. Every string that reaches a text-shaped formatter is
9
9
  * therefore escaped here first.
10
10
  */
11
+ import { createDefaultSecretFieldMatcher } from "../loggerEntry.secretFields.js";
11
12
  /**
12
- * Matches C0 control characters plus DEL — everything that can forge a
13
- * record boundary or drive a terminal.
13
+ * Matches C0 controls, DEL, C1 controls (NEL, CSI) and the Unicode
14
+ * line/paragraph separators — everything that can forge a record
15
+ * boundary or drive a terminal.
14
16
  */
15
- const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
17
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
16
18
  /**
17
19
  * Replacement token written in place of a redacted value.
18
20
  */
19
21
  export const LOGGER_REDACTION_TOKEN = "[REDACTED]";
20
22
  /**
21
- * Field names treated as secrets by default.
23
+ * Replacement token written in place of a property whose getter threw.
22
24
  *
23
- * Matching is case-insensitive and substring-based so `dbPassword`,
24
- * `X-Api-Key` and `refresh_token` are all covered.
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]";
30
+ /**
31
+ * Legacy substring pattern for secret field names.
32
+ *
33
+ * @deprecated No longer the default: it redacted `passenger`/`compass`
34
+ * and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
35
+ * The default is now the word-based matcher over
36
+ * `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
37
+ * back into the old behaviour.
25
38
  */
26
39
  export const DEFAULT_LOGGER_SECRET_PATTERN = /(pass(word|wd)?|secret|token|api[-_.]?key|private[-_.]?key|credential|authorization|cookie)/i;
27
40
  /**
28
41
  * Escapes control characters that could forge log records.
29
42
  *
30
43
  * CR, LF, TAB and the ANSI escape byte become printable escapes; every
31
- * other C0 control character and DEL becomes `\xNN`. Ordinary text,
32
- * including every non-ASCII character, is returned unchanged.
44
+ * other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
45
+ * U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
33
46
  */
34
47
  export function escapeLogText(value) {
35
48
  return value.replace(CONTROL_CHARACTERS, (character) => {
@@ -42,6 +55,10 @@ export function escapeLogText(value) {
42
55
  return "\\t";
43
56
  case "\u001b":
44
57
  return "\\u001b";
58
+ case "\u2028":
59
+ return "\\u2028";
60
+ case "\u2029":
61
+ return "\\u2029";
45
62
  default: {
46
63
  const code = character.charCodeAt(0);
47
64
  return `\\x${code.toString(16).padStart(2, "0")}`;
@@ -62,25 +79,46 @@ export function createSecretMatcher(options = {}) {
62
79
  if (options.enabled === false) {
63
80
  return () => false;
64
81
  }
82
+ const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
83
+ const configured = options.pattern;
84
+ if (configured === undefined) {
85
+ const isDefaultSecret = createDefaultSecretFieldMatcher();
86
+ return (key) => exact.has(key.toLowerCase()) || isDefaultSecret(key);
87
+ }
65
88
  // Copy the pattern without `g`/`y`: those flags make `test()` advance
66
89
  // `lastIndex`, so a shared pattern would match a secret-named field on
67
90
  // one entry and let it through unredacted on the next.
68
- const configured = options.pattern ?? DEFAULT_LOGGER_SECRET_PATTERN;
69
91
  const pattern = configured.global || configured.sticky
70
92
  ? new RegExp(configured.source, configured.flags.replace(/[gy]/gu, ""))
71
93
  : configured;
72
- const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
73
94
  return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
74
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
+ }
75
108
  /**
76
109
  * Recursively replaces secret-named fields with a redaction token.
77
110
  *
78
- * Nesting, arrays and getters are all covered: the walk descends into
79
- * every enumerable own property, and a getter is read here — once,
80
- * before the value can reach a transport. Cycles resolve to
81
- * "[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.
82
120
  */
83
- 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) {
84
122
  if (value === null || typeof value !== "object") {
85
123
  return value;
86
124
  }
@@ -91,23 +129,43 @@ export function redactLogValue(value, isSecret, replacement = LOGGER_REDACTION_T
91
129
  return "[Circular]";
92
130
  }
93
131
  seen.add(value);
94
- if (Array.isArray(value)) {
95
- 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;
96
166
  }
97
- const result = {};
98
- for (const [key, item] of Object.entries(value)) {
99
- // defineProperty, never assignment: a "__proto__" key coming from
100
- // JSON.parse of untrusted input would otherwise reach the inherited
101
- // setter and replace this object's prototype.
102
- Object.defineProperty(result, key, {
103
- value: isSecret(key)
104
- ? replacement
105
- : redactLogValue(item, isSecret, replacement, seen),
106
- enumerable: true,
107
- writable: true,
108
- configurable: true,
109
- });
167
+ finally {
168
+ seen.delete(value);
110
169
  }
111
- return result;
112
170
  }
113
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
@@ -12,4 +12,15 @@ export declare function getLoggerErrorCause(error: unknown): unknown;
12
12
  export declare function createLoggerTransportError(transportName: string, error: unknown): LoggerTransportError;
13
13
  /** Creates a formatter error while preserving the original failure. */
14
14
  export declare function createLoggerFormatterError(formatterName: string, error: unknown): LoggerFormatterError;
15
+ /**
16
+ * Rethrows collected failures: the single failure itself, or an
17
+ * AggregateError when more than one step failed. Does nothing when the
18
+ * list is empty.
19
+ */
20
+ export declare function throwCollectedFailures(failures: readonly unknown[], message: string): void;
21
+ /**
22
+ * Runs every step even when earlier ones fail, then rethrows the
23
+ * collected failures via {@link throwCollectedFailures}.
24
+ */
25
+ export declare function settleAllOrThrow(steps: readonly (() => unknown)[], message: string): Promise<void>;
15
26
  //# sourceMappingURL=loggerError.helpers.d.ts.map
@@ -38,4 +38,25 @@ export function createLoggerFormatterError(formatterName, error) {
38
38
  const message = error instanceof Error ? error.message : String(error);
39
39
  return new LoggerFormatterError(`Logger formatter "${formatterName}" failed: ${message}`, { formatterName, cause });
40
40
  }
41
+ /**
42
+ * Rethrows collected failures: the single failure itself, or an
43
+ * AggregateError when more than one step failed. Does nothing when the
44
+ * list is empty.
45
+ */
46
+ export function throwCollectedFailures(failures, message) {
47
+ if (failures.length === 0)
48
+ return;
49
+ if (failures.length === 1)
50
+ throw failures[0];
51
+ throw new AggregateError(failures, message);
52
+ }
53
+ /**
54
+ * Runs every step even when earlier ones fail, then rethrows the
55
+ * collected failures via {@link throwCollectedFailures}.
56
+ */
57
+ export async function settleAllOrThrow(steps, message) {
58
+ const results = await Promise.allSettled(steps.map(async (step) => step()));
59
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
60
+ throwCollectedFailures(failures, message);
61
+ }
41
62
  //# sourceMappingURL=loggerError.helpers.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
  }
@@ -11,7 +11,7 @@ import { createTextLoggerFormatter } from "./loggerFormatterFormatters.text.js";
11
11
  */
12
12
  export function createCompactLoggerFormatter(options = {}) {
13
13
  return createLoggerFormatter((entry) => {
14
- const level = entry.levelName.toUpperCase();
14
+ const level = escapeLogText(entry.levelName.toUpperCase());
15
15
  const logger = entry.logger ? ` ${escapeLogText(entry.logger)}:` : "";
16
16
  return `${level}${logger} ${escapeLogText(entry.message)}`;
17
17
  }, {
@@ -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
  }