@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
@@ -11,6 +11,12 @@ export declare function formatMetadata(metadata: Record<string, unknown>, separa
11
11
  export declare function formatValue(value: unknown): string;
12
12
  /**
13
13
  * Formats an Error.
14
+ *
15
+ * With a stack trace the output is intentionally multi-line, but only
16
+ * the frame lines break the line: the header (which carries the
17
+ * attacker-influenceable message) is escaped as a single line, and every
18
+ * frame line is escaped and indented so none of them can start at
19
+ * column 0 and parse as a record of its own.
14
20
  */
15
21
  export declare function formatError(error: Error, includeStackTrace: boolean): string;
16
22
  //# sourceMappingURL=loggerFormatterFormatters.metadata.d.ts.map
@@ -23,28 +23,63 @@ export function formatValue(value) {
23
23
  }
24
24
  if (typeof value === "string") {
25
25
  if (/\s/.test(value)) {
26
- return JSON.stringify(value);
26
+ // JSON escapes C0 controls but not DEL, C1 or U+2028/U+2029.
27
+ return escapeLogText(JSON.stringify(value));
27
28
  }
28
29
  // A value with no whitespace can still carry ANSI escapes or other
29
30
  // C0 controls, which used to reach the sink verbatim.
30
31
  return escapeLogText(value);
31
32
  }
32
33
  if (typeof value === "object") {
33
- return JSON.stringify(serializeLoggerValue(value));
34
+ return escapeLogText(JSON.stringify(serializeLoggerValue(value)));
34
35
  }
35
36
  return escapeLogText(String(value));
36
37
  }
38
+ /**
39
+ * Splits an error's stack into its header (name and message) and its
40
+ * frame lines.
41
+ *
42
+ * The header is derived from the error itself rather than from the first
43
+ * stack line, because the message can contain newlines of its own and
44
+ * would otherwise spill across several "lines" of the stack.
45
+ */
46
+ function splitStack(error, stack) {
47
+ const message = String(error.message ?? "");
48
+ const header = message ? `${error.name}: ${message}` : String(error.name);
49
+ if (stack.startsWith(header)) {
50
+ const rest = stack.slice(header.length).replace(/^\r?\n/u, "");
51
+ return { header, frames: rest ? rest.split("\n") : [] };
52
+ }
53
+ const lines = stack.split("\n");
54
+ const firstFrame = lines.findIndex((line) => /^\s+at\s/u.test(line));
55
+ if (firstFrame === -1) {
56
+ return { header: stack, frames: [] };
57
+ }
58
+ return {
59
+ header: lines.slice(0, firstFrame).join("\n"),
60
+ frames: lines.slice(firstFrame),
61
+ };
62
+ }
37
63
  /**
38
64
  * Formats an Error.
65
+ *
66
+ * With a stack trace the output is intentionally multi-line, but only
67
+ * the frame lines break the line: the header (which carries the
68
+ * attacker-influenceable message) is escaped as a single line, and every
69
+ * frame line is escaped and indented so none of them can start at
70
+ * column 0 and parse as a record of its own.
39
71
  */
