@warlock.js/logger 5.24.0 → 5.25.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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "dependencies": {
5
5
  "@mongez/copper": "^2.1.3",
6
6
  "@mongez/reinforcements": "^4.0.2",
7
- "@warlock.js/fs": "5.24.0",
7
+ "@warlock.js/fs": "5.25.0",
8
8
  "dayjs": "^1.11.9",
9
9
  "safe-stable-stringify": "^2.5.0"
10
10
  },
@@ -32,7 +32,7 @@
32
32
  "warlock": {
33
33
  "environment": "server"
34
34
  },
35
- "version": "5.24.0",
35
+ "version": "5.25.0",
36
36
  "main": "./cjs/index.cjs",
37
37
  "module": "./esm/index.mjs",
38
38
  "types": "./esm/index.d.mts",
@@ -1,111 +1,111 @@
1
- ---
2
- name: capture-unhandled-errors
3
- description: 'captureAnyUnhandledRejection() installs process.on(''unhandledRejection'') → log.error and process.on(''uncaughtException'') → log.fatal + process.exit(1) so process-level failures land in your channels and a fatal crash is never silently swallowed into exit 0. Triggers: `captureAnyUnhandledRejection`, `exitOnUncaughtException`, `unhandledRejection`, `uncaughtException`, `log.error`, `log.fatal`; "log unhandled promise rejections", "catch uncaught exceptions to a file", "record crashes before exit", "server exits 0 with no error", "silent exit / production server stopped", "global error handler with logger"; typical import `import { captureAnyUnhandledRejection, log } from "@warlock.js/logger"`. Skip: flushing — `@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `Sentry.init`, `@sentry/node`; native `process.on(''unhandledRejection'')`.'
4
- ---
5
-
6
- # Error capture — routing Node's unhandled errors through the logger
7
-
8
- `captureAnyUnhandledRejection()` installs two process-level listeners so crashes are logged — and, for an `uncaughtException`, made loud and terminal — instead of being silently swallowed.
9
-
10
- ## What it does
11
-
12
- ```ts
13
- import { captureAnyUnhandledRejection } from "@warlock.js/logger";
14
-
15
- captureAnyUnhandledRejection();
16
- ```
17
-
18
- Registers:
19
- - `process.on("unhandledRejection", reason => log.error("app", "unhandledRejection", reason))` — logged; the process is kept alive.
20
- - `process.on("uncaughtException", error => log.fatal("app", "uncaughtException", error))` — logged, then `process.exit(1)` (by default).
21
-
22
- The split is intentional: an `uncaughtException` leaves the process in an undefined state, so it's semantically `fatal` and takes the process down. An `unhandledRejection` is a failure but not always process-ending (depends on Node's `--unhandled-rejections` policy and your app's recovery), so it stays at `error` and never exits. This makes "page on fatal" alerting clean — only true crashes ring the pager.
23
-
24
- ## Why it exits (and why that matters)
25
-
26
- Registering *any* `uncaughtException` listener **suppresses** Node's default "print the stack + exit non-zero." A listener that only logs therefore turns an unrecoverable crash into a silent `exit 0` — which is exactly how a config file that throws at boot can look like "the server started, then just stopped," with no error printed. So the handler restores the contract:
27
-
28
- - **Exits non-zero** after logging (`process.exit(1)`), following a best-effort, time-bounded `log.flush()` so buffered `FileLog` / `SentryLog` entries drain first. Opt out with `captureAnyUnhandledRejection({ exitOnUncaughtException: false })` where the process is expected to recover on its own (a dev server reloading via HMR).
29
- - **Falls back to `console.error`** when no terminal channel is configured yet — the early-boot window, before `log.configure(...)`, where `log.fatal` has nowhere visible to go. When a `ConsoleLog` is present it already prints the entry, so the fallback is skipped (no double output).
30
-
31
- > The framework wires this for you in `bootstrap()` as `captureAnyUnhandledRejection({ exitOnUncaughtException: Application.isProduction })` — production crashes loudly and non-zero, the dev server logs-and-continues so HMR can recover.
32
-
33
- ## When to call it
34
-
35
- **Once**, at startup, **after** channels are registered. Typical place: immediately after your `log.configure({...})` call.
36
-
37
- ```ts title="src/index.ts"
38
- import {
39
- log,
40
- ConsoleLog,
41
- FileLog,
42
- captureAnyUnhandledRejection,
43
- } from "@warlock.js/logger";
44
-
45
- log.configure({
46
- channels: [new ConsoleLog(), new FileLog({ levels: ["error"] })],
47
- autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"], // ← important; see below
48
- });
49
-
50
- captureAnyUnhandledRejection();
51
- ```
52
-
53
- ## Flushing on the crash path
54
-
55
- The `uncaughtException` handler runs a best-effort, time-bounded `log.flush()` **before** its own `process.exit(1)`, so buffered `FileLog` / `SentryLog` entries drain even though `process.exit()` skips `beforeExit`. You don't need `autoFlushOn: ["beforeExit"]` for the fatal entry to survive — the handler already drains.
56
-
57
- `"beforeExit"` in `autoFlushOn` is still worth setting for the *other* exit routes (a natural drain when the event loop empties on its own). For signal-driven shutdown (`SIGINT` / `SIGTERM`), include those signals in `autoFlushOn`. See [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md).
58
-
59
- ## Idempotency — don't call it twice
60
-
61
- Calling `captureAnyUnhandledRejection()` a second time registers a second pair of listeners. Your next rejection gets logged twice. There's no dedup; just call it once.
62
-
63
- ## What it does **not** do
64
-
65
- - **Does not swallow a fatal error.** After an `uncaughtException` it records the error, then exits non-zero itself — restoring Node's own default, which merely registering the listener would otherwise suppress. Pass `{ exitOnUncaughtException: false }` only when the process is meant to survive (e.g. HMR).
66
- - **Does not exit on an `unhandledRejection`.** That path only logs (at `error`); the process keeps running. Set Node's `--unhandled-rejections=throw` if you want a rejection to escalate to an `uncaughtException`.
67
- - **Does not install Node's `--unhandled-rejections` policy.** That's a Node flag; set it in your launch script if you want strict mode.
68
- - **Does not hook `SIGTERM` / `SIGINT`** — use `enableAutoFlush` for signal flushes.
69
- - **Does not filter.** Every rejection is logged at `error` and every uncaught exception at `fatal`, both with `module: "app"`. Filter per-channel if some noise slips in.
70
-
71
- ## Checking an error was captured in tests
72
-
73
- Don't mock `process.on` — use a capturing channel and emit the listener directly:
74
-
75
- ```ts
76
- import { log, captureAnyUnhandledRejection, LogChannel } from "@warlock.js/logger";
77
- import type { LoggingData } from "@warlock.js/logger";
78
-
79
- class Capture extends LogChannel {
80
- public name = "capture";
81
- public received: LoggingData[] = [];
82
- public log(data: LoggingData) { this.received.push({ ...data }); }
83
- }
84
-
85
- it("routes unhandled rejections to the logger", async () => {
86
- const capture = new Capture();
87
- const originalChannels = log.channels;
88
- log.channels = [capture];
89
-
90
- captureAnyUnhandledRejection();
91
- process.emit("unhandledRejection", new Error("boom"), Promise.resolve());
92
-
93
- await new Promise((r) => setTimeout(r, 0));
94
-
95
- expect(capture.received[0]!.module).toBe("app");
96
- expect(capture.received[0]!.action).toBe("unhandledRejection");
97
-
98
- log.channels = originalChannels;
99
- });
100
- ```
101
-
102
- Testing the **`uncaughtException`** path additionally trips `process.exit(1)`, so stub it (`vi.spyOn(process, "exit").mockImplementation(() => undefined as never)`) or pass `{ exitOnUncaughtException: false }` — otherwise the emitted exception tears the test runner down. See [`@warlock.js/logger/test-logging-code/SKILL.md`](@warlock.js/logger/test-logging-code/SKILL.md).
103
-
104
- ## Module + action the capture uses
105
-
106
- Both listeners log with:
107
- - `module: "app"`
108
- - `action: "unhandledRejection"` (at `error`) or `action: "uncaughtException"` (at `fatal`)
109
- - `message`: the rejection reason / exception (keep it as the raw `Error` object — file channels capture the stack).
110
-
111
- If you want these routed to a specific file, filter on `data.module === "app"`. See [`@warlock.js/logger/filter-log-entries/SKILL.md`](@warlock.js/logger/filter-log-entries/SKILL.md).
1
+ ---
2
+ name: capture-unhandled-errors
3
+ description: 'captureAnyUnhandledRejection() installs process.on(''unhandledRejection'') → log.error and process.on(''uncaughtException'') → log.fatal + process.exit(1) so process-level failures land in your channels and a fatal crash is never silently swallowed into exit 0. Triggers: `captureAnyUnhandledRejection`, `exitOnUncaughtException`, `unhandledRejection`, `uncaughtException`, `log.error`, `log.fatal`; "log unhandled promise rejections", "catch uncaught exceptions to a file", "record crashes before exit", "server exits 0 with no error", "silent exit / production server stopped", "global error handler with logger"; typical import `import { captureAnyUnhandledRejection, log } from "@warlock.js/logger"`. Skip: flushing — `@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `Sentry.init`, `@sentry/node`; native `process.on(''unhandledRejection'')`.'
4
+ ---
5
+
6
+ # Error capture — routing Node's unhandled errors through the logger
7
+
8
+ `captureAnyUnhandledRejection()` installs two process-level listeners so crashes are logged — and, for an `uncaughtException`, made loud and terminal — instead of being silently swallowed.
9
+
10
+ ## What it does
11
+
12
+ ```ts
13
+ import { captureAnyUnhandledRejection } from "@warlock.js/logger";
14
+
15
+ captureAnyUnhandledRejection();
16
+ ```
17
+
18
+ Registers:
19
+ - `process.on("unhandledRejection", reason => log.error("app", "unhandledRejection", reason))` — logged; the process is kept alive.
20
+ - `process.on("uncaughtException", error => log.fatal("app", "uncaughtException", error))` — logged, then `process.exit(1)` (by default).
21
+
22
+ The split is intentional: an `uncaughtException` leaves the process in an undefined state, so it's semantically `fatal` and takes the process down. An `unhandledRejection` is a failure but not always process-ending (depends on Node's `--unhandled-rejections` policy and your app's recovery), so it stays at `error` and never exits. This makes "page on fatal" alerting clean — only true crashes ring the pager.
23
+
24
+ ## Why it exits (and why that matters)
25
+
26
+ Registering *any* `uncaughtException` listener **suppresses** Node's default "print the stack + exit non-zero." A listener that only logs therefore turns an unrecoverable crash into a silent `exit 0` — which is exactly how a config file that throws at boot can look like "the server started, then just stopped," with no error printed. So the handler restores the contract:
27
+
28
+ - **Exits non-zero** after logging (`process.exit(1)`), following a best-effort, time-bounded `log.flush()` so buffered `FileLog` / `SentryLog` entries drain first. Opt out with `captureAnyUnhandledRejection({ exitOnUncaughtException: false })` where the process is expected to recover on its own (a dev server reloading via HMR).
29
+ - **Falls back to `console.error`** when no terminal channel is configured yet — the early-boot window, before `log.configure(...)`, where `log.fatal` has nowhere visible to go. When a `ConsoleLog` is present it already prints the entry, so the fallback is skipped (no double output).
30
+
31
+ > The framework wires this for you in `bootstrap()` as `captureAnyUnhandledRejection({ exitOnUncaughtException: Application.isProduction })` — production crashes loudly and non-zero, the dev server logs-and-continues so HMR can recover.
32
+
33
+ ## When to call it
34
+
35
+ **Once**, at startup, **after** channels are registered. Typical place: immediately after your `log.configure({...})` call.
36
+
37
+ ```ts title="src/index.ts"
38
+ import {
39
+ log,
40
+ ConsoleLog,
41
+ FileLog,
42
+ captureAnyUnhandledRejection,
43
+ } from "@warlock.js/logger";
44
+
45
+ log.configure({
46
+ channels: [new ConsoleLog(), new FileLog({ levels: ["error"] })],
47
+ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"], // ← important; see below
48
+ });
49
+
50
+ captureAnyUnhandledRejection();
51
+ ```
52
+
53
+ ## Flushing on the crash path
54
+
55
+ The `uncaughtException` handler runs a best-effort, time-bounded `log.flush()` **before** its own `process.exit(1)`, so buffered `FileLog` / `SentryLog` entries drain even though `process.exit()` skips `beforeExit`. You don't need `autoFlushOn: ["beforeExit"]` for the fatal entry to survive — the handler already drains.
56
+
57
+ `"beforeExit"` in `autoFlushOn` is still worth setting for the *other* exit routes (a natural drain when the event loop empties on its own). For signal-driven shutdown (`SIGINT` / `SIGTERM`), include those signals in `autoFlushOn`. See [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md).
58
+
59
+ ## Idempotency — don't call it twice
60
+
61
+ Calling `captureAnyUnhandledRejection()` a second time registers a second pair of listeners. Your next rejection gets logged twice. There's no dedup; just call it once.
62
+
63
+ ## What it does **not** do
64
+
65
+ - **Does not swallow a fatal error.** After an `uncaughtException` it records the error, then exits non-zero itself — restoring Node's own default, which merely registering the listener would otherwise suppress. Pass `{ exitOnUncaughtException: false }` only when the process is meant to survive (e.g. HMR).
66
+ - **Does not exit on an `unhandledRejection`.** That path only logs (at `error`); the process keeps running. Set Node's `--unhandled-rejections=throw` if you want a rejection to escalate to an `uncaughtException`.
67
+ - **Does not install Node's `--unhandled-rejections` policy.** That's a Node flag; set it in your launch script if you want strict mode.
68
+ - **Does not hook `SIGTERM` / `SIGINT`** — use `enableAutoFlush` for signal flushes.
69
+ - **Does not filter.** Every rejection is logged at `error` and every uncaught exception at `fatal`, both with `module: "app"`. Filter per-channel if some noise slips in.
70
+
71
+ ## Checking an error was captured in tests
72
+
73
+ Don't mock `process.on` — use a capturing channel and emit the listener directly:
74
+
75
+ ```ts
76
+ import { log, captureAnyUnhandledRejection, LogChannel } from "@warlock.js/logger";
77
+ import type { LoggingData } from "@warlock.js/logger";
78
+
79
+ class Capture extends LogChannel {
80
+ public name = "capture";
81
+ public received: LoggingData[] = [];
82
+ public log(data: LoggingData) { this.received.push({ ...data }); }
83
+ }
84
+
85
+ it("routes unhandled rejections to the logger", async () => {
86
+ const capture = new Capture();
87
+ const originalChannels = log.channels;
88
+ log.channels = [capture];
89
+
90
+ captureAnyUnhandledRejection();
91
+ process.emit("unhandledRejection", new Error("boom"), Promise.resolve());
92
+
93
+ await new Promise((r) => setTimeout(r, 0));
94
+
95
+ expect(capture.received[0]!.module).toBe("app");
96
+ expect(capture.received[0]!.action).toBe("unhandledRejection");
97
+
98
+ log.channels = originalChannels;
99
+ });
100
+ ```
101
+
102
+ Testing the **`uncaughtException`** path additionally trips `process.exit(1)`, so stub it (`vi.spyOn(process, "exit").mockImplementation(() => undefined as never)`) or pass `{ exitOnUncaughtException: false }` — otherwise the emitted exception tears the test runner down. See [`@warlock.js/logger/test-logging-code/SKILL.md`](@warlock.js/logger/test-logging-code/SKILL.md).
103
+
104
+ ## Module + action the capture uses
105
+
106
+ Both listeners log with:
107
+ - `module: "app"`
108
+ - `action: "unhandledRejection"` (at `error`) or `action: "uncaughtException"` (at `fatal`)
109
+ - `message`: the rejection reason / exception (keep it as the raw `Error` object — file channels capture the stack).
110
+
111
+ If you want these routed to a specific file, filter on `data.module === "app"`. See [`@warlock.js/logger/filter-log-entries/SKILL.md`](@warlock.js/logger/filter-log-entries/SKILL.md).
@@ -1,105 +1,105 @@
1
- ---
2
- name: configure-logger
3
- description: 'Register channels via log.addChannel / log.setChannels / log.configure({channels, autoFlushOn, redact, minLevel}) at boot. Triggers: `log.configure`, `log.addChannel`, `log.setChannels`, `Logger`, `autoFlushOn`, `disableAutoFlush`; "wire channels at startup", "branch logger by NODE_ENV", "isolate a library''s logger", "replace channel list"; typical import `import { log, Logger, ConsoleLog, FileLog } from "@warlock.js/logger"`. Skip: channel picks — `@warlock.js/logger/pick-log-channel/SKILL.md`; flushing — `@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`; redaction — `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`; competing libs `winston.createLogger`, `pino`.'
4
- ---
5
-
6
- # Setup — registering channels at startup
7
-
8
- The logger is a singleton. Do all setup in one place, as early in the app entry point as possible.
9
-
10
- ## The three channel-registration methods
11
-
12
- | Method | Semantics |
13
- |---|---|
14
- | `log.addChannel(channel)` | **Appends.** Safe to call multiple times. |
15
- | `log.setChannels([...])` | **Replaces** the full list. |
16
- | `log.configure({ channels, autoFlushOn, redact, minLevel })` | **Replaces** channels if provided; installs auto-flush if provided; sets redact / minLevel if provided. All four are optional. |
17
-
18
- All three return `this` — chainable.
19
-
20
- ## Recommended pattern — one dedicated file
21
-
22
- ```ts title="src/logger.ts"
23
- import { log, ConsoleLog, FileLog, JSONFileLog } from "@warlock.js/logger";
24
-
25
- if (process.env.NODE_ENV === "production") {
26
- log.configure({
27
- channels: [
28
- new FileLog({ storagePath: "./storage/logs", chunk: "daily", rotate: true }),
29
- new JSONFileLog({ storagePath: "./storage/logs-json", chunk: "daily" }),
30
- ],
31
- autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
32
- });
33
- } else if (process.env.NODE_ENV === "test") {
34
- log.setChannels([]); // silence logger during tests
35
- } else {
36
- log.setChannels([new ConsoleLog()]);
37
- }
38
- ```
39
-
40
- Import it once at the top of `src/index.ts`:
41
-
42
- ```ts title="src/index.ts"
43
- import "./logger"; // side-effect: configures singleton
44
- import { log } from "@warlock.js/logger";
45
-
46
- log.info("app", "start", "Server listening on :3000");
47
- ```
48
-
49
- ## What `configure({ autoFlushOn })` does
50
-
51
- Registers one process-level handler per event that calls `log.flushSync()` before Node exits. See [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md) for the full behavior table.
52
-
53
- ```ts
54
- log.configure({
55
- channels: [new FileLog()],
56
- autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
57
- });
58
- // Now a buffered FileLog flushes on Ctrl+C, container stop, and natural exit.
59
- ```
60
-
61
- Calling `configure({ autoFlushOn })` a second time **replaces** previous handlers (not stacks them). Call `log.disableAutoFlush()` to tear them down.
62
-
63
- ## Creating an isolated Logger
64
-
65
- Rarely needed. Useful when a library wants its own channel list that doesn't share with the host app:
66
-
67
- ```ts
68
- import { Logger, ConsoleLog } from "@warlock.js/logger";
69
-
70
- export const libraryLogger = new Logger();
71
- libraryLogger.addChannel(new ConsoleLog({ filter: (d) => d.module === "my-lib" }));
72
- ```
73
-
74
- Every `new Logger()` gets a unique `id` (string, prefixed `"logger-"`).
75
-
76
- ## Order matters — ANSI stripping across channels
77
-
78
- `Logger.log` shallow-clones the entry per non-terminal channel before stripping ANSI codes. Registering a terminal channel (ConsoleLog) **after** a non-terminal one (FileLog) still works — ConsoleLog sees the original colored message. But if you register them in reverse and add a channel that mutates `data` in place, the non-terminal channel will see the terminal channel's version. Prefer the built-ins; custom channels should not mutate `data`.
79
-
80
- ## When to call what
81
-
82
- - **`addChannel`** — most common. Add channels as you discover you need them during setup.
83
- - **`setChannels`** — when env branching makes the full list clear at once (production vs dev).
84
- - **`configure`** — when you also want to install auto-flush, redact, or minLevel in the same call.
85
-
86
- ## Combining everything
87
-
88
- ```ts
89
- log.configure({
90
- channels: [
91
- new ConsoleLog({ showContext: true }),
92
- new FileLog({ chunk: "daily" }),
93
- ],
94
- autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
95
- redact: { paths: ["context.password", "context.headers.authorization"] },
96
- minLevel: process.env.LOG_LEVEL === "debug" ? "debug" : "info",
97
- });
98
- ```
99
-
100
- See [`@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md) for the redact contract and [`@warlock.js/logger/filter-log-entries/SKILL.md`](@warlock.js/logger/filter-log-entries/SKILL.md) for `minLevel`.
101
-
102
- ## See also
103
-
104
- - [`@warlock.js/logger/pick-log-channel/SKILL.md`](@warlock.js/logger/pick-log-channel/SKILL.md) — what each built-in channel does
105
- - [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md) — `autoFlushOn` event behavior
1
+ ---
2
+ name: configure-logger
3
+ description: 'Register channels via log.addChannel / log.setChannels / log.configure({channels, autoFlushOn, redact, minLevel}) at boot. Triggers: `log.configure`, `log.addChannel`, `log.setChannels`, `Logger`, `autoFlushOn`, `disableAutoFlush`; "wire channels at startup", "branch logger by NODE_ENV", "isolate a library''s logger", "replace channel list"; typical import `import { log, Logger, ConsoleLog, FileLog } from "@warlock.js/logger"`. Skip: channel picks — `@warlock.js/logger/pick-log-channel/SKILL.md`; flushing — `@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`; redaction — `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`; competing libs `winston.createLogger`, `pino`.'
4
+ ---
5
+
6
+ # Setup — registering channels at startup
7
+
8
+ The logger is a singleton. Do all setup in one place, as early in the app entry point as possible.
9
+
10
+ ## The three channel-registration methods
11
+
12
+ | Method | Semantics |
13
+ |---|---|
14
+ | `log.addChannel(channel)` | **Appends.** Safe to call multiple times. |
15
+ | `log.setChannels([...])` | **Replaces** the full list. |
16
+ | `log.configure({ channels, autoFlushOn, redact, minLevel })` | **Replaces** channels if provided; installs auto-flush if provided; sets redact / minLevel if provided. All four are optional. |
17
+
18
+ All three return `this` — chainable.
19
+
20
+ ## Recommended pattern — one dedicated file
21
+
22
+ ```ts title="src/logger.ts"
23
+ import { log, ConsoleLog, FileLog, JSONFileLog } from "@warlock.js/logger";
24
+
25
+ if (process.env.NODE_ENV === "production") {
26
+ log.configure({
27
+ channels: [
28
+ new FileLog({ storagePath: "./storage/logs", chunk: "daily", rotate: true }),
29
+ new JSONFileLog({ storagePath: "./storage/logs-json", chunk: "daily" }),
30
+ ],
31
+ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
32
+ });
33
+ } else if (process.env.NODE_ENV === "test") {
34
+ log.setChannels([]); // silence logger during tests
35
+ } else {
36
+ log.setChannels([new ConsoleLog()]);
37
+ }
38
+ ```
39
+
40
+ Import it once at the top of `src/index.ts`:
41
+
42
+ ```ts title="src/index.ts"
43
+ import "./logger"; // side-effect: configures singleton
44
+ import { log } from "@warlock.js/logger";
45
+
46
+ log.info("app", "start", "Server listening on :3000");
47
+ ```
48
+
49
+ ## What `configure({ autoFlushOn })` does
50
+
51
+ Registers one process-level handler per event that calls `log.flushSync()` before Node exits. See [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md) for the full behavior table.
52
+
53
+ ```ts
54
+ log.configure({
55
+ channels: [new FileLog()],
56
+ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
57
+ });
58
+ // Now a buffered FileLog flushes on Ctrl+C, container stop, and natural exit.
59
+ ```
60
+
61
+ Calling `configure({ autoFlushOn })` a second time **replaces** previous handlers (not stacks them). Call `log.disableAutoFlush()` to tear them down.
62
+
63
+ ## Creating an isolated Logger
64
+
65
+ Rarely needed. Useful when a library wants its own channel list that doesn't share with the host app:
66
+
67
+ ```ts
68
+ import { Logger, ConsoleLog } from "@warlock.js/logger";
69
+
70
+ export const libraryLogger = new Logger();
71
+ libraryLogger.addChannel(new ConsoleLog({ filter: (d) => d.module === "my-lib" }));
72
+ ```
73
+
74
+ Every `new Logger()` gets a unique `id` (string, prefixed `"logger-"`).
75
+
76
+ ## Order matters — ANSI stripping across channels
77
+
78
+ `Logger.log` shallow-clones the entry per non-terminal channel before stripping ANSI codes. Registering a terminal channel (ConsoleLog) **after** a non-terminal one (FileLog) still works — ConsoleLog sees the original colored message. But if you register them in reverse and add a channel that mutates `data` in place, the non-terminal channel will see the terminal channel's version. Prefer the built-ins; custom channels should not mutate `data`.
79
+
80
+ ## When to call what
81
+
82
+ - **`addChannel`** — most common. Add channels as you discover you need them during setup.
83
+ - **`setChannels`** — when env branching makes the full list clear at once (production vs dev).
84
+ - **`configure`** — when you also want to install auto-flush, redact, or minLevel in the same call.
85
+
86
+ ## Combining everything
87
+
88
+ ```ts
89
+ log.configure({
90
+ channels: [
91
+ new ConsoleLog({ showContext: true }),
92
+ new FileLog({ chunk: "daily" }),
93
+ ],
94
+ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
95
+ redact: { paths: ["context.password", "context.headers.authorization"] },
96
+ minLevel: process.env.LOG_LEVEL === "debug" ? "debug" : "info",
97
+ });
98
+ ```
99
+
100
+ See [`@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md) for the redact contract and [`@warlock.js/logger/filter-log-entries/SKILL.md`](@warlock.js/logger/filter-log-entries/SKILL.md) for `minLevel`.
101
+
102
+ ## See also
103
+
104
+ - [`@warlock.js/logger/pick-log-channel/SKILL.md`](@warlock.js/logger/pick-log-channel/SKILL.md) — what each built-in channel does
105
+ - [`@warlock.js/logger/flush-logs-on-shutdown/SKILL.md`](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md) — `autoFlushOn` event behavior