@warlock.js/logger 5.30.0 → 5.32.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 (69) hide show
  1. package/CHANGELOG.md +32 -5
  2. package/README.md +8 -11
  3. package/cjs/index.cjs +339 -31
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/channels/console-log.d.mts +2 -4
  6. package/esm/channels/console-log.d.mts.map +1 -1
  7. package/esm/channels/console-log.mjs.map +1 -1
  8. package/esm/channels/file-log.d.mts +130 -15
  9. package/esm/channels/file-log.d.mts.map +1 -1
  10. package/esm/channels/file-log.mjs +204 -23
  11. package/esm/channels/file-log.mjs.map +1 -1
  12. package/esm/channels/file-name-pattern.d.mts +15 -0
  13. package/esm/channels/file-name-pattern.d.mts.map +1 -0
  14. package/esm/channels/file-name-pattern.mjs +41 -0
  15. package/esm/channels/file-name-pattern.mjs.map +1 -0
  16. package/esm/channels/index.mjs +2 -2
  17. package/esm/channels/json-file-log.d.mts +1 -3
  18. package/esm/channels/json-file-log.d.mts.map +1 -1
  19. package/esm/channels/json-file-log.mjs +24 -5
  20. package/esm/channels/json-file-log.mjs.map +1 -1
  21. package/esm/channels/sentry-log.d.mts +7 -8
  22. package/esm/channels/sentry-log.d.mts.map +1 -1
  23. package/esm/channels/sentry-log.mjs.map +1 -1
  24. package/esm/index.d.mts +5 -3
  25. package/esm/index.mjs +3 -2
  26. package/esm/log-channel.d.mts +23 -3
  27. package/esm/log-channel.d.mts.map +1 -1
  28. package/esm/log-channel.mjs +22 -0
  29. package/esm/log-channel.mjs.map +1 -1
  30. package/esm/logger.d.mts +8 -5
  31. package/esm/logger.d.mts.map +1 -1
  32. package/esm/logger.mjs +12 -3
  33. package/esm/logger.mjs.map +1 -1
  34. package/esm/redact/default-keys.d.mts +3 -4
  35. package/esm/redact/default-keys.d.mts.map +1 -1
  36. package/esm/redact/escape-log-value.d.mts +25 -0
  37. package/esm/redact/escape-log-value.d.mts.map +1 -0
  38. package/esm/redact/escape-log-value.mjs +37 -0
  39. package/esm/redact/escape-log-value.mjs.map +1 -0
  40. package/esm/redact/index.mjs +2 -1
  41. package/esm/redact/mask-email.d.mts +3 -4
  42. package/esm/redact/mask-email.d.mts.map +1 -1
  43. package/esm/redact/redact.d.mts +3 -5
  44. package/esm/redact/redact.d.mts.map +1 -1
  45. package/esm/redact/redact.mjs.map +1 -1
  46. package/esm/types.d.mts +14 -14
  47. package/esm/types.d.mts.map +1 -1
  48. package/esm/utils/capture-unhandled-errors.d.mts +2 -3
  49. package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
  50. package/esm/utils/clear-message.d.mts +1 -2
  51. package/esm/utils/clear-message.d.mts.map +1 -1
  52. package/esm/utils/index.mjs +1 -1
  53. package/esm/utils/safe-json-stringify.d.mts +1 -2
  54. package/esm/utils/safe-json-stringify.d.mts.map +1 -1
  55. package/llms-full.txt +241 -141
  56. package/llms.txt +2 -2
  57. package/package.json +2 -2
  58. package/skills/capture-unhandled-errors/SKILL.md +9 -10
  59. package/skills/configure-logger/SKILL.md +7 -10
  60. package/skills/filter-log-entries/SKILL.md +2 -2
  61. package/skills/flush-logs-on-shutdown/SKILL.md +32 -13
  62. package/skills/index.md +4 -1
  63. package/skills/logger-basics/SKILL.md +18 -21
  64. package/skills/overview/SKILL.md +17 -6
  65. package/skills/pick-log-channel/SKILL.md +36 -24
  66. package/skills/redact-sensitive-log-fields/SKILL.md +48 -18
  67. package/skills/ship-logs-to-sentry/SKILL.md +18 -18
  68. package/skills/test-logging-code/SKILL.md +4 -2
  69. package/skills/write-custom-log-channel/SKILL.md +50 -17
package/cjs/index.cjs CHANGED
@@ -20,7 +20,7 @@ var __copyProps = (to, from, except, desc) => {
20
20
  }
21
21
  return to;
22
22
  };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
24
24
  value: mod,
25
25
  enumerable: true
26
26
  }) : target, mod));
@@ -56,6 +56,28 @@ var LogChannel = class {
56
56
  }, 0);
57
57
  }
58
58
  /**
59
+ * Route this channel's own failures (those outside a `log()` call) to the
60
+ * given reporter. `Logger` calls this when the channel is added, so the
61
+ * failures reach `log.onChannelError(...)`.
62
+ */
63
+ setErrorReporter(reporter) {
64
+ this.errorReporter = reporter;
65
+ }
66
+ /**
67
+ * Report a failure that happened outside a `log()` call. Never throws:
68
+ * without a reporter it writes one line to stderr.
69
+ */
70
+ reportError(error, entry) {
71
+ try {
72
+ if (this.errorReporter) {
73
+ this.errorReporter(error, entry);
74
+ return;
75
+ }
76
+ const reason = error instanceof Error ? error.message : String(error);
77
+ process.stderr.write(`[@warlock.js/logger] channel "${this.name}" failed: ${reason}\n`);
78
+ } catch {}
79
+ }
80
+ /**
59
81
  * Get config value
60
82
  */
61
83
  config(key) {
@@ -194,8 +216,54 @@ var ConsoleLog = class extends LogChannel {
194
216
  }
195
217
  };
196
218
 
219
+ //#endregion
220
+ //#region ../../packages/logger/src/channels/file-name-pattern.ts
221
+ const TOKEN = /\{(YYYY|MM|DD|HH|mm|name)\}/g;
222
+ const pad = (value) => String(value).padStart(2, "0");
223
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
224
+ /**
225
+ * Render a file name option for the given date.
226
+ */
227
+ function renderFileName(option, date, name) {
228
+ if (typeof option === "function") return option(date);
229
+ return option.replace(TOKEN, (_match, token) => {
230
+ switch (token) {
231
+ case "YYYY": return String(date.getFullYear());
232
+ case "MM": return pad(date.getMonth() + 1);
233
+ case "DD": return pad(date.getDate());
234
+ case "HH": return pad(date.getHours());
235
+ case "mm": return pad(date.getMinutes());
236
+ default: return name;
237
+ }
238
+ });
239
+ }
240
+ /**
241
+ * Regular-expression source matching every name a pattern string can render:
242
+ * literal text is escaped, date tokens become digit runs and `{name}` the
243
+ * escaped channel name. The source is not anchored.
244
+ */
245
+ function fileNamePatternSource(pattern, name) {
246
+ let source = "";
247
+ let lastIndex = 0;
248
+ for (const match of pattern.matchAll(TOKEN)) {
249
+ const index = match.index ?? 0;
250
+ const token = match[1];
251
+ source += escapeRegExp(pattern.slice(lastIndex, index));
252
+ source += token === "YYYY" ? "\\d{4}" : token === "name" ? escapeRegExp(name) : "\\d{2}";
253
+ lastIndex = index + match[0].length;
254
+ }
255
+ return source + escapeRegExp(pattern.slice(lastIndex));
256
+ }
257
+
197
258
  //#endregion
198
259
  //#region ../../packages/logger/src/channels/file-log.ts
260
+ /** Default file name of a `daily` chunk: sortable ISO date. */
261
+ const DAILY_FILE_NAME = "{YYYY}-{MM}-{DD}";
262
+ /** Default file name of an `hourly` chunk: sortable ISO date and hour. */
263
+ const HOURLY_FILE_NAME = "{YYYY}-{MM}-{DD}-{HH}";
264
+ /** Default date part of a size-rotated file's name. */
265
+ const ROTATE_FILE_NAME = "{YYYY}-{MM}-{DD}";
266
+ const DAY_MS = 864e5;
199
267
  var FileLog = class extends LogChannel {
200
268
  constructor(..._args) {
201
269
  super(..._args);
@@ -210,10 +278,8 @@ var FileLog = class extends LogChannel {
210
278
  chunk: "single",
211
279
  maxMessagesToWrite: 100,
212
280
  filter: () => true,
213
- maxFileSize: 10 * 1024 * 1024,
214
- get rotateFileName() {
215
- return (0, dayjs.default)().format("DD-MM-YYYY");
216
- },
281
+ maxFileSize: 10485760,
282
+ rotateFileName: ROTATE_FILE_NAME,
217
283
  dateFormat: {
218
284
  date: "DD-MM-YYYY",
219
285
  time: "HH:mm:ss"
@@ -221,6 +287,15 @@ var FileLog = class extends LogChannel {
221
287
  };
222
288
  this.lastWriteTime = Date.now();
223
289
  this.isWriting = false;
290
+ this.retentionDue = false;
291
+ this.missingMatchReported = false;
292
+ }
293
+ /**
294
+ * {@inheritdoc}
295
+ */
296
+ setConfigurations(configurations) {
297
+ assertRetention(configurations.retention);
298
+ return super.setConfigurations(configurations);
224
299
  }
225
300
  /**
226
301
  * Check file size for file rotation
@@ -228,19 +303,24 @@ var FileLog = class extends LogChannel {
228
303
  async checkAndRotateFile(filePath = this.filePath) {
229
304
  if (!this.config("rotate")) return;
230
305
  try {
231
- if ((await fs.default.promises.stat(filePath)).size >= this.config("maxFileSize")) await this.rotateLogFile();
306
+ if ((await fs.default.promises.stat(filePath)).size >= this.config("maxFileSize")) await this.rotateLogFile(filePath);
232
307
  } catch (error) {
233
308
  if (error.code !== "ENOENT") console.error("Error checking log file:", error);
234
309
  }
235
310
  }
236
311
  /**
237
- * Rotate log file
312
+ * Rotate the given log file (the active one by default) aside as
313
+ * `<file name>-<rotateFileName>-<epoch ms>.<extension>` in its directory.
238
314
  */
239
- async rotateLogFile() {
240
- const fileName = `${this.fileName}-${this.config("rotateFileName")}-${Date.now()}`;
315
+ async rotateLogFile(filePath = this.filePath) {
316
+ const now = /* @__PURE__ */ new Date();
241
317
  const extension = this.extension;
242
- const rotatedFilePath = path.default.join(this.storagePath, `${fileName}.${extension}`);
243
- await fs.default.promises.rename(this.filePath, rotatedFilePath).catch((error) => {
318
+ const baseName = path.default.basename(filePath, `.${extension}`);
319
+ const rotateName = renderFileName(this.config("rotateFileName") ?? "{YYYY}-{MM}-{DD}", now, this.baseName);
320
+ const rotatedFilePath = path.default.join(path.default.dirname(filePath), `${baseName}-${rotateName}-${now.getTime()}.${extension}`);
321
+ await fs.default.promises.rename(filePath, rotatedFilePath).then(() => {
322
+ this.retentionDue = true;
323
+ }, (error) => {
244
324
  console.error("Error rotating file:", error);
245
325
  });
246
326
  }
@@ -286,17 +366,32 @@ var FileLog = class extends LogChannel {
286
366
  return this.config("maxMessagesToWrite");
287
367
  }
288
368
  /**
289
- * Get file name
369
+ * The configured `name` option.
290
370
  */
291
- get fileName() {
371
+ get baseName() {
372
+ return this.config("name") ?? "app";
373
+ }
374
+ /**
375
+ * The `fileName` option, or the chunk's default. `undefined` means the
376
+ * plain `name` of a `single` chunk, used as is (braces included).
377
+ */
378
+ get fileNamePattern() {
379
+ const configured = this.config("fileName");
380
+ if (configured !== void 0) return configured;
292
381
  switch (this.config("chunk")) {
293
- case "single":
294
- default: return this.config("name");
295
- case "daily": return (0, dayjs.default)().format("DD-MM-YYYY");
296
- case "hourly": return (0, dayjs.default)().format("DD-MM-YYYY-HH-00-00-a");
382
+ case "daily": return DAILY_FILE_NAME;
383
+ case "hourly": return HOURLY_FILE_NAME;
384
+ default: return;
297
385
  }
298
386
  }
299
387
  /**
388
+ * Current file name, without extension
389
+ */
390
+ get fileName() {
391
+ const pattern = this.fileNamePattern;
392
+ return pattern === void 0 ? this.baseName : renderFileName(pattern, /* @__PURE__ */ new Date(), this.baseName);
393
+ }
394
+ /**
300
395
  * Get file extension
301
396
  */
302
397
  get extension() {
@@ -321,6 +416,138 @@ var FileLog = class extends LogChannel {
321
416
  const logsDirectory = this.storagePath;
322
417
  await (0, _warlock_js_fs.ensureDirectoryAsync)(logsDirectory);
323
418
  this.initMessageFlush();
419
+ await this.prune();
420
+ }
421
+ /**
422
+ * Remove old log files as configured by `retention`. Runs on boot and
423
+ * whenever a new file is started; call it yourself to prune on demand.
424
+ * Never rejects: failures go to the channel error handler.
425
+ */
426
+ prune() {
427
+ const run = (this.pruning ?? Promise.resolve()).then(() => this.pruneFiles());
428
+ this.pruning = run;
429
+ return run;
430
+ }
431
+ /**
432
+ * Prune once a write started a new file: after a size rotation, or when a
433
+ * dated file name rolled over. Never throws.
434
+ */
435
+ async afterWrite() {
436
+ let fileName;
437
+ try {
438
+ fileName = this.fileName;
439
+ } catch {
440
+ return;
441
+ }
442
+ const rolledOver = this.lastFileName !== void 0 && this.lastFileName !== fileName;
443
+ this.lastFileName = fileName;
444
+ if (!rolledOver && !this.retentionDue) return;
445
+ this.retentionDue = false;
446
+ await this.prune();
447
+ }
448
+ /**
449
+ * Apply `retention` to every directory this channel writes to.
450
+ */
451
+ async pruneFiles() {
452
+ const retention = this.config("retention");
453
+ if (!retention || retention.maxFiles === void 0 && retention.maxAgeDays === void 0) return;
454
+ try {
455
+ const matcher = this.retentionMatcher(retention);
456
+ if (!matcher) return;
457
+ const activeFile = `${this.fileName}.${this.extension}`;
458
+ for (const directory of await this.retentionDirectories()) await this.pruneDirectory(directory, matcher, retention, activeFile);
459
+ } catch (error) {
460
+ this.reportRetentionError(error, this.storagePath);
461
+ }
462
+ }
463
+ /**
464
+ * The expression a file name must match to be pruned, or `undefined` when
465
+ * none can be derived (a function `fileName` without `retention.match`).
466
+ */
467
+ retentionMatcher(retention) {
468
+ if (retention.match) return new RegExp(retention.match.source, retention.match.flags.replace(/[gy]/g, ""));
469
+ const pattern = this.fileNamePattern;
470
+ if (typeof pattern === "function") {
471
+ if (!this.missingMatchReported) {
472
+ this.missingMatchReported = true;
473
+ this.reportRetentionError(/* @__PURE__ */ new Error("FileLog retention needs `retention.match` when `fileName` is a function; no files were pruned"), this.storagePath);
474
+ }
475
+ return;
476
+ }
477
+ const name = this.baseName;
478
+ const fileSource = pattern === void 0 ? escapeRegExp(name) : fileNamePatternSource(pattern, name);
479
+ const rotateName = this.config("rotateFileName") ?? "{YYYY}-{MM}-{DD}";
480
+ const rotateSource = typeof rotateName === "function" ? ".+" : fileNamePatternSource(rotateName, name);
481
+ return new RegExp(`^${fileSource}(?:-${rotateSource}-\\d{13,})?\\.${escapeRegExp(this.extension)}$`);
482
+ }
483
+ /**
484
+ * The log directory, or each group directory when `groupBy` is set.
485
+ */
486
+ async retentionDirectories() {
487
+ let directories = [this.storagePath];
488
+ const depth = this.config("groupBy")?.length ?? 0;
489
+ for (let level = 0; level < depth; level++) {
490
+ const next = [];
491
+ for (const directory of directories) {
492
+ const entries = await fs.default.promises.readdir(directory, { withFileTypes: true }).catch((error) => {
493
+ if (error.code !== "ENOENT") this.reportRetentionError(error, directory);
494
+ return [];
495
+ });
496
+ for (const entry of entries) if (entry.isDirectory()) next.push(path.default.join(directory, entry.name));
497
+ }
498
+ directories = next;
499
+ }
500
+ return directories;
501
+ }
502
+ /**
503
+ * Remove the matching files of one directory that `retention` no longer
504
+ * keeps. Each failure is reported and the rest still run.
505
+ */
506
+ async pruneDirectory(directory, matcher, retention, activeFile) {
507
+ let names;
508
+ try {
509
+ names = await fs.default.promises.readdir(directory);
510
+ } catch (error) {
511
+ if (error.code !== "ENOENT") this.reportRetentionError(error, directory);
512
+ return;
513
+ }
514
+ const candidates = [];
515
+ for (const name of names) {
516
+ if (name === activeFile || !matcher.test(name)) continue;
517
+ const filePath = path.default.join(directory, name);
518
+ try {
519
+ const stats = await fs.default.promises.stat(filePath);
520
+ if (stats.isFile()) candidates.push({
521
+ filePath,
522
+ name,
523
+ modifiedAt: stats.mtimeMs
524
+ });
525
+ } catch (error) {
526
+ if (error.code !== "ENOENT") this.reportRetentionError(error, filePath);
527
+ }
528
+ }
529
+ candidates.sort((a, b) => b.modifiedAt - a.modifiedAt || (a.name < b.name ? 1 : -1));
530
+ const keep = retention.maxFiles === void 0 ? Infinity : retention.maxFiles - (names.includes(activeFile) ? 1 : 0);
531
+ const oldestKept = retention.maxAgeDays === void 0 ? -Infinity : Date.now() - retention.maxAgeDays * DAY_MS;
532
+ for (const [index, candidate] of candidates.entries()) {
533
+ if (index < keep && candidate.modifiedAt >= oldestKept) continue;
534
+ try {
535
+ await fs.default.promises.unlink(candidate.filePath);
536
+ } catch (error) {
537
+ if (error.code !== "ENOENT") this.reportRetentionError(error, candidate.filePath);
538
+ }
539
+ }
540
+ }
541
+ /**
542
+ * Send a retention failure to the channel error handler.
543
+ */
544
+ reportRetentionError(error, target) {
545
+ this.reportError(error, {
546
+ type: "error",
547
+ module: "logger",
548
+ action: "retention",
549
+ message: `Log retention failed for ${target}`
550
+ });
324
551
  }
325
552
  /**
326
553
  * Synchronously flush messages
@@ -412,14 +639,17 @@ var FileLog = class extends LogChannel {
412
639
  if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;
413
640
  this.isWriting = true;
414
641
  if (this.messagedShouldBeGrouped) return await this.writeGroupedMessagesToFile();
415
- await this.checkAndRotateFile();
416
642
  try {
417
- await this.write(this.filePath, this.content);
643
+ const filePath = this.filePath;
644
+ await this.checkAndRotateFile(filePath);
645
+ await this.write(filePath, this.content);
418
646
  this.onSave();
419
647
  } catch (error) {
420
648
  console.error("Failed to write log:", error);
421
649
  this.isWriting = false;
650
+ return;
422
651
  }
652
+ await this.afterWrite();
423
653
  }
424
654
  /**
425
655
  * Write grouped messages to the file
@@ -428,11 +658,11 @@ var FileLog = class extends LogChannel {
428
658
  this.prepareGroupedMessages();
429
659
  for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {
430
660
  const directoryPath = path.default.join(this.storagePath, key);
431
- await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
432
- const filePath = path.default.join(directoryPath, `${this.fileName}.${this.extension}`);
433
- await this.checkAndRotateFile(filePath);
434
- const content = groupMessages.map((message) => message.content).join(os.EOL) + os.EOL;
435
661
  try {
662
+ await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
663
+ const filePath = path.default.join(directoryPath, `${this.fileName}.${this.extension}`);
664
+ await this.checkAndRotateFile(filePath);
665
+ const content = groupMessages.map((message) => message.content).join(os.EOL) + os.EOL;
436
666
  await this.write(filePath, content);
437
667
  } catch (error) {
438
668
  console.error("Failed to write log:", error);
@@ -440,6 +670,7 @@ var FileLog = class extends LogChannel {
440
670
  }
441
671
  this.onSave();
442
672
  this.isWriting = false;
673
+ await this.afterWrite();
443
674
  }
444
675
  /**
445
676
  * Prepare grouped messages
@@ -465,6 +696,16 @@ var FileLog = class extends LogChannel {
465
696
  });
466
697
  }
467
698
  };
699
+ /**
700
+ * Reject a `retention` that could never keep or remove anything sensibly.
701
+ */
702
+ function assertRetention(retention) {
703
+ if (!retention) return;
704
+ const { maxFiles, maxAgeDays, match } = retention;
705
+ if (maxFiles !== void 0 && (!Number.isInteger(maxFiles) || maxFiles < 1)) throw new TypeError("FileLog retention.maxFiles must be a positive integer");
706
+ if (maxAgeDays !== void 0 && (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0)) throw new TypeError("FileLog retention.maxAgeDays must be a positive number");
707
+ if (match !== void 0 && !(match instanceof RegExp)) throw new TypeError("FileLog retention.match must be a RegExp");
708
+ }
468
709
 
469
710
  //#endregion
470
711
  //#region ../../packages/logger/src/utils/safe-json-stringify.ts
@@ -577,10 +818,18 @@ var JSONFileLog = class extends FileLog {
577
818
  if (this.messages.length === 0 || this.isWriting || !this.isInitialized) return;
578
819
  this.isWriting = true;
579
820
  if (this.messagedShouldBeGrouped) return await this.writeGroupedMessagesToFile();
580
- await this.checkAndRotateFile();
821
+ let filePath;
822
+ try {
823
+ filePath = this.filePath;
824
+ } catch (error) {
825
+ console.error("Failed to write log:", error);
826
+ this.isWriting = false;
827
+ return;
828
+ }
829
+ await this.checkAndRotateFile(filePath);
581
830
  let fileContents = { messages: [] };
582
- if (await (0, _warlock_js_fs.fileExistsAsync)(this.filePath)) try {
583
- fileContents = await (0, _warlock_js_fs.getJsonFileAsync)(this.filePath);
831
+ if (await (0, _warlock_js_fs.fileExistsAsync)(filePath)) try {
832
+ fileContents = await (0, _warlock_js_fs.getJsonFileAsync)(filePath);
584
833
  } catch (error) {
585
834
  console.error("Error reading log file, reinitializing:", error);
586
835
  fileContents = { messages: [] };
@@ -588,22 +837,32 @@ var JSONFileLog = class extends FileLog {
588
837
  else fileContents = { messages: [] };
589
838
  fileContents.messages.push(...this.messages);
590
839
  try {
591
- await fs.default.promises.writeFile(this.filePath, safeJsonStringify(fileContents, 2));
840
+ await fs.default.promises.writeFile(filePath, safeJsonStringify(fileContents, 2));
592
841
  this.onSave();
593
842
  } catch (error) {
594
843
  console.error("Failed to write log:", error);
595
844
  this.isWriting = false;
845
+ return;
596
846
  }
847
+ await this.afterWrite();
597
848
  }
598
849
  /**
599
850
  * Write grouped messages to the file
600
851
  */
601
852
  async writeGroupedMessagesToFile() {
853
+ let fileName;
854
+ try {
855
+ fileName = this.fileName;
856
+ } catch (error) {
857
+ console.error("Failed to write log:", error);
858
+ this.isWriting = false;
859
+ return;
860
+ }
602
861
  this.prepareGroupedMessages();
603
862
  for (const [key, groupMessages] of Object.entries(this.groupedMessages)) {
604
863
  const directoryPath = path.default.join(this.storagePath, key);
605
864
  await (0, _warlock_js_fs.ensureDirectoryAsync)(directoryPath);
606
- const filePath = path.default.join(directoryPath, `${this.fileName}.${this.extension}`);
865
+ const filePath = path.default.join(directoryPath, `${fileName}.${this.extension}`);
607
866
  await this.checkAndRotateFile(filePath);
608
867
  let fileContents = { messages: [] };
609
868
  if (await (0, _warlock_js_fs.fileExistsAsync)(filePath)) try {
@@ -622,6 +881,7 @@ var JSONFileLog = class extends FileLog {
622
881
  }
623
882
  }
624
883
  this.onSave();
884
+ await this.afterWrite();
625
885
  }
626
886
  };
627
887
 
@@ -1200,6 +1460,41 @@ function mergeRedact(base, extra) {
1200
1460
  };
1201
1461
  }
1202
1462
 
1463
+ //#endregion
1464
+ //#region ../../packages/logger/src/redact/escape-log-value.ts
1465
+ /**
1466
+ * ANSI escape sequences: CSI (`ESC [ … final`) and the two-character
1467
+ * `ESC x` escapes (`ESC c` resets a terminal). Bounded so matching stays linear on long input.
1468
+ */
1469
+ const ANSI_ESCAPE = /\u001b(?:\[[0-?]{0,32}[ -/]{0,8}[@-~]|[0-~])/g;
1470
+ /** Control characters that are not already handled as `\r`, `\n` or `\t`. */
1471
+ const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g;
1472
+ /**
1473
+ * Make client-controlled text safe to put inside a log line: line breaks and
1474
+ * tabs become the visible escapes `\r`, `\n`, `\t`, other control characters
1475
+ * (including NUL, DEL, C1 controls and the Unicode line/paragraph separators)
1476
+ * become `\uXXXX`, and ANSI terminal escape sequences are removed.
1477
+ *
1478
+ * Without it, a user agent or form field containing `\n2026-01-01 [info] …`
1479
+ * writes a forged entry into a text log, and an ANSI sequence can rewrite what
1480
+ * a terminal shows. Structured channels (JSON) are not affected, but console
1481
+ * and file channels are.
1482
+ *
1483
+ * @example
1484
+ * log.info("auth", "login", `Login from ${escapeLogValue(request.userAgent)}`);
1485
+ */
1486
+ function escapeLogValue(value, options = {}) {
1487
+ let text = typeof value === "string" ? value : String(value);
1488
+ const maxLength = options.maxLength;
1489
+ let truncated = false;
1490
+ if (maxLength !== void 0 && maxLength >= 0 && text.length > maxLength) {
1491
+ text = text.slice(0, maxLength);
1492
+ truncated = true;
1493
+ }
1494
+ const escaped = text.replace(ANSI_ESCAPE, "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(CONTROL, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
1495
+ return truncated ? `${escaped}…[truncated]` : escaped;
1496
+ }
1497
+
1203
1498
  //#endregion
1204
1499
  //#region ../../packages/logger/src/redact/mask-email.ts
1205
1500
  /**
@@ -1280,7 +1575,7 @@ function clearMessage(message) {
1280
1575
 
1281
1576
  //#endregion
1282
1577
  //#region ../../packages/logger/src/logger.ts
1283
- const SIGNAL_EVENTS = new Set([
1578
+ const SIGNAL_EVENTS = /* @__PURE__ */ new Set([
1284
1579
  "SIGINT",
1285
1580
  "SIGTERM",
1286
1581
  "SIGHUP",
@@ -1314,14 +1609,14 @@ var Logger = class {
1314
1609
  * Add a new channel
1315
1610
  */
1316
1611
  addChannel(channel) {
1317
- this.channels.push(channel);
1612
+ this.channels.push(this.attachChannel(channel));
1318
1613
  return this;
1319
1614
  }
1320
1615
  /**
1321
1616
  * Set base configurations
1322
1617
  */
1323
1618
  configure(config) {
1324
- if (config.channels) this.channels = config.channels;
1619
+ if (config.channels) this.setChannels(config.channels);
1325
1620
  if (config.autoFlushOn) this.enableAutoFlush(config.autoFlushOn);
1326
1621
  if (config.minLevel !== void 0) this.setMinLevel(config.minLevel);
1327
1622
  if (config.redact !== void 0) this.setRedact(config.redact);
@@ -1388,9 +1683,18 @@ var Logger = class {
1388
1683
  */
1389
1684
  setChannels(channels) {
1390
1685
  this.channels = channels;
1686
+ for (const channel of channels) this.attachChannel(channel);
1391
1687
  return this;
1392
1688
  }
1393
1689
  /**
1690
+ * Route a channel's own failures (outside `log()`, such as file retention)
1691
+ * to this logger's channel error handler.
1692
+ */
1693
+ attachChannel(channel) {
1694
+ if (typeof channel.setErrorReporter === "function") channel.setErrorReporter((error, entry) => this.reportChannelError(error, channel, entry));
1695
+ return channel;
1696
+ }
1697
+ /**
1394
1698
  * Normalize log data to a single object
1395
1699
  */
1396
1700
  normalizeLogData(dataOrModule, action, message = "", level, context) {
@@ -1779,18 +2083,22 @@ function captureAnyUnhandledRejection(options = {}) {
1779
2083
 
1780
2084
  //#endregion
1781
2085
  exports.ConsoleLog = ConsoleLog;
2086
+ exports.DAILY_FILE_NAME = DAILY_FILE_NAME;
1782
2087
  exports.DEFAULT_FLUSH_TIMEOUT_MS = DEFAULT_FLUSH_TIMEOUT_MS;
1783
2088
  exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
1784
2089
  exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
1785
2090
  exports.FileLog = FileLog;
2091
+ exports.HOURLY_FILE_NAME = HOURLY_FILE_NAME;
1786
2092
  exports.JSONFileLog = JSONFileLog;
1787
2093
  exports.LogChannel = LogChannel;
1788
2094
  exports.Logger = Logger;
1789
2095
  exports.MASK_EMAILS_MAX_LENGTH = MASK_EMAILS_MAX_LENGTH;
2096
+ exports.ROTATE_FILE_NAME = ROTATE_FILE_NAME;
1790
2097
  exports.SentryLog = SentryLog;
1791
2098
  exports.applyRedact = applyRedact;
1792
2099
  exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
1793
2100
  exports.clearMessage = clearMessage;
2101
+ exports.escapeLogValue = escapeLogValue;
1794
2102
  exports.log = log;
1795
2103
  exports.maskEmail = maskEmail;
1796
2104
  exports.maskEmailsInText = maskEmailsInText;