@warlock.js/logger 4.1.15 → 4.2.1

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 (61) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -145
  4. package/cjs/index.cjs +276 -11
  5. package/cjs/index.cjs.map +1 -1
  6. package/esm/channels/console-log.d.mts +1 -1
  7. package/esm/channels/console-log.d.mts.map +1 -1
  8. package/esm/channels/console-log.mjs +4 -1
  9. package/esm/channels/console-log.mjs.map +1 -1
  10. package/esm/channels/file-log.d.mts +13 -3
  11. package/esm/channels/file-log.d.mts.map +1 -1
  12. package/esm/channels/file-log.mjs +15 -1
  13. package/esm/channels/file-log.mjs.map +1 -1
  14. package/esm/channels/index.mjs +1 -0
  15. package/esm/channels/json-file-log.d.mts +1 -1
  16. package/esm/channels/json-file-log.d.mts.map +1 -1
  17. package/esm/channels/json-file-log.mjs +1 -1
  18. package/esm/channels/json-file-log.mjs.map +1 -1
  19. package/esm/channels/sentry-log.d.mts +182 -0
  20. package/esm/channels/sentry-log.d.mts.map +1 -0
  21. package/esm/channels/sentry-log.mjs +202 -0
  22. package/esm/channels/sentry-log.mjs.map +1 -0
  23. package/esm/index.d.mts +2 -1
  24. package/esm/index.mjs +2 -1
  25. package/esm/log-channel.d.mts +9 -1
  26. package/esm/log-channel.d.mts.map +1 -1
  27. package/esm/log-channel.mjs +1 -1
  28. package/esm/log-channel.mjs.map +1 -1
  29. package/esm/logger.d.mts +32 -1
  30. package/esm/logger.d.mts.map +1 -1
  31. package/esm/logger.mjs +44 -2
  32. package/esm/logger.mjs.map +1 -1
  33. package/esm/redact/redact.d.mts +1 -1
  34. package/esm/redact/redact.d.mts.map +1 -1
  35. package/esm/redact/redact.mjs +1 -1
  36. package/esm/redact/redact.mjs.map +1 -1
  37. package/esm/types.d.mts +19 -3
  38. package/esm/types.d.mts.map +1 -1
  39. package/esm/utils/capture-unhandled-errors.d.mts +9 -1
  40. package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
  41. package/esm/utils/capture-unhandled-errors.mjs +10 -2
  42. package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
  43. package/esm/utils/clear-message.d.mts +1 -1
  44. package/esm/utils/clear-message.d.mts.map +1 -1
  45. package/esm/utils/clear-message.mjs +1 -1
  46. package/esm/utils/clear-message.mjs.map +1 -1
  47. package/esm/utils/safe-json-stringify.d.mts +1 -1
  48. package/esm/utils/safe-json-stringify.d.mts.map +1 -1
  49. package/esm/utils/safe-json-stringify.mjs +1 -1
  50. package/esm/utils/safe-json-stringify.mjs.map +1 -1
  51. package/llms-full.txt +1497 -1296
  52. package/llms.txt +20 -19
  53. package/package.json +5 -2
  54. package/skills/capture-unhandled-errors/SKILL.md +103 -103
  55. package/skills/filter-log-entries/SKILL.md +120 -120
  56. package/skills/flush-logs-on-shutdown/SKILL.md +117 -91
  57. package/skills/logger-basics/SKILL.md +88 -85
  58. package/skills/overview/SKILL.md +90 -86
  59. package/skills/pick-log-channel/SKILL.md +155 -139
  60. package/skills/ship-logs-to-sentry/SKILL.md +118 -0
  61. package/skills/write-custom-log-channel/SKILL.md +190 -160
package/cjs/index.cjs CHANGED
@@ -39,7 +39,7 @@ path = __toESM(path, 1);
39
39
  let safe_stable_stringify = require("safe-stable-stringify");
40
40
  let _mongez_reinforcements = require("@mongez/reinforcements");
41
41
 
42
- //#region ../../@warlock.js/logger/src/log-channel.ts
42
+ //#region ../@warlock.js/logger/src/log-channel.ts
43
43
  var LogChannel = class {
44
44
  /**
45
45
  * Constructor
@@ -112,7 +112,7 @@ var LogChannel = class {
112
112
  };
113
113
 
114
114
  //#endregion
115
- //#region ../../@warlock.js/logger/src/channels/console-log.ts
115
+ //#region ../@warlock.js/logger/src/channels/console-log.ts
116
116
  var ConsoleLog = class extends LogChannel {
117
117
  constructor(..._args) {
118
118
  super(..._args);
@@ -142,6 +142,9 @@ var ConsoleLog = class extends LogChannel {
142
142
  case "success":
143
143
  console.log(_mongez_copper.colors.green("✓"), _mongez_copper.colors.yellow(`(${date})`), _mongez_copper.colors.cyan(`[${module}]`), _mongez_copper.colors.magenta(`[${action}]`), _mongez_copper.colors.greenBright(message));
144
144
  break;
145
+ case "fatal":
146
+ console.log(_mongez_copper.colors.bgRedBright(_mongez_copper.colors.bold(" ☠ ")), _mongez_copper.colors.yellow(`(${date})`), _mongez_copper.colors.cyan(`[${module}]`), _mongez_copper.colors.magenta(`[${action}]`), _mongez_copper.colors.redBright.bold(message));
147
+ break;
145
148
  default: console.log("[log]", _mongez_copper.colors.yellow(`(${date})`), _mongez_copper.colors.cyan(`[${module}]`), _mongez_copper.colors.magenta(`[${action}]`), message);
146
149
  }
147
150
  if (typeof message === "object") console.log(message);
@@ -157,7 +160,7 @@ var ConsoleLog = class extends LogChannel {
157
160
  };
158
161
 
159
162
  //#endregion
160
- //#region ../../@warlock.js/logger/src/channels/file-log.ts
163
+ //#region ../@warlock.js/logger/src/channels/file-log.ts
161
164
  var FileLog = class extends LogChannel {
162
165
  constructor(..._args) {
163
166
  super(..._args);
@@ -305,6 +308,20 @@ var FileLog = class extends LogChannel {
305
308
  this.onSave();
306
309
  }
307
310
  /**
311
+ * Asynchronously drain buffered entries to disk.
312
+ *
313
+ * The async analogue of {@link flushSync}: it reuses the same async writer
314
+ * as the background interval, so a caller on a graceful-shutdown path can
315
+ * `await channel.flush()` (or `await log.flush()`) and rely on the buffer
316
+ * being on disk once it resolves. `JSONFileLog` inherits this unchanged —
317
+ * its overridden `writeMessagesToFile` performs the JSON merge.
318
+ */
319
+ async flush() {
320
+ if (this.messages.length === 0 && Object.keys(this.groupedMessages).length === 0) return;
321
+ this.isWriting = false;
322
+ await this.writeMessagesToFile();
323
+ }
324
+ /**
308
325
  * {@inheritdoc}
309
326
  */
310
327
  async log(data) {
@@ -415,7 +432,7 @@ var FileLog = class extends LogChannel {
415
432
  };
416
433
 
417
434
  //#endregion
418
- //#region ../../@warlock.js/logger/src/utils/safe-json-stringify.ts
435
+ //#region ../@warlock.js/logger/src/utils/safe-json-stringify.ts
419
436
  /**
420
437
  * Replacer that surfaces Error data — `name`, `message`, and `stack` are
421
438
  * non-enumerable on `Error`, so neither default JSON serialization nor an
@@ -446,7 +463,7 @@ function safeJsonStringify(value, space) {
446
463
  }
447
464
 
448
465
  //#endregion
449
- //#region ../../@warlock.js/logger/src/channels/json-file-log.ts
466
+ //#region ../@warlock.js/logger/src/channels/json-file-log.ts
450
467
  var JSONFileLog = class extends FileLog {
451
468
  constructor(..._args) {
452
469
  super(..._args);
@@ -574,7 +591,204 @@ var JSONFileLog = class extends FileLog {
574
591
  };
575
592
 
576
593
  //#endregion
577
- //#region ../../@warlock.js/logger/src/redact/redact.ts
594
+ //#region ../@warlock.js/logger/src/channels/sentry-log.ts
595
+ let Sentry;
596
+ let isModuleExists = null;
597
+ let loadingPromise;
598
+ const SENTRY_INSTALL_INSTRUCTIONS = `
599
+ The Sentry log channel requires the @sentry/node package.
600
+ Install it with:
601
+
602
+ npm install @sentry/node
603
+
604
+ Or with your preferred package manager:
605
+
606
+ pnpm add @sentry/node
607
+ yarn add @sentry/node
608
+ `.trim();
609
+ /**
610
+ * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any
611
+ * import failure to "not installed" — the curated install message surfaces at
612
+ * `log()` time, never as a boot-time module-resolution crash.
613
+ */
614
+ function loadSentry() {
615
+ if (isModuleExists !== null) return Promise.resolve();
616
+ if (loadingPromise) return loadingPromise;
617
+ loadingPromise = (async () => {
618
+ try {
619
+ Sentry = await import("@sentry/node");
620
+ isModuleExists = true;
621
+ } catch {
622
+ isModuleExists = false;
623
+ }
624
+ })();
625
+ return loadingPromise;
626
+ }
627
+ /**
628
+ * Forwards log entries to Sentry.
629
+ *
630
+ * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry
631
+ * **events**: an `Error` message via `captureException` (preserving the real
632
+ * stack), any other message via `captureMessage`. Every other level becomes a
633
+ * **breadcrumb** — buffered and attached to the next event, consuming no error
634
+ * quota. `module` / `action` are attached as searchable tags and the entry's
635
+ * `context` as a structured Sentry context.
636
+ *
637
+ * The SDK is an optional peer: pass an existing `client` (reused as-is) or
638
+ * `options` (the channel lazily imports `@sentry/node` and initializes it). On
639
+ * graceful shutdown, `await log.flush()` drains pending events via
640
+ * `Sentry.flush(timeout)`.
641
+ *
642
+ * @example
643
+ * // Existing app — reuse the initialized Sentry client
644
+ * import * as Sentry from "@sentry/node";
645
+ * log.addChannel(new SentryLog({ client: Sentry }));
646
+ *
647
+ * @example
648
+ * // New app — let the channel initialize Sentry
649
+ * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));
650
+ */
651
+ var SentryLog = class extends LogChannel {
652
+ /**
653
+ * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an
654
+ * entry logged on the same tick as construction — e.g. at app boot, before
655
+ * the base schedules `init()` on the next tick via `setTimeout(0)` — is not
656
+ * silently dropped. The `options` (lazy-import) path is inherently async and
657
+ * still resolves in `init()`.
658
+ */
659
+ constructor(configurations) {
660
+ super(configurations);
661
+ this.name = "sentry";
662
+ this.description = "Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)";
663
+ this.defaultConfigurations = {
664
+ eventLevels: [
665
+ "fatal",
666
+ "error",
667
+ "warn"
668
+ ],
669
+ flushTimeout: 2e3
670
+ };
671
+ this.warnedMissing = false;
672
+ const injected = this.config("client");
673
+ if (injected) this.sentry = injected;
674
+ }
675
+ /**
676
+ * Resolve the forwarder: reuse the injected client, otherwise lazily import
677
+ * `@sentry/node` and (only when explicit `options` are supplied and no client
678
+ * exists yet) initialize it. Never throws — the base runs `init()` inside an
679
+ * un-awaited `setTimeout`, so a throw would become an unhandled rejection and
680
+ * `isInitialized` would never flip; a missing SDK is reported from `log()`.
681
+ */
682
+ async init() {
683
+ const injected = this.config("client");
684
+ if (injected) {
685
+ this.sentry = injected;
686
+ return;
687
+ }
688
+ await loadSentry();
689
+ if (!Sentry) return;
690
+ const options = this.config("options");
691
+ if (options && !Sentry.getClient()) Sentry.init(options);
692
+ this.sentry = Sentry;
693
+ }
694
+ /**
695
+ * {@inheritdoc}
696
+ */
697
+ async log(data) {
698
+ if (!this.shouldBeLogged(data)) return;
699
+ if (isModuleExists === null) await loadSentry();
700
+ if (!this.sentry) {
701
+ this.reportMissingSdk();
702
+ return;
703
+ }
704
+ const { module, action, message, type: level, context } = data;
705
+ if (this.isEventLevel(level)) {
706
+ this.captureEvent(this.sentry, {
707
+ module,
708
+ action,
709
+ message,
710
+ level,
711
+ context
712
+ });
713
+ return;
714
+ }
715
+ this.sentry.addBreadcrumb({
716
+ category: module,
717
+ message: this.toText(message),
718
+ level: this.toSentryLevel(level),
719
+ data: context
720
+ });
721
+ }
722
+ /**
723
+ * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable
724
+ * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.
725
+ */
726
+ async flush() {
727
+ if (!this.sentry) return;
728
+ await this.sentry.flush(this.config("flushTimeout"));
729
+ }
730
+ /**
731
+ * Whether the level should be sent as a Sentry event (vs a breadcrumb).
732
+ */
733
+ isEventLevel(level) {
734
+ return Boolean(this.config("eventLevels")?.includes(level));
735
+ }
736
+ /**
737
+ * Send an entry as a Sentry event. An `Error` message goes through
738
+ * `captureException` so Sentry parses the real stack and groups properly;
739
+ * any other message goes through `captureMessage`. `module` / `action` are
740
+ * attached as tags and `context` as a structured context, scoped to this
741
+ * event only via `withScope`.
742
+ */
743
+ captureEvent(sentry, entry) {
744
+ const { module, action, message, level, context } = entry;
745
+ const sentryLevel = this.toSentryLevel(level);
746
+ sentry.withScope((scope) => {
747
+ scope.setLevel(sentryLevel);
748
+ scope.setTags({
749
+ module,
750
+ action
751
+ });
752
+ if (context) scope.setContext("context", context);
753
+ if (message instanceof Error) sentry.captureException(message);
754
+ else sentry.captureMessage(this.toText(message), sentryLevel);
755
+ });
756
+ }
757
+ /**
758
+ * Map a logger level to a Sentry severity. `success` has no Sentry
759
+ * equivalent, so it is reported as informational.
760
+ */
761
+ toSentryLevel(level) {
762
+ switch (level) {
763
+ case "warn": return "warning";
764
+ case "success": return "info";
765
+ default: return level;
766
+ }
767
+ }
768
+ /**
769
+ * Coerce a message into the string Sentry's APIs expect — an `Error`'s
770
+ * `.message`, a string as-is, anything else safely JSON-serialized.
771
+ */
772
+ toText(message) {
773
+ if (typeof message === "string") return message;
774
+ if (message instanceof Error) return message.message;
775
+ return safeJsonStringify(message);
776
+ }
777
+ /**
778
+ * Surface the install instructions exactly once when the SDK is absent. The
779
+ * logger can't log through itself here, so this writes to stderr — matching
780
+ * how the file channels report write failures.
781
+ */
782
+ reportMissingSdk() {
783
+ if (isModuleExists === false && !this.warnedMissing) {
784
+ this.warnedMissing = true;
785
+ console.error(SENTRY_INSTALL_INSTRUCTIONS);
786
+ }
787
+ }
788
+ };
789
+
790
+ //#endregion
791
+ //#region ../@warlock.js/logger/src/redact/redact.ts
578
792
  /**
579
793
  * Deep-clone a value with structural fidelity for log entries — handles plain
580
794
  * objects, arrays, `Date`, `Error`, and primitives. Anything else is copied
@@ -681,7 +895,7 @@ function mergeRedact(base, extra) {
681
895
  }
682
896
 
683
897
  //#endregion
684
- //#region ../../@warlock.js/logger/src/utils/clear-message.ts
898
+ //#region ../@warlock.js/logger/src/utils/clear-message.ts
685
899
  /**
686
900
  * Clear message from any terminal codes
687
901
  */
@@ -691,7 +905,7 @@ function clearMessage(message) {
691
905
  }
692
906
 
693
907
  //#endregion
694
- //#region ../../@warlock.js/logger/src/logger.ts
908
+ //#region ../@warlock.js/logger/src/logger.ts
695
909
  const SIGNAL_EVENTS = new Set([
696
910
  "SIGINT",
697
911
  "SIGTERM",
@@ -711,7 +925,8 @@ const LEVEL_RANK = {
711
925
  info: 1,
712
926
  success: 1,
713
927
  warn: 2,
714
- error: 3
928
+ error: 3,
929
+ fatal: 4
715
930
  };
716
931
  var Logger = class {
717
932
  constructor() {
@@ -863,6 +1078,19 @@ var Logger = class {
863
1078
  return this.log(data);
864
1079
  }
865
1080
  /**
1081
+ * Make fatal log — for unrecoverable failures where the application is going
1082
+ * down (failed bootstrap, lost connection to a required dependency that the
1083
+ * caller has decided not to retry, an `uncaughtException`).
1084
+ *
1085
+ * Identical shape to {@link error}; the level is purely informational —
1086
+ * `fatal` does NOT auto-flush or exit. The caller decides whether to call
1087
+ * `await log.flush()` and `process.exit(...)`.
1088
+ */
1089
+ fatal(dataOrModule, action, message = "", context) {
1090
+ const data = this.normalizeLogData(dataOrModule, action, message, "fatal", context);
1091
+ return this.log(data);
1092
+ }
1093
+ /**
866
1094
  * Log an `error` entry when `condition` is falsy. No-op otherwise — the
867
1095
  * entry is never built and channels are not invoked, so this is genuinely
868
1096
  * free in the happy path. Mirrors the spirit of `console.assert` but routes
@@ -908,6 +1136,34 @@ var Logger = class {
908
1136
  for (const channel of this.channels) if (channel.flushSync) channel.flushSync();
909
1137
  }
910
1138
  /**
1139
+ * Asynchronously drain every channel that implements `flush()`.
1140
+ *
1141
+ * Unlike {@link flushSync}, this awaits each channel's async I/O — the
1142
+ * correct call for a graceful shutdown that can afford to wait
1143
+ * (`await log.flush()` after closing the HTTP server, before
1144
+ * `process.exit`). A channel whose delivery is async (a network transport,
1145
+ * an async disk write) implements `flush()`, not `flushSync()`.
1146
+ *
1147
+ * Channels are isolated: a channel whose flush rejects can neither prevent
1148
+ * the others from draining nor escape as an unhandled rejection. Channels
1149
+ * without `flush()` are skipped.
1150
+ *
1151
+ * @example
1152
+ * async function shutdown() {
1153
+ * await httpServer.close();
1154
+ * await log.flush();
1155
+ * process.exit(0);
1156
+ * }
1157
+ */
1158
+ async flush() {
1159
+ await Promise.allSettled(this.channels.map(async (channel) => {
1160
+ if (!channel.flush) return;
1161
+ try {
1162
+ await channel.flush();
1163
+ } catch {}
1164
+ }));
1165
+ }
1166
+ /**
911
1167
  * Register one process-level handler per event that calls `flushSync()`
912
1168
  * before the process terminates.
913
1169
  *
@@ -967,7 +1223,7 @@ var Logger = class {
967
1223
  const log = new Logger();
968
1224
 
969
1225
  //#endregion
970
- //#region ../../@warlock.js/logger/src/utils/capture-unhandled-errors.ts
1226
+ //#region ../@warlock.js/logger/src/utils/capture-unhandled-errors.ts
971
1227
  /**
972
1228
  * Route Node's process-level failure events through the logger so they land in
973
1229
  * every configured channel with full stack context. Registers one listener for
@@ -975,6 +1231,14 @@ const log = new Logger();
975
1231
  * after channels are configured. Pair with `autoFlushOn: ["beforeExit"]` so the
976
1232
  * final entry survives the process exit that follows an uncaught exception.
977
1233
  *
1234
+ * Levels chosen for semantic honesty:
1235
+ *
1236
+ * - `uncaughtException` → `log.fatal` — by default Node terminates the process,
1237
+ * so the failure is unrecoverable.
1238
+ * - `unhandledRejection` → `log.error` — a rejected promise is a failure, but
1239
+ * not necessarily process-ending (depends on Node's `--unhandled-rejections`
1240
+ * policy and your app's recovery), so it stays at error.
1241
+ *
978
1242
  * @example
979
1243
  * log.configure({ channels: [new ConsoleLog(), new FileLog()] });
980
1244
  * captureAnyUnhandledRejection();
@@ -984,7 +1248,7 @@ function captureAnyUnhandledRejection() {
984
1248
  log.error("app", "unhandledRejection", reason);
985
1249
  });
986
1250
  process.on("uncaughtException", (error) => {
987
- log.error("app", "uncaughtException", error);
1251
+ log.fatal("app", "uncaughtException", error);
988
1252
  });
989
1253
  }
990
1254
 
@@ -994,6 +1258,7 @@ exports.FileLog = FileLog;
994
1258
  exports.JSONFileLog = JSONFileLog;
995
1259
  exports.LogChannel = LogChannel;
996
1260
  exports.Logger = Logger;
1261
+ exports.SentryLog = SentryLog;
997
1262
  exports.applyRedact = applyRedact;
998
1263
  exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
999
1264
  exports.clearMessage = clearMessage;