@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
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Structured logging with transports, formatters, log levels, secret
4
4
  redaction, and context propagation for Zudojs applications.
5
5
 
6
+ <!-- zudo-docs:start -->
7
+
8
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-logger](https://zudojs.oyinlola.site/docs/packages-logger) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-logger.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
9
+
10
+ <!-- zudo-docs:end -->
11
+
6
12
  ## Installation
7
13
 
8
14
  ```bash
@@ -39,8 +45,21 @@ Built in: `createConsoleLoggerTransport`, and the composites
39
45
  `createBufferedLoggerTransport`. File and HTTP transports are not
40
46
  included — implement the `LoggerTransport` interface for those.
41
47
 
48
+ The multi and conditional composites forward `flush()` and `close()` to
49
+ the transports they wrap, so a buffered or file transport nested inside
50
+ is drained and released by the logger's own `flush()`/`close()`. The
51
+ multi transport writes to every sink even when one throws; the failures
52
+ are reported afterwards (several as one `AggregateError`). The buffered
53
+ transport writes each entry independently, so a failing write loses only
54
+ that entry, and a failure from a timer-triggered flush is rethrown by the
55
+ next `flush()` or `close()`.
56
+
42
57
  `transportTimeout` (default 10s) bounds every transport write, so a
43
- 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.
44
63
 
45
64
  `throwTransportErrors` (default `false`) rethrows transport and formatter
46
65
  failures instead of dropping them. A synchronous transport throws from the
@@ -48,21 +67,41 @@ log call itself; a failure from an asynchronous transport (or with
48
67
  `asynchronous: true`) cannot, so it is rethrown by the next `flush()` or
49
68
  `close()`, which still flush and close the transports first.
50
69
 
70
+ `flush()` and `close()` isolate each transport: one that throws does not
71
+ stop the rest from being flushed and closed, and `close()` always leaves
72
+ the logger disposed. The failures are rethrown afterwards (several as one
73
+ `AggregateError`).
74
+
51
75
  ## Flushing
52
76
 
53
77
  Dispatch completes synchronously when every transport is synchronous.
54
78
  With an asynchronous transport — or with `asynchronous: true`, which
55
79
  always defers so the caller stays off the transport's critical path —
56
80
  writes are in flight until drained. `flush()` and `close()` drain them,
57
- so nothing is lost at exit.
81
+ including writes started by child loggers (`child()`, `withContext()`),
82
+ so nothing is lost at exit. A child reports itself disposed once its root
83
+ logger is closed.
58
84
 
59
85
  ## Secret redaction
60
86
 
61
87
  Redaction is **on by default**. Metadata and context fields whose NAME
62
- looks like a secret — password, secret, token, api key, private key,
63
- credential, authorization, cookie — are replaced with `"[REDACTED]"`
64
- before the entry reaches any formatter or transport. Nested objects,
65
- arrays and getters are all covered.
88
+ looks like a secret are replaced with `"[REDACTED]"` before the entry
89
+ reaches any formatter or transport. Names are split into words
90
+ (`x-api-key`, `api_key` and `apiKey` all read as `api key`) and matched
91
+ against `DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret,
92
+ token, jwt, bearer, auth, authorization, cookie, session, sid,
93
+ credential, api key, private key, client secret, card number, cvv, ssn,
94
+ pin, otp and more — so `sessionId` and `cardNumber` are redacted while
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
103
+ your own RegExp (the old substring default is still exported as
104
+ `DEFAULT_LOGGER_SECRET_PATTERN`).
66
105
 
67
106
  ```typescript
68
107
  logger.info("login", { user: "alice", password: "hunter2" });
@@ -74,16 +113,22 @@ createLogger({ redact: { enabled: false } }); // opt out
74
113
 
75
114
  ## Log injection
76
115
 
77
- Text-shaped formatters escape control characters in the message, the
78
- logger name, metadata keys and values, context values and source
79
- locations. A newline or ANSI escape inside attacker-supplied text
80
- becomes `\n` / `\u001b` rather than forging an extra log record or
81
- driving the operator's terminal. The JSON formatter relies on
116
+ Text-shaped formatters escape control characters (C0, DEL, C1 and
117
+ U+2028/U+2029) in the message, the level, the logger name, metadata keys
118
+ and values, context values, source locations and error stacks. A newline
119
+ or ANSI escape inside attacker-supplied text becomes `\n` / `\u001b`
120
+ rather than forging an extra log record or driving the operator's
121
+ terminal. In a stack trace only the frame lines break the line: the
122
+ error's name and message are escaped as one line, and every frame line is
123
+ indented so none can start at column 0 and pass for a record. The JSON formatter relies on
82
124
  `JSON.stringify`, which escapes the same characters.
83
125
 
84
126
  Metadata is normalized before serialization, so circular references
85
127
  (`"[Circular]"`), BigInt values and functions never make a formatter
86
- 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.
87
132
 
88
133
  ## Context
89
134
 
@@ -118,6 +163,12 @@ Pass `{ colors: true }` in the formatter context to colourize the level
118
163
  tag of text output. Colour codes are emitted only around the fixed level
119
164
  name, never around user-supplied text.
120
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
+
121
172
  ## Use Cases
122
173
 
123
174
  - Application logging
@@ -35,7 +35,13 @@ export declare class ZudojsLogger implements Logger, ZudojsLoggerContext {
35
35
  private readonly _pending;
36
36
  private readonly _dispatchFailures;
37
37
  private _droppedFailures;
38
- constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage);
38
+ private readonly _root;
39
+ /**
40
+ * @param root - The logger this one was derived from. A child's
41
+ * dispatches are tracked by its root, so the root's flush()/close()
42
+ * drains them, and a child reports itself disposed once its root is.
43
+ */
44
+ constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage, root?: ZudojsLogger);
39
45
  get configuration(): LoggerConfiguration;
40
46
  get contextStorage(): LoggerContextStorage;
41
47
  get name(): string;
@@ -20,7 +20,14 @@ export class ZudojsLogger {
20
20
  _pending = new Set();
21
21
  _dispatchFailures = [];
22
22
  _droppedFailures = 0;
23
- constructor(options = {}, contextStorage) {
23
+ _root;
24
+ /**
25
+ * @param root - The logger this one was derived from. A child's
26
+ * dispatches are tracked by its root, so the root's flush()/close()
27
+ * drains them, and a child reports itself disposed once its root is.
28
+ */
29
+ constructor(options = {}, contextStorage, root) {
30
+ this._root = root;
24
31
  this._configuration = resolveLoggerOptions(options);
25
32
  this._contextStorage = contextStorage ?? createLoggerContextStorage();
26
33
  this._configuration = normalizeConfiguration(this._configuration);
@@ -90,7 +97,7 @@ export class ZudojsLogger {
90
97
  return this._closing;
91
98
  }
92
99
  assertActive() {
93
- assertActiveHelper(this._disposed, this._configuration.name);
100
+ assertActiveHelper(this.isDisposed(), this._configuration.name);
94
101
  }
95
102
  assertMutable() {
96
103
  assertMutableHelper(this._configuration.mutable);
@@ -99,7 +106,7 @@ export class ZudojsLogger {
99
106
  handleInfrastructureErrorHelper(this._configuration.throwTransportErrors, error);
100
107
  }
101
108
  isDisposed() {
102
- return this._disposed;
109
+ return this._disposed || (this._root?.isDisposed() ?? false);
103
110
  }
104
111
  markDisposed() {
105
112
  this._disposed = true;
@@ -108,9 +115,13 @@ export class ZudojsLogger {
108
115
  this._configuration = Object.freeze(config);
109
116
  }
110
117
  createChildLogger(options) {
111
- return new ZudojsLogger(options, this._contextStorage);
118
+ return new ZudojsLogger(options, this._contextStorage, this._root ?? this);
112
119
  }
113
120
  trackDispatch(dispatch) {
121
+ if (this._root) {
122
+ this._root.trackDispatch(dispatch);
123
+ return;
124
+ }
114
125
  // A dispatch rejects only when handleError threw — i.e. when
115
126
  // `throwTransportErrors` is on and an asynchronous transport failed.
116
127
  // Nothing can throw from the log call that started it, so the failure
@@ -134,6 +145,9 @@ export class ZudojsLogger {
134
145
  this._pending.add(tracked);
135
146
  }
136
147
  async drainDispatches() {
148
+ if (this._root) {
149
+ return this._root.drainDispatches();
150
+ }
137
151
  // A dispatch can start further dispatches (a transport that logs),
138
152
  // so drain until the set is genuinely empty.
139
153
  while (this._pending.size > 0) {
@@ -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
@@ -17,10 +17,19 @@ export declare function enableLogger(ctx: ZudojsLoggerContext): void;
17
17
  export declare function disableLogger(ctx: ZudojsLoggerContext): void;
18
18
  /**
19
19
  * Flushes all transport buffers.
20
+ *
21
+ * In-flight dispatches land in the transports before they are flushed.
22
+ * Every transport is flushed even when a dispatch or another transport
23
+ * failed; the failures are rethrown afterwards (several as one
24
+ * AggregateError).
20
25
  */
21
26
  export declare function flushLogger(ctx: ZudojsLoggerContext): Promise<void>;
22
27
  /**
23
28
  * Closes all transports and marks logger as disposed.
29
+ *
30
+ * Closing is terminal: every transport is flushed and closed and the
31
+ * logger is marked disposed even when a dispatch or a transport failed;
32
+ * the failures are rethrown afterwards.
24
33
  */
25
34
  export declare function closeLogger(ctx: ZudojsLoggerContext): Promise<void>;
26
35
  //# sourceMappingURL=loggerCoreMethods.lifecycle.d.ts.map
@@ -5,6 +5,7 @@ import { LoggerLevel } from "../../../loggerLevel/loggerLevel.type.js";
5
5
  import { createLoggerTransport } from "../../../loggerTransport/loggerTransport.core.js";
6
6
  import { isLoggerTransport } from "../../../loggerTransport/loggerTransportGuard.js";
7
7
  import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
8
+ import { throwCollectedFailures } from "../../../loggerErrors/loggerError.helpers.js";
8
9
  /**
9
10
  * Sets the logger level.
10
11
  */
@@ -44,72 +45,73 @@ export function disableLogger(ctx) {
44
45
  });
45
46
  }
46
47
  /**
47
- * Flushes all transport buffers.
48
+ * Flushes (and optionally closes) every configured transport, isolating
49
+ * each one: a failing sink is collected and the walk continues, so one
50
+ * bad transport cannot leave every later transport unflushed/unclosed.
48
51
  */
49
- export async function flushLogger(ctx) {
50
- ctx.assertActive();
51
- // In-flight dispatches must land in the transports before those
52
- // transports are asked to flush, otherwise flush() is a no-op for
53
- // everything logged in the same tick. A dispatch failure (surfaced
54
- // when `throwTransportErrors` is on) is rethrown only after the
55
- // transports have still been flushed.
56
- let failure;
57
- let failed = false;
58
- try {
59
- await ctx.drainDispatches();
60
- }
61
- catch (error) {
62
- failure = error;
63
- failed = true;
64
- }
52
+ async function settleTransports(ctx, close, failures) {
65
53
  for (const transport of ctx.configuration.transports) {
66
- if (!isLoggerTransport(transport)) {
54
+ if (!isLoggerTransport(transport))
67
55
  continue;
68
- }
69
56
  const registered = createLoggerTransport(transport);
70
- if (!registered.enabled) {
57
+ if (!close && !registered.enabled)
71
58
  continue;
59
+ const steps = close
60
+ ? [() => registered.flush?.(), () => registered.close?.()]
61
+ : [() => registered.flush?.()];
62
+ for (const step of steps) {
63
+ try {
64
+ await step();
65
+ }
66
+ catch (error) {
67
+ failures.push(error);
68
+ }
72
69
  }
73
- if (registered.flush) {
74
- await registered.flush();
75
- }
76
70
  }
77
- if (failed)
78
- throw failure;
71
+ }
72
+ /** Drains in-flight dispatches, collecting (not throwing) a failure. */
73
+ async function drainInto(ctx, failures) {
74
+ try {
75
+ await ctx.drainDispatches();
76
+ }
77
+ catch (error) {
78
+ failures.push(error);
79
+ }
80
+ }
81
+ /**
82
+ * Flushes all transport buffers.
83
+ *
84
+ * In-flight dispatches land in the transports before they are flushed.
85
+ * Every transport is flushed even when a dispatch or another transport
86
+ * failed; the failures are rethrown afterwards (several as one
87
+ * AggregateError).
88
+ */
89
+ export async function flushLogger(ctx) {
90
+ ctx.assertActive();
91
+ const failures = [];
92
+ await drainInto(ctx, failures);
93
+ await settleTransports(ctx, false, failures);
94
+ throwCollectedFailures(failures, "Logger flush failed.");
79
95
  }
80
96
  /**
81
97
  * Closes all transports and marks logger as disposed.
98
+ *
99
+ * Closing is terminal: every transport is flushed and closed and the
100
+ * logger is marked disposed even when a dispatch or a transport failed;
101
+ * the failures are rethrown afterwards.
82
102
  */
83
103
  export async function closeLogger(ctx) {
84
104
  if (ctx.isDisposed()) {
85
105
  return;
86
106
  }
87
- // Closing is terminal: transports are flushed and closed and the logger
88
- // is marked disposed even when a dispatch failed; the failure is rethrown
89
- // afterwards.
90
- let failure;
91
- let failed = false;
107
+ const failures = [];
92
108
  try {
93
- await ctx.drainDispatches();
109
+ await drainInto(ctx, failures);
110
+ await settleTransports(ctx, true, failures);
94
111
  }
95
- catch (error) {
96
- failure = error;
97
- failed = true;
98
- }
99
- for (const transport of ctx.configuration.transports) {
100
- if (!isLoggerTransport(transport)) {
101
- continue;
102
- }
103
- const registered = createLoggerTransport(transport);
104
- if (registered.flush) {
105
- await registered.flush();
106
- }
107
- if (registered.close) {
108
- await registered.close();
109
- }
112
+ finally {
113
+ ctx.markDisposed();
110
114
  }
111
- ctx.markDisposed();
112
- if (failed)
113
- throw failure;
115
+ throwCollectedFailures(failures, "Logger close failed.");
114
116
  }
115
117
  //# sourceMappingURL=loggerCoreMethods.lifecycle.js.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./loggerEntryHelpers/index.js";
7
7
  export * from "./loggerEntry.core.js";
8
+ export { DEFAULT_LOGGER_SECRET_FIELDS, createDefaultSecretFieldMatcher, } from "./loggerEntry.secretFields.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./loggerEntryHelpers/index.js";
7
7
  export * from "./loggerEntry.core.js";
8
+ export { DEFAULT_LOGGER_SECRET_FIELDS, createDefaultSecretFieldMatcher, } from "./loggerEntry.secretFields.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
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 declare const DEFAULT_LOGGER_SECRET_FIELDS: readonly string[];
15
+ /**
16
+ * Builds the default secret-field predicate from a word list.
17
+ */
18
+ export declare function createDefaultSecretFieldMatcher(fields?: readonly string[]): (key: string) => boolean;
19
+ //# sourceMappingURL=loggerEntry.secretFields.d.ts.map