40
72
  export function formatError(error, includeStackTrace) {
41
- if (includeStackTrace && error.stack) {
42
- // A stack trace is intentionally multi-line, but everything in it
43
- // that came from user input (the message) must not be able to
44
- // introduce a further record boundary of its own.
45
- return `\n${error.stack.split("\n").map(escapeLogText).join("\n")}`;
73
+ if (includeStackTrace && typeof error.stack === "string" && error.stack) {
74
+ const { header, frames } = splitStack(error, error.stack);
75
+ const lines = [escapeLogText(header)];
76
+ for (const frame of frames) {
77
+ const escaped = escapeLogText(frame);
78
+ lines.push(/^\s/u.test(escaped) ? escaped : ` ${escaped}`);
79
+ }
80
+ return `\n${lines.join("\n")}`;
46
81
  }
47
82
  const serialized = serializeLoggerError(error);
48
- return `error=${JSON.stringify(serialized)}`;
83
+ return `error=${escapeLogText(JSON.stringify(serialized))}`;
49
84
  }
50
85
  //# sourceMappingURL=loggerFormatterFormatters.metadata.js.map
@@ -43,7 +43,7 @@ export function createTextLoggerFormatter(options = {}) {
43
43
  // output. Colour codes are emitted only on explicit opt-in, and
44
44
  // only around the fixed level name — never around user text,
45
45
  // which stays escaped.
46
- const levelTag = `[${entry.levelName.toUpperCase()}]`;
46
+ const levelTag = `[${escapeLogText(entry.levelName.toUpperCase())}]`;
47
47
  parts.push(context.colors ? colorizeLevel(entry.levelName, levelTag) : levelTag);
48
48
  if (includeLogger && entry.logger) {
49
49
  parts.push(`[${escapeLogText(entry.logger)}]`);
@@ -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. */
@@ -2,7 +2,9 @@
2
2
  * Buffered logger transport.
3
3
  */
4
4
  import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
5
- import { isLoggerTransportObject } from "../loggerTransportGuard.js";
5
+ import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
6
+ import { throwCollectedFailures } from "../../loggerErrors/loggerError.helpers.js";
7
+ import { LoggerTransportClosedError } from "../../loggerErrors/loggerError.base.js";
6
8
  /**
7
9
  * Creates a transport that buffers entries before forwarding
8
10
  * them to another transport.
@@ -12,14 +14,51 @@ export function createBufferedLoggerTransport(transport, options = {}) {
12
14
  const maxSize = options.maxSize ?? 100;
13
15
  const flushInterval = options.flushInterval ?? 0;
14
16
  let timer;
15
- const flush = async () => {
17
+ let deferredFailure;
18
+ let closed = false;
19
+ // Each entry is written on its own: one failing write used to abort the
20
+ // loop after the whole batch had already been spliced out, losing every
21
+ // entry behind it. Only the entries that actually failed are dropped.
22
+ const drain = async () => {
16
23
  if (buffer.length === 0) {
17
24
  return;
18
25
  }
19
26
  const entries = buffer.splice(0, buffer.length);
27
+ const failures = [];
20
28
  for (const entry of entries) {
21
- await writeLoggerTransport(transport, entry);
29
+ try {
30
+ await writeLoggerTransport(transport, entry);
31
+ }
32
+ catch (error) {
33
+ failures.push(error);
34
+ }
35
+ }
36
+ throwCollectedFailures(failures, `${failures.length} buffered log entries failed to write.`);
37
+ };
38
+ // A failure from a timer-triggered drain has no caller to reach, so it
39
+ // is kept and rethrown by the next explicit flush()/close().
40
+ const takeDeferredFailure = () => {
41
+ if (!deferredFailure)
42
+ return [];
43
+ const { error } = deferredFailure;
44
+ deferredFailure = undefined;
45
+ return [error];
46
+ };
47
+ const flush = async () => {
48
+ const failures = takeDeferredFailure();
49
+ try {
50
+ await drain();
51
+ }
52
+ catch (error) {
53
+ failures.push(error);
54
+ }
55
+ try {
56
+ await flushLoggerTransport(transport);
22
57
  }
58
+ catch (error) {
59
+ failures.push(error);
60
+ }
61
+ throwCollectedFailures(failures, "Buffered logger transport flush failed.");
23
62
  };
24
63
  const scheduleFlush = () => {
25
64
  if (flushInterval <= 0 || timer) {
@@ -28,10 +67,10 @@ export function createBufferedLoggerTransport(transport, options = {}) {
28
67
  timer = setTimeout(async () => {
29
68
  timer = undefined;
30
69
  try {
31
- await flush();
70
+ await drain();
32
71
  }
33
- catch {
34
- /* deliberate no-op */
72
+ catch (error) {
73
+ deferredFailure ??= { error };
35
74
  }
36
75
  }, flushInterval);
37
76
  // A pending flush is housekeeping, not work: left referenced it kept a
@@ -43,9 +82,15 @@ export function createBufferedLoggerTransport(transport, options = {}) {
43
82
  name: options.name ?? "buffered",
44
83
  enabled: options.enabled ?? true,
45
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
+ }
46
91
  buffer.push(entry);
47
92
  if (buffer.length >= maxSize) {
48
- await flush();
93
+ await drain();
49
94
  }
50
95
  else {
51
96
  scheduleFlush();
@@ -53,14 +98,25 @@ export function createBufferedLoggerTransport(transport, options = {}) {
53
98
  },
54
99
  flush,
55
100
  async close() {
101
+ closed = true;
56
102
  if (timer) {
57
103
  clearTimeout(timer);
58
104
  timer = undefined;
59
105
  }
60
- await flush();
61
- if (isLoggerTransportObject(transport) && transport.close) {
62
- await transport.close();
106
+ const failures = [];
107
+ try {
108
+ await flush();
109
+ }
110
+ catch (error) {
111
+ failures.push(error);
112
+ }
113
+ try {
114
+ await closeLoggerTransport(transport);
115
+ }
116
+ catch (error) {
117
+ failures.push(error);
63
118
  }
119
+ throwCollectedFailures(failures, "Buffered logger transport close failed.");
64
120
  },
65
121
  };
66
122
  return createLoggerTransport(buffered, options);
@@ -7,11 +7,20 @@ import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffer
7
7
  /**
8
8
  * Creates a transport that forwards entries to another
9
9
  * transport only when a predicate passes.
10
+ *
11
+ * `flush()` and `close()` are forwarded to the inner transport, so a
12
+ * buffered or file transport nested inside is drained and released by
13
+ * the logger's own `flush()`/`close()`.
10
14
  */
11
15
  export declare function createConditionalLoggerTransport(transport: LoggerTransportLike, predicate: (entry: LoggerEntry) => boolean | Promise<boolean>, options?: LoggerTransportOptions): RegisteredLoggerTransport;
12
16
  /**
13
17
  * Creates a transport that forwards entries to multiple
14
18
  * transports.
19
+ *
20
+ * Every sink receives every entry even when another sink throws: writes
21
+ * are settled independently and the failures are rethrown afterwards
22
+ * (one failure as itself, several as an AggregateError). `flush()` and
23
+ * `close()` fan out to every inner transport the same way.
15
24
  */
16
25
  export declare function createMultiLoggerTransport(transports: readonly LoggerTransportLike[], options?: LoggerTransportOptions): RegisteredLoggerTransport;
17
26
  export { createBufferedLoggerTransport };
@@ -2,27 +2,48 @@
2
2
  * Composite logger transports.
3
3
  */
4
4
  import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
5
+ import { createLoggerTransportId } from "../loggerTransportGuard.js";
6
+ import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
7
+ import { settleAllOrThrow } from "../../loggerErrors/loggerError.helpers.js";
5
8
  import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffered.js";
6
9
  /**
7
10
  * Creates a transport that forwards entries to another
8
11
  * transport only when a predicate passes.
12
+ *
13
+ * `flush()` and `close()` are forwarded to the inner transport, so a
14
+ * buffered or file transport nested inside is drained and released by
15
+ * the logger's own `flush()`/`close()`.
9
16
  */
10
17
  export function createConditionalLoggerTransport(transport, predicate, options = {}) {
11
- return createLoggerTransport(async (entry, context) => {
12
- if (await predicate(entry)) {
13
- await writeLoggerTransport(transport, entry, context);
14
- }
18
+ return createLoggerTransport({
19
+ name: options.name ?? createLoggerTransportId(),
20
+ enabled: options.enabled ?? true,
21
+ async write(entry, context) {
22
+ if (await predicate(entry)) {
23
+ await writeLoggerTransport(transport, entry, context);
24
+ }
25
+ },
26
+ flush: () => flushLoggerTransport(transport),
27
+ close: () => closeLoggerTransport(transport),
15
28
  }, options);
16
29
  }
17
30
  /**
18
31
  * Creates a transport that forwards entries to multiple
19
32
  * transports.
33
+ *
34
+ * Every sink receives every entry even when another sink throws: writes
35
+ * are settled independently and the failures are rethrown afterwards
36
+ * (one failure as itself, several as an AggregateError). `flush()` and
37
+ * `close()` fan out to every inner transport the same way.
20
38
  */
21
39
  export function createMultiLoggerTransport(transports, options = {}) {
22
- return createLoggerTransport(async (entry, context) => {
23
- for (const transport of transports) {
24
- await writeLoggerTransport(transport, entry, context);
25
- }
40
+ const sinks = [...transports];
41
+ return createLoggerTransport({
42
+ name: options.name ?? createLoggerTransportId(),
43
+ enabled: options.enabled ?? true,
44
+ write: (entry, context) => settleAllOrThrow(sinks.map((sink) => () => writeLoggerTransport(sink, entry, context)), "Multiple logger transports failed to write an entry."),
45
+ flush: () => settleAllOrThrow(sinks.map((sink) => () => flushLoggerTransport(sink)), "Multiple logger transports failed to flush."),
46
+ close: () => settleAllOrThrow(sinks.map((sink) => () => closeLoggerTransport(sink)), "Multiple logger transports failed to close."),
26
47
  }, options);
27
48
  }
28
49
  export { createBufferedLoggerTransport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/logger",
3
- "version": "1.1.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.0.1"
31
+ "@zudojs/errors": "1.2.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "7.0.2",