@warlock.js/logger 5.19.0 → 5.20.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/llms.txt CHANGED
@@ -7,14 +7,14 @@
7
7
  ## Skills
8
8
 
9
9
  - [capture-unhandled-errors](@warlock.js/logger/capture-unhandled-errors/SKILL.md): (no description)
10
- - [configure-logger](@warlock.js/logger/configure-logger/SKILL.md): Setup — registering channels at startup in @warlock.js/logger; use when you need to configure logger.
10
+ - [configure-logger](@warlock.js/logger/configure-logger/SKILL.md): (no description)
11
11
  - [filter-log-entries](@warlock.js/logger/filter-log-entries/SKILL.md): (no description)
12
12
  - [flush-logs-on-shutdown](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md): (no description)
13
13
  - [logger-basics](@warlock.js/logger/logger-basics/SKILL.md): Start here for @warlock.js/logger channels, structured entries, transports, and application logging setup.
14
14
  - [overview](@warlock.js/logger/overview/SKILL.md): (no description)
15
15
  - [pick-log-channel](@warlock.js/logger/pick-log-channel/SKILL.md): (no description)
16
- - [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): Redaction — keeping secrets out of logs in @warlock.js/logger; use when you need to redact sensitive log fields.
16
+ - [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): (no description)
17
17
  - [ship-logs-to-sentry](@warlock.js/logger/ship-logs-to-sentry/SKILL.md): (no description)
18
- - [test-logging-code](@warlock.js/logger/test-logging-code/SKILL.md): Testing — code that logs, and asserting on log output in @warlock.js/logger; use when you need to test logging code.
19
- - [use-log-helpers](@warlock.js/logger/use-log-helpers/SKILL.md): Helpers — assert, timer in @warlock.js/logger; use when you need to use log helpers.
18
+ - [test-logging-code](@warlock.js/logger/test-logging-code/SKILL.md): (no description)
19
+ - [use-log-helpers](@warlock.js/logger/use-log-helpers/SKILL.md): (no description)
20
20
  - [write-custom-log-channel](@warlock.js/logger/write-custom-log-channel/SKILL.md): (no description)
package/package.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "description": "A powerful logging system for messages and errors in nodejs.",
4
4
  "dependencies": {
5
5
  "@mongez/copper": "^2.1.2",
6
- "@mongez/reinforcements": "^4.0.1",
7
- "@warlock.js/fs": "5.19.0",
6
+ "@mongez/reinforcements": "^4.0.2",
7
+ "@warlock.js/fs": "5.20.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.19.0",
35
+ "version": "5.20.0",
36
36
  "main": "./cjs/index.cjs",
37
37
  "module": "./esm/index.mjs",
38
38
  "types": "./esm/index.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: capture-unhandled-errors
3
- description: "Error capture — routing Node's unhandled errors through the logger in @warlock.js/logger; use when you need to 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
4
  ---
5
5
 
6
6
  # Error capture — routing Node's unhandled errors through the logger
@@ -1,105 +1,105 @@
1
- ---
2
- name: configure-logger
3
- description: "Setup — registering channels at startup in @warlock.js/logger; use when you need to configure logger."
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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: filter-log-entries
3
- description: "Filtering — levels + filter predicate + minLevel in @warlock.js/logger; use when you need to filter log entries."
3
+ description: 'Drop log entries — per-channel levels whitelist, per-channel filter predicate, logger-wide setMinLevel(level) fast path. Triggers: `levels`, `filter`, `minLevel`, `log.setMinLevel`, `shouldBeLogged`, `LoggingData`, `LogLevel`; "silence a noisy module", "route errors to a dedicated file", "raise global severity floor", "drop debug logs in prod"; typical import `import { log } from "@warlock.js/logger"`. Skip: custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; channel picks — `@warlock.js/logger/pick-log-channel/SKILL.md`; competing libs `pino.levels`, `winston.format.filter`, `debug` env var.'
4
4
  ---
5
5
 
6
6
  # Filtering — `levels` + `filter` predicate + `minLevel`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: flush-logs-on-shutdown
3
- description: "Lifecycle — flushing buffered channels before exit in @warlock.js/logger; use when you need to flush logs on shutdown."
3
+ description: 'Drain buffered channels before exit — log.flushSync() or log.configure({autoFlushOn: [''SIGINT'', ''SIGTERM'', ''beforeExit'']}) installs handlers that re-raise the signal. Triggers: `log.flush`, `log.flushSync`, `autoFlushOn`, `enableAutoFlush`, `disableAutoFlush`, `SIGINT`, `SIGTERM`, `beforeExit`; "drain logs before exit", "await log.flush() before process.exit", "drain async or network channels on shutdown", "wire SIGTERM for container shutdown", "my logs never showed after a crash", "graceful shutdown logging"; typical import `import { log, FileLog } from "@warlock.js/logger"`. Skip: error capture — `@warlock.js/logger/capture-unhandled-errors/SKILL.md`; custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; competing `pino.final`, `winston.end`; native `process.on(''exit'')`.'
4
4
  ---
5
5
 
6
6
  # Lifecycle — flushing buffered channels before exit
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: "Structured logging for Warlock.js with pluggable channels, redaction and shutdown flushing. Exports `log`, `Logger`, `LogChannel`, `ConsoleLog`, `FileLog`, `JSONFileLog`, `SentryLog`, `applyRedact`, `DEFAULT_REDACT_KEYS`, `captureAnyUnhandledRejection`. Use for: \"log an error with context\", \"write logs to a file as JSON\", \"send errors to Sentry\", \"hide passwords and tokens in logs\", \"flush logs before exit\", \"catch unhandled rejections\", \"write my own log channel\". Not this package: HTTP request handling and error responses live in @warlock.js/core; scheduled jobs are @warlock.js/scheduler."
3
+ ---
4
+ # @warlock.js/logger
5
+
6
+ The `log` singleton (a `Logger`) sends each entry, with module, action, level and message, to every configured `LogChannel`. Channels decide where entries go (console, file, JSON file, Sentry, custom) and can filter by level or module.
7
+
8
+ ## The 80% path
9
+ 1. Orient: `overview.md`, `logger-basics.md`.
10
+ 2. Pick channels: `pick-log-channel.md`; set them up: `configure-logger.md`.
11
+ 3. Log via `log.info/warn/error` or helpers: `use-log-helpers.md`.
12
+ 4. Redact secrets: `redact-sensitive-log-fields.md`; narrow output: `filter-log-entries.md`.
13
+ 5. Production safety: `capture-unhandled-errors.md`, `flush-logs-on-shutdown.md`, `ship-logs-to-sentry.md`.
14
+ 6. Extend or test: `write-custom-log-channel.md`, `test-logging-code.md`.
15
+
16
+ ## Conventions and pitfalls
17
+ - `fatal` does not flush or exit by itself; the caller awaits `log.flush()` and then exits.
18
+ - File channels buffer, so an unflushed process exit loses the tail of the log.
19
+ - Configure redaction before logging request bodies or auth data; the default key set is a floor, not a guarantee.
20
+ - Log structured context objects rather than interpolated strings, so channels and Sentry can index them.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pick-log-channel
3
- description: "Channels — which one to pick and how to configure it in @warlock.js/logger; use when you need to choose log channel."
3
+ description: 'Pick one of the four built-in channels — ConsoleLog (terminal), FileLog (plain text on disk), JSONFileLog (structured JSON for aggregators like Loki / Datadog / Elastic), SentryLog (forwards errors + breadcrumbs to Sentry). Triggers: `ConsoleLog`, `FileLog`, `JSONFileLog`, `SentryLog`, `chunk`, `rotate`, `groupBy`, `maxFileSize`, `showContext`, `log.channel`; "log to a file", "rotate log files", "daily log chunks", "json logs for datadog / loki / elastic", "send logs to Sentry"; typical import `import { ConsoleLog, FileLog, JSONFileLog, SentryLog } from "@warlock.js/logger"`. Skip: Sentry-specific setup — `@warlock.js/logger/ship-logs-to-sentry/SKILL.md`; custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; registration — `@warlock.js/logger/configure-logger/SKILL.md`; competing libs `winston-daily-rotate-file`, `pino-pretty`.'
4
4
  ---
5
5
 
6
6
  # Channels — which one to pick and how to configure it
@@ -1,156 +1,156 @@
1
- ---
2
- name: redact-sensitive-log-fields
3
- description: "Redaction — keeping secrets out of logs in @warlock.js/logger; use when you need to redact sensitive log fields."
4
- ---
5
-
6
- # Redaction — keeping secrets out of logs
7
-
8
- Three layers: a **built-in key denylist that is on by default**, plus opt-in path globs configured at the logger and/or per channel.
9
-
10
- ## Layer 0 — the default denylist (since 4.15.0, no configuration needed)
11
-
12
- Common secret **key names** are censored at any depth of `context`, `message`, and an `Error`'s own enumerable properties — before any of your config runs:
13
-
14
- ```ts
15
- log.error("auth", "login", "failed", { headers: req.headers, body: req.body });
16
- // context.headers.authorization → "[REDACTED]"
17
- // context.body.password → "[REDACTED]"
18
- // context.body.email → untouched
19
- ```
20
-
21
- Keys are matched **exactly**, on a normalized form (lower-cased, separators stripped) — so one entry covers `apiKey` / `api_key` / `API-KEY` / `x-api-key`. It is not substring matching: `tokenCount` and `passwordUpdatedAt` survive. Read the exact set from `DEFAULT_REDACT_KEYS`.
22
-
23
- ```ts
24
- import { DEFAULT_REDACT_KEYS } from "@warlock.js/logger";
25
-
26
- log.configure({
27
- redact: {
28
- keys: ["internalRef"], // union with the built-in set
29
- defaultKeys: false, // opt out of the built-in set entirely
30
- },
31
- });
32
- ```
33
-
34
- `defaultKeys: false` is an escape hatch, not a tuning knob — it restores the pre-4.15.0 behavior where a `password` in `context` reaches every sink in cleartext. Prefer adding a function `censor` if you only need to keep a prefix.
35
-
36
- **A channel cannot turn the default set off** (it can only add keys, or turn it back *on* if the logger-wide config disabled it) — same additive-only contract as paths, below. And `log.setRedact(undefined)` clears *your* paths, not the default denylist.
37
-
38
- Not reachable by any layer: secrets interpolated into a `message` string (`` `token=${t}` ``), `Map`/`Set`/`Buffer` contents, and getter-backed or non-enumerable properties.
39
-
40
- ## Layers 1 & 2 — path globs, opt-in
41
-
42
- For anything the denylist can't name by key — a secret under an app-specific key, or a value you want partially masked rather than blanked.
43
-
44
- The model in one line:
45
-
46
- > Logger-wide redaction is the security floor. Per-channel redaction adds more paths. **No channel can ever undo a logger-wide redaction.**
47
-
48
- That guarantee is the whole point — once you've set `password` to redact at the logger, you can audit one place to know nothing leaks it, regardless of how many channels you add.
49
-
50
- ## Logger-wide floor
51
-
52
- ```ts
53
- import { log } from "@warlock.js/logger";
54
-
55
- log.configure({
56
- redact: {
57
- paths: [
58
- "context.password",
59
- "context.*.token",
60
- "context.headers.authorization",
61
- ],
62
- censor: "[REDACTED]", // default — string or function
63
- },
64
- });
65
-
66
- // runtime equivalent:
67
- log.setRedact({ paths: ["context.password"] });
68
- log.setRedact(undefined); // clear your paths (the default denylist stays on)
69
- ```
70
-
71
- Every channel sees the redacted entry. Cheap: applied **once** before fan-out; channels share the redacted clone unless they add their own paths.
72
-
73
- ## Per-channel additive
74
-
75
- ```ts
76
- new SlackChannel({
77
- webhook: "...",
78
- redact: {
79
- paths: ["context.user.email", "context.metadata.*"],
80
- // censor inherited from logger-wide when omitted
81
- },
82
- });
83
- ```
84
-
85
- The channel's `paths` are **merged** with the logger floor — the channel runs a single combined redact pass, never replaces the floor. The channel's `censor` (if provided) wins for both its own and the logger's paths in this channel only; the logger floor still uses its own censor for other channels.
86
-
87
- ### When to set redact per-channel
88
-
89
- - Loud destinations with broader audiences (Slack, Discord, error trackers, anything off your machine) — redact more aggressively.
90
- - Local-only destinations (FileLog you alone read, the dev terminal) — keep the floor minimal so you can debug.
91
-
92
- ### When NOT to set it
93
-
94
- If you want raw context in your dev terminal, **don't add redact at the logger level** — set it only on the file/JSON/network channels. Logger-wide is the floor, so it applies everywhere; you can't opt a single channel out.
95
-
96
- ## Path syntax
97
-
98
- Paths are dotted glob patterns evaluated against the full `LoggingData`:
99
-
100
- ```
101
- type LoggingData = {
102
- type: "info" | ...,
103
- module: string,
104
- action: string,
105
- message: any, // ← prefix paths with "message." to redact here
106
- context?: object, // ← prefix paths with "context." to redact here
107
- };
108
- ```
109
-
110
- | Pattern | Matches |
111
- | --- | --- |
112
- | `context.password` | exactly `data.context.password` |
113
- | `context.*.token` | `data.context.<any>.token` (one segment in between) |
114
- | `**.password` | `data.context.password`, `data.context.user.password`, … any depth |
115
- | `message.apiKey` | when message is an object, `data.message.apiKey` |
116
- | `context.users.*.token` | array element redaction (`*` matches indices too) |
117
-
118
- Wildcards:
119
-
120
- - `*` — exactly one segment (any object key, any array index).
121
- - `**` — zero or more segments, greedily; matches at any depth.
122
-
123
- ## Censor variants
124
-
125
- ```ts
126
- // String — replace with a literal.
127
- { censor: "[REDACTED]" }
128
- { censor: "***" }
129
-
130
- // Function — receives original value + dotted path, returns the replacement.
131
- {
132
- censor: (value, path) => {
133
- if (typeof value !== "string") return "[REDACTED]";
134
- return value.length > 4 ? `${value.slice(0, 2)}***${value.slice(-2)}` : "***";
135
- },
136
- }
137
- ```
138
-
139
- Function censors are called for every match — keep them cheap. The path is the actual matched location (e.g. `"context.users.0.token"` for an array hit).
140
-
141
- ## Immutability
142
-
143
- `applyRedact` always returns a deep clone — your input data is never mutated. `Date` and `Error` instances are reconstructed (so `instanceof` checks still work). Circular references are tolerated.
144
-
145
- ## What about the `message` field?
146
-
147
- If `message` is a plain object, paths under `message.*` work as expected. If `message` is a string (the most common case), redaction won't scan it — string scrubbing requires regex and is out of scope for this primitive. Wrap secrets in `context` and they'll be redacted reliably.
148
-
149
- ## Performance notes
150
-
151
- - **No redact configured** → the default denylist still runs (since 4.15.0): a cheap presence scan for a denylisted key on every `log()` call, with the deep clone + censor pass skipped entirely when nothing matches. Only `{ defaultKeys: false }` (no `paths`, no extra `keys`) gets back to zero work.
152
- - **Logger-wide redact only** → one deep clone + one path-walk per `log()` call, shared by every channel.
153
- - **Channel adds paths** → that channel re-clones from the original input and runs the merged pass once. Other channels still share the cheaper logger-wide clone.
154
- - Each path is matched independently; cost grows linearly with `paths.length`.
155
-
156
- For most apps with `<10` redact paths and shallow context, the cost is below 100µs per entry. If you're logging millions of entries per second through paths like `**.something`, profile before scaling up — `**` is the only pattern that recurses through every key.
1
+ ---
2
+ name: redact-sensitive-log-fields
3
+ description: 'Strip secrets from log output — a built-in secret-key denylist on by default (DEFAULT_REDACT_KEYS), plus two-layer additive redaction via log.configure({redact: {paths, keys}}) (logger floor) + per-channel redact (more on top). Dotted glob paths (*, **). Triggers: `redact`, `paths`, `keys`, `defaultKeys`, `censor`, `log.setRedact`, `applyRedact`; "redact passwords in logs", "strip tokens from log output", "hide authorization headers", "scrub PII before logging", "turn off default redaction"; typical import `import { log } from "@warlock.js/logger"`. Skip: filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; competing libs `pino.redact`, `fast-redact`.'
4
+ ---
5
+
6
+ # Redaction — keeping secrets out of logs
7
+
8
+ Three layers: a **built-in key denylist that is on by default**, plus opt-in path globs configured at the logger and/or per channel.
9
+
10
+ ## Layer 0 — the default denylist (since 4.15.0, no configuration needed)
11
+
12
+ Common secret **key names** are censored at any depth of `context`, `message`, and an `Error`'s own enumerable properties — before any of your config runs:
13
+
14
+ ```ts
15
+ log.error("auth", "login", "failed", { headers: req.headers, body: req.body });
16
+ // context.headers.authorization → "[REDACTED]"
17
+ // context.body.password → "[REDACTED]"
18
+ // context.body.email → untouched
19
+ ```
20
+
21
+ Keys are matched **exactly**, on a normalized form (lower-cased, separators stripped) — so one entry covers `apiKey` / `api_key` / `API-KEY` / `x-api-key`. It is not substring matching: `tokenCount` and `passwordUpdatedAt` survive. Read the exact set from `DEFAULT_REDACT_KEYS`.
22
+
23
+ ```ts
24
+ import { DEFAULT_REDACT_KEYS } from "@warlock.js/logger";
25
+
26
+ log.configure({
27
+ redact: {
28
+ keys: ["internalRef"], // union with the built-in set
29
+ defaultKeys: false, // opt out of the built-in set entirely
30
+ },
31
+ });
32
+ ```
33
+
34
+ `defaultKeys: false` is an escape hatch, not a tuning knob — it restores the pre-4.15.0 behavior where a `password` in `context` reaches every sink in cleartext. Prefer adding a function `censor` if you only need to keep a prefix.
35
+
36
+ **A channel cannot turn the default set off** (it can only add keys, or turn it back *on* if the logger-wide config disabled it) — same additive-only contract as paths, below. And `log.setRedact(undefined)` clears *your* paths, not the default denylist.
37
+
38
+ Not reachable by any layer: secrets interpolated into a `message` string (`` `token=${t}` ``), `Map`/`Set`/`Buffer` contents, and getter-backed or non-enumerable properties.
39
+
40
+ ## Layers 1 & 2 — path globs, opt-in
41
+
42
+ For anything the denylist can't name by key — a secret under an app-specific key, or a value you want partially masked rather than blanked.
43
+
44
+ The model in one line:
45
+
46
+ > Logger-wide redaction is the security floor. Per-channel redaction adds more paths. **No channel can ever undo a logger-wide redaction.**
47
+
48
+ That guarantee is the whole point — once you've set `password` to redact at the logger, you can audit one place to know nothing leaks it, regardless of how many channels you add.
49
+
50
+ ## Logger-wide floor
51
+
52
+ ```ts
53
+ import { log } from "@warlock.js/logger";
54
+
55
+ log.configure({
56
+ redact: {
57
+ paths: [
58
+ "context.password",
59
+ "context.*.token",
60
+ "context.headers.authorization",
61
+ ],
62
+ censor: "[REDACTED]", // default — string or function
63
+ },
64
+ });
65
+
66
+ // runtime equivalent:
67
+ log.setRedact({ paths: ["context.password"] });
68
+ log.setRedact(undefined); // clear your paths (the default denylist stays on)
69
+ ```
70
+
71
+ Every channel sees the redacted entry. Cheap: applied **once** before fan-out; channels share the redacted clone unless they add their own paths.
72
+
73
+ ## Per-channel additive
74
+
75
+ ```ts
76
+ new SlackChannel({
77
+ webhook: "...",
78
+ redact: {
79
+ paths: ["context.user.email", "context.metadata.*"],
80
+ // censor inherited from logger-wide when omitted
81
+ },
82
+ });
83
+ ```
84
+
85
+ The channel's `paths` are **merged** with the logger floor — the channel runs a single combined redact pass, never replaces the floor. The channel's `censor` (if provided) wins for both its own and the logger's paths in this channel only; the logger floor still uses its own censor for other channels.
86
+
87
+ ### When to set redact per-channel
88
+
89
+ - Loud destinations with broader audiences (Slack, Discord, error trackers, anything off your machine) — redact more aggressively.
90
+ - Local-only destinations (FileLog you alone read, the dev terminal) — keep the floor minimal so you can debug.
91
+
92
+ ### When NOT to set it
93
+
94
+ If you want raw context in your dev terminal, **don't add redact at the logger level** — set it only on the file/JSON/network channels. Logger-wide is the floor, so it applies everywhere; you can't opt a single channel out.
95
+
96
+ ## Path syntax
97
+
98
+ Paths are dotted glob patterns evaluated against the full `LoggingData`:
99
+
100
+ ```
101
+ type LoggingData = {
102
+ type: "info" | ...,
103
+ module: string,
104
+ action: string,
105
+ message: any, // ← prefix paths with "message." to redact here
106
+ context?: object, // ← prefix paths with "context." to redact here
107
+ };
108
+ ```
109
+
110
+ | Pattern | Matches |
111
+ | --- | --- |
112
+ | `context.password` | exactly `data.context.password` |
113
+ | `context.*.token` | `data.context.<any>.token` (one segment in between) |
114
+ | `**.password` | `data.context.password`, `data.context.user.password`, … any depth |
115
+ | `message.apiKey` | when message is an object, `data.message.apiKey` |
116
+ | `context.users.*.token` | array element redaction (`*` matches indices too) |
117
+
118
+ Wildcards:
119
+
120
+ - `*` — exactly one segment (any object key, any array index).
121
+ - `**` — zero or more segments, greedily; matches at any depth.
122
+
123
+ ## Censor variants
124
+
125
+ ```ts
126
+ // String — replace with a literal.
127
+ { censor: "[REDACTED]" }
128
+ { censor: "***" }
129
+
130
+ // Function — receives original value + dotted path, returns the replacement.
131
+ {
132
+ censor: (value, path) => {
133
+ if (typeof value !== "string") return "[REDACTED]";
134
+ return value.length > 4 ? `${value.slice(0, 2)}***${value.slice(-2)}` : "***";
135
+ },
136
+ }
137
+ ```
138
+
139
+ Function censors are called for every match — keep them cheap. The path is the actual matched location (e.g. `"context.users.0.token"` for an array hit).
140
+
141
+ ## Immutability
142
+
143
+ `applyRedact` always returns a deep clone — your input data is never mutated. `Date` and `Error` instances are reconstructed (so `instanceof` checks still work). Circular references are tolerated.
144
+
145
+ ## What about the `message` field?
146
+
147
+ If `message` is a plain object, paths under `message.*` work as expected. If `message` is a string (the most common case), redaction won't scan it — string scrubbing requires regex and is out of scope for this primitive. Wrap secrets in `context` and they'll be redacted reliably.
148
+
149
+ ## Performance notes
150
+
151
+ - **No redact configured** → the default denylist still runs (since 4.15.0): a cheap presence scan for a denylisted key on every `log()` call, with the deep clone + censor pass skipped entirely when nothing matches. Only `{ defaultKeys: false }` (no `paths`, no extra `keys`) gets back to zero work.
152
+ - **Logger-wide redact only** → one deep clone + one path-walk per `log()` call, shared by every channel.
153
+ - **Channel adds paths** → that channel re-clones from the original input and runs the merged pass once. Other channels still share the cheaper logger-wide clone.
154
+ - Each path is matched independently; cost grows linearly with `paths.length`.
155
+
156
+ For most apps with `<10` redact paths and shallow context, the cost is below 100µs per entry. If you're logging millions of entries per second through paths like `**.something`, profile before scaling up — `**` is the only pattern that recurses through every key.