@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.
- package/CHANGELOG.md +32 -5
- package/README.md +8 -11
- package/cjs/index.cjs +339 -31
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.d.mts +2 -4
- package/esm/channels/console-log.d.mts.map +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +130 -15
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +204 -23
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/file-name-pattern.d.mts +15 -0
- package/esm/channels/file-name-pattern.d.mts.map +1 -0
- package/esm/channels/file-name-pattern.mjs +41 -0
- package/esm/channels/file-name-pattern.mjs.map +1 -0
- package/esm/channels/index.mjs +2 -2
- package/esm/channels/json-file-log.d.mts +1 -3
- package/esm/channels/json-file-log.d.mts.map +1 -1
- package/esm/channels/json-file-log.mjs +24 -5
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.d.mts +7 -8
- package/esm/channels/sentry-log.d.mts.map +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/index.d.mts +5 -3
- package/esm/index.mjs +3 -2
- package/esm/log-channel.d.mts +23 -3
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +22 -0
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +8 -5
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +12 -3
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/default-keys.d.mts +3 -4
- package/esm/redact/default-keys.d.mts.map +1 -1
- package/esm/redact/escape-log-value.d.mts +25 -0
- package/esm/redact/escape-log-value.d.mts.map +1 -0
- package/esm/redact/escape-log-value.mjs +37 -0
- package/esm/redact/escape-log-value.mjs.map +1 -0
- package/esm/redact/index.mjs +2 -1
- package/esm/redact/mask-email.d.mts +3 -4
- package/esm/redact/mask-email.d.mts.map +1 -1
- package/esm/redact/redact.d.mts +3 -5
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +14 -14
- package/esm/types.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts +2 -3
- package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
- package/esm/utils/clear-message.d.mts +1 -2
- package/esm/utils/clear-message.d.mts.map +1 -1
- package/esm/utils/index.mjs +1 -1
- package/esm/utils/safe-json-stringify.d.mts +1 -2
- package/esm/utils/safe-json-stringify.d.mts.map +1 -1
- package/llms-full.txt +241 -141
- package/llms.txt +2 -2
- package/package.json +2 -2
- package/skills/capture-unhandled-errors/SKILL.md +9 -10
- package/skills/configure-logger/SKILL.md +7 -10
- package/skills/filter-log-entries/SKILL.md +2 -2
- package/skills/flush-logs-on-shutdown/SKILL.md +32 -13
- package/skills/index.md +4 -1
- package/skills/logger-basics/SKILL.md +18 -21
- package/skills/overview/SKILL.md +17 -6
- package/skills/pick-log-channel/SKILL.md +36 -24
- package/skills/redact-sensitive-log-fields/SKILL.md +48 -18
- package/skills/ship-logs-to-sentry/SKILL.md +18 -18
- package/skills/test-logging-code/SKILL.md +4 -2
- package/skills/write-custom-log-channel/SKILL.md +50 -17
package/llms.txt
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
- [capture-unhandled-errors](@warlock.js/logger/capture-unhandled-errors/SKILL.md): 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 — the `flush-logs-on-shutdown` topic; filtering — the `filter-log-entries` topic; competing `Sentry.init`, `@sentry/node`; native `process.on('unhandledRejection')`.
|
|
10
10
|
- [configure-logger](@warlock.js/logger/configure-logger/SKILL.md): 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 — the `pick-log-channel` topic; flushing — the `flush-logs-on-shutdown` topic; redaction — the `redact-sensitive-log-fields` topic; competing libs `winston.createLogger`, `pino`.
|
|
11
11
|
- [filter-log-entries](@warlock.js/logger/filter-log-entries/SKILL.md): 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 — the `write-custom-log-channel` topic; channel picks — the `pick-log-channel` topic; competing libs `pino.levels`, `winston.format.filter`, `debug` env var.
|
|
12
|
-
- [flush-logs-on-shutdown](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md): 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 — the `capture-unhandled-errors` topic; custom sinks — the `write-custom-log-channel` topic; competing `pino.final`, `winston.end`; native `process.on('exit')`.
|
|
12
|
+
- [flush-logs-on-shutdown](@warlock.js/logger/flush-logs-on-shutdown/SKILL.md): 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`, `onChannelError`, `log.flush({ timeout })`, `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 — the `capture-unhandled-errors` topic; custom sinks — the `write-custom-log-channel` topic; competing `pino.final`, `winston.end`; native `process.on('exit')`.
|
|
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): @warlock.js/logger overview for orientation and choosing the focused skill for the task.
|
|
15
15
|
- [pick-log-channel](@warlock.js/logger/pick-log-channel/SKILL.md): 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 — the `ship-logs-to-sentry` topic; custom sinks — the `write-custom-log-channel` topic; registration — the `configure-logger` topic; competing libs `winston-daily-rotate-file`, `pino-pretty`.
|
|
16
|
-
- [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): 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 — the `filter-log-entries` topic; custom sinks — the `write-custom-log-channel` topic; competing libs `pino.redact`, `fast-redact`.
|
|
16
|
+
- [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): 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`, `maskEmail`, `maskEmailsInText`; "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 — the `filter-log-entries` topic; custom sinks — the `write-custom-log-channel` topic; competing libs `pino.redact`, `fast-redact`.
|
|
17
17
|
- [ship-logs-to-sentry](@warlock.js/logger/ship-logs-to-sentry/SKILL.md): Forward log entries to Sentry with the SentryLog channel — error/warn become events (captureException/captureMessage), every other level a breadcrumb (no quota). @sentry/node is an OPTIONAL peer, lazily imported. Triggers: `SentryLog`, `@sentry/node`, `eventLevels`, `flushTimeout`, `Sentry.flush`, `captureException`, `addBreadcrumb`, `withScope`; "send logs to Sentry", "report errors to Sentry", "Sentry log channel", "Sentry breadcrumbs from logs", "log channel for Sentry"; typical import `import { SentryLog } from "@warlock.js/logger"`. Skip: file/console channels — the `pick-log-channel` topic; custom sinks — the `write-custom-log-channel` topic; graceful-shutdown flushing — the `flush-logs-on-shutdown` topic; Slack alerting recipe.
|
|
18
18
|
- [test-logging-code](@warlock.js/logger/test-logging-code/SKILL.md): Test code that touches the logger — silence globally via log.setChannels([]) in setupFiles, assert specific log lines via a capturing LogChannel subclass (prefer it over vi.spyOn — it asserts on delivered entries, not just method calls, and isolates the shared singleton cleanly). Triggers: `log.setChannels`, `LogChannel`, `LoggingData`, `Logger`, `log.channels`; "silence logger in vitest", "assert a log line was emitted", "capture log output in tests", "test code that logs"; typical import `import { log, Logger, LogChannel, type LoggingData } from "@warlock.js/logger"`. Skip: custom sinks — the `write-custom-log-channel` topic; filtering — the `filter-log-entries` topic; competing `vi.spyOn(console)`, `jest.spyOn`.
|
|
19
19
|
- [use-log-helpers](@warlock.js/logger/use-log-helpers/SKILL.md): Two DX shortcuts on every Logger — log.assert(condition, module, action, message, context?) logs an error when condition is falsy (free on the happy path), log.timer(module, action) returns an end-function emitting an info entry with measured duration. Triggers: `log.assert`, `log.timer`, `durationMs`; "assert an invariant via logger", "measure how long an operation took", "time a request", "log operation duration"; typical import `import { log } from "@warlock.js/logger"`. Skip: basics — the `logger-basics` topic; filtering — the `filter-log-entries` topic; competing `console.assert`, `console.time`, `console.timeEnd`, `perf_hooks.performance.now`.
|
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.
|
|
7
|
+
"@warlock.js/fs": "5.32.0",
|
|
8
8
|
"dayjs": "^1.11.9",
|
|
9
9
|
"safe-stable-stringify": "^2.5.0"
|
|
10
10
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"warlock": {
|
|
34
34
|
"environment": "server"
|
|
35
35
|
},
|
|
36
|
-
"version": "5.
|
|
36
|
+
"version": "5.32.0",
|
|
37
37
|
"main": "./cjs/index.cjs",
|
|
38
38
|
"module": "./esm/index.mjs",
|
|
39
39
|
"types": "./esm/index.d.mts",
|
|
@@ -16,6 +16,7 @@ captureAnyUnhandledRejection();
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Registers:
|
|
19
|
+
|
|
19
20
|
- `process.on("unhandledRejection", reason => log.error("app", "unhandledRejection", reason))` — logged; the process is kept alive.
|
|
20
21
|
- `process.on("uncaughtException", error => log.fatal("app", "uncaughtException", error))` — logged, then `process.exit(1)` (by default).
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ The split is intentional: an `uncaughtException` leaves the process in an undefi
|
|
|
23
24
|
|
|
24
25
|
## Why it exits (and why that matters)
|
|
25
26
|
|
|
26
|
-
Registering
|
|
27
|
+
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
|
|
|
28
29
|
- **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
30
|
- **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).
|
|
@@ -35,16 +36,11 @@ Registering *any* `uncaughtException` listener **suppresses** Node's default "pr
|
|
|
35
36
|
**Once**, at startup, **after** channels are registered. Typical place: immediately after your `log.configure({...})` call.
|
|
36
37
|
|
|
37
38
|
```ts title="src/index.ts"
|
|
38
|
-
import {
|
|
39
|
-
log,
|
|
40
|
-
ConsoleLog,
|
|
41
|
-
FileLog,
|
|
42
|
-
captureAnyUnhandledRejection,
|
|
43
|
-
} from "@warlock.js/logger";
|
|
39
|
+
import { log, ConsoleLog, FileLog, captureAnyUnhandledRejection } from "@warlock.js/logger";
|
|
44
40
|
|
|
45
41
|
log.configure({
|
|
46
42
|
channels: [new ConsoleLog(), new FileLog({ levels: ["error"] })],
|
|
47
|
-
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
43
|
+
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"], // ← important; see below
|
|
48
44
|
});
|
|
49
45
|
|
|
50
46
|
captureAnyUnhandledRejection();
|
|
@@ -54,7 +50,7 @@ captureAnyUnhandledRejection();
|
|
|
54
50
|
|
|
55
51
|
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
52
|
|
|
57
|
-
`"beforeExit"` in `autoFlushOn` is still worth setting for the
|
|
53
|
+
`"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 the `flush-logs-on-shutdown` topic.
|
|
58
54
|
|
|
59
55
|
## Idempotency — don't call it twice
|
|
60
56
|
|
|
@@ -79,7 +75,9 @@ import type { LoggingData } from "@warlock.js/logger";
|
|
|
79
75
|
class Capture extends LogChannel {
|
|
80
76
|
public name = "capture";
|
|
81
77
|
public received: LoggingData[] = [];
|
|
82
|
-
public log(data: LoggingData) {
|
|
78
|
+
public log(data: LoggingData) {
|
|
79
|
+
this.received.push({ ...data });
|
|
80
|
+
}
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
it("routes unhandled rejections to the logger", async () => {
|
|
@@ -104,6 +102,7 @@ Testing the **`uncaughtException`** path additionally trips `process.exit(1)`, s
|
|
|
104
102
|
## Module + action the capture uses
|
|
105
103
|
|
|
106
104
|
Both listeners log with:
|
|
105
|
+
|
|
107
106
|
- `module: "app"`
|
|
108
107
|
- `action: "unhandledRejection"` (at `error`) or `action: "uncaughtException"` (at `fatal`)
|
|
109
108
|
- `message`: the rejection reason / exception (keep it as the raw `Error` object — file channels capture the stack).
|
|
@@ -9,10 +9,10 @@ The logger is a singleton. Do all setup in one place, as early in the app entry
|
|
|
9
9
|
|
|
10
10
|
## The three channel-registration methods
|
|
11
11
|
|
|
12
|
-
| Method
|
|
13
|
-
|
|
14
|
-
| `log.addChannel(channel)`
|
|
15
|
-
| `log.setChannels([...])`
|
|
12
|
+
| Method | Semantics |
|
|
13
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
14
|
+
| `log.addChannel(channel)` | **Appends.** Safe to call multiple times. |
|
|
15
|
+
| `log.setChannels([...])` | **Replaces** the full list. |
|
|
16
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
17
|
|
|
18
18
|
All three return `this` — chainable.
|
|
@@ -31,7 +31,7 @@ if (process.env.NODE_ENV === "production") {
|
|
|
31
31
|
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
32
32
|
});
|
|
33
33
|
} else if (process.env.NODE_ENV === "test") {
|
|
34
|
-
log.setChannels([]);
|
|
34
|
+
log.setChannels([]); // silence logger during tests
|
|
35
35
|
} else {
|
|
36
36
|
log.setChannels([new ConsoleLog()]);
|
|
37
37
|
}
|
|
@@ -40,7 +40,7 @@ if (process.env.NODE_ENV === "production") {
|
|
|
40
40
|
Import it once at the top of `src/index.ts`:
|
|
41
41
|
|
|
42
42
|
```ts title="src/index.ts"
|
|
43
|
-
import "./logger";
|
|
43
|
+
import "./logger"; // side-effect: configures singleton
|
|
44
44
|
import { log } from "@warlock.js/logger";
|
|
45
45
|
|
|
46
46
|
log.info("app", "start", "Server listening on :3000");
|
|
@@ -87,10 +87,7 @@ Every `new Logger()` gets a unique `id` (string, prefixed `"logger-"`).
|
|
|
87
87
|
|
|
88
88
|
```ts
|
|
89
89
|
log.configure({
|
|
90
|
-
channels: [
|
|
91
|
-
new ConsoleLog({ showContext: true }),
|
|
92
|
-
new FileLog({ chunk: "daily" }),
|
|
93
|
-
],
|
|
90
|
+
channels: [new ConsoleLog({ showContext: true }), new FileLog({ chunk: "daily" })],
|
|
94
91
|
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
95
92
|
redact: { paths: ["context.password", "context.headers.authorization"] },
|
|
96
93
|
minLevel: process.env.LOG_LEVEL === "debug" ? "debug" : "info",
|
|
@@ -38,7 +38,7 @@ For the common "drop everything below X" case, skip the per-channel `levels` arr
|
|
|
38
38
|
log.setMinLevel("info");
|
|
39
39
|
// debug entries are dropped before fan-out — no channel ever sees them.
|
|
40
40
|
|
|
41
|
-
log.configure({ minLevel: "warn" });
|
|
41
|
+
log.configure({ minLevel: "warn" }); // shorthand inside configure()
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Severity ordering: `debug < info ≈ success < warn < error < fatal`. `success` is treated as informational severity — `setMinLevel("warn")` drops it. `fatal` is strictly above `error`, so `setMinLevel("fatal")` admits only fatal entries (handy for "page me only on fatal" routing).
|
|
@@ -46,7 +46,7 @@ Severity ordering: `debug < info ≈ success < warn < error < fatal`. `success`
|
|
|
46
46
|
Pass `undefined` to clear:
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
log.setMinLevel(undefined);
|
|
49
|
+
log.setMinLevel(undefined); // accept everything again
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
This runs **before** the channel loop — cheaper than per-channel `levels` filters when you want a uniform floor. Per-channel `levels` and `filter` still run on top for channels that need a tighter or differently-shaped rule.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: 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 — the `capture-unhandled-errors` topic; custom sinks — the `write-custom-log-channel` topic; competing `pino.final`, `winston.end`; native `process.on(''exit'')`.'
|
|
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`, `onChannelError`, `log.flush({ timeout })`, `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 — the `capture-unhandled-errors` topic; custom sinks — the `write-custom-log-channel` topic; competing `pino.final`, `winston.end`; native `process.on(''exit'')`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Lifecycle — flushing buffered channels before exit
|
|
@@ -20,14 +20,15 @@ log.configure({
|
|
|
20
20
|
|
|
21
21
|
### What each event does
|
|
22
22
|
|
|
23
|
-
| Event
|
|
24
|
-
|
|
23
|
+
| Event | Behavior |
|
|
24
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
25
25
|
| `SIGINT` / `SIGTERM` / `SIGHUP` / `SIGBREAK` / `SIGUSR2` | Flush → remove this handler → re-raise the signal so Node's default exit code runs (e.g. 130 for SIGINT). |
|
|
26
|
-
| `beforeExit`
|
|
26
|
+
| `beforeExit` | Flush in place. Node continues its natural exit. |
|
|
27
27
|
|
|
28
28
|
### Default recommendation
|
|
29
29
|
|
|
30
30
|
`["SIGINT", "SIGTERM", "beforeExit"]` covers:
|
|
31
|
+
|
|
31
32
|
- Local `Ctrl+C` (SIGINT)
|
|
32
33
|
- Container orchestrators (`docker stop`, Kubernetes sending SIGTERM)
|
|
33
34
|
- Natural exit (Node finished all work)
|
|
@@ -46,7 +47,7 @@ Use this when you need async work (close an HTTP server, drain a queue) **before
|
|
|
46
47
|
async function gracefulShutdown() {
|
|
47
48
|
await httpServer.close();
|
|
48
49
|
await queue.drain();
|
|
49
|
-
log.flushSync();
|
|
50
|
+
log.flushSync(); // still sync — guarantees disk write before exit
|
|
50
51
|
process.exit(0);
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -72,13 +73,13 @@ process.once("SIGTERM", gracefulShutdown);
|
|
|
72
73
|
|
|
73
74
|
`log.flush()` drains every channel concurrently: each waits for its own async deliveries still in flight (level methods are fire-and-forget and never return a promise; entries logged during the flush are awaited too), then runs its `flush()`. A hung network delivery can't hold up the file channel, and one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. It is bounded: `log.flush({ timeout })` resolves after `timeout` ms (default 5000) and reports each channel still draining to `onChannelError`.
|
|
74
75
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
| I/O
|
|
78
|
-
| Safe in a re-raising signal handler | yes
|
|
79
|
-
| Used by `autoFlushOn`
|
|
80
|
-
| Reach for it when
|
|
81
|
-
| `FileLog` / `JSONFileLog`
|
|
76
|
+
| | `flushSync()` | `flush()` |
|
|
77
|
+
| ----------------------------------- | ------------------------------------- | ------------------------------------------------------------ |
|
|
78
|
+
| I/O | synchronous — blocks the loop | asynchronous — awaited |
|
|
79
|
+
| Safe in a re-raising signal handler | yes | no — the signal exits before the promise settles |
|
|
80
|
+
| Used by `autoFlushOn` | yes | no |
|
|
81
|
+
| Reach for it when | file channels, last-resort durability | network/async channels, manual `await` before `process.exit` |
|
|
82
|
+
| `FileLog` / `JSONFileLog` | ✓ | ✓ (async write) |
|
|
82
83
|
|
|
83
84
|
`autoFlushOn` always uses `flushSync()` — signal re-raising can't wait on a promise. If a channel needs async delivery on shutdown, drive `await log.flush()` from your own handler and leave that signal out of `autoFlushOn`.
|
|
84
85
|
|
|
@@ -99,7 +100,7 @@ log.flushSync();
|
|
|
99
100
|
|
|
100
101
|
## Unhandled errors
|
|
101
102
|
|
|
102
|
-
The `uncaughtException` path in `captureAnyUnhandledRejection()` (the `capture-unhandled-errors` topic) already runs a best-effort, time-bounded `log.flush()` before its own `process.exit(1)`, so the fatal entry drains without extra wiring (and `process.exit()` skips `beforeExit`, so a `beforeExit` handler would not fire on that path anyway). Still set `autoFlushOn` for the
|
|
103
|
+
The `uncaughtException` path in `captureAnyUnhandledRejection()` (the `capture-unhandled-errors` topic) already runs a best-effort, time-bounded `log.flush()` before its own `process.exit(1)`, so the fatal entry drains without extra wiring (and `process.exit()` skips `beforeExit`, so a `beforeExit` handler would not fire on that path anyway). Still set `autoFlushOn` for the _other_ shutdown routes — `"SIGINT"` / `"SIGTERM"` and a natural `"beforeExit"` — so those don't lose the last buffered batch.
|
|
103
104
|
|
|
104
105
|
```ts
|
|
105
106
|
log.configure({
|
|
@@ -115,3 +116,21 @@ captureAnyUnhandledRejection();
|
|
|
115
116
|
- **Don't `await` inside a signal handler you wrote yourself and then call `flushSync`** — if an async step rejects, you skip the flush. Wrap in `try { await x } finally { log.flushSync(); process.exit(1); }`.
|
|
116
117
|
- **Don't call `process.exit()` inside `autoFlushOn` handlers** — signal handlers here already re-raise the signal. Forcing an exit breaks exit codes.
|
|
117
118
|
- **Don't rely on the 5-second flush interval for shutdown safety.** It's a throughput optimization, not a durability guarantee.
|
|
119
|
+
|
|
120
|
+
## Observe channel failures and drain explicitly
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { log } from "@warlock.js/logger";
|
|
124
|
+
|
|
125
|
+
log.onChannelError((error, channel) => {
|
|
126
|
+
// Do not call this logger from its own failure handler.
|
|
127
|
+
console.error("Log delivery failed", channel.name, error);
|
|
128
|
+
});
|
|
129
|
+
log.info("app", "shutdown", "Draining logs");
|
|
130
|
+
await log.flush({ timeout: 3_000 });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Level methods return the logger synchronously. `flush()` waits for async
|
|
134
|
+
channel work within its timeout, reports failures through `onChannelError`, and
|
|
135
|
+
resolves so shutdown can continue. Timeout does not cancel a channel's network
|
|
136
|
+
operation; its driver owns cancellation.
|
package/skills/index.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
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
3
|
---
|
|
4
|
+
|
|
4
5
|
# @warlock.js/logger
|
|
5
6
|
|
|
6
7
|
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
|
|
|
8
9
|
## The 80% path
|
|
10
|
+
|
|
9
11
|
1. Orient: `overview.md`, `logger-basics.md`.
|
|
10
12
|
2. Pick channels: `pick-log-channel.md`; set them up: `configure-logger.md`.
|
|
11
13
|
3. Log via `log.info/warn/error` or helpers: `use-log-helpers.md`.
|
|
@@ -14,6 +16,7 @@ The `log` singleton (a `Logger`) sends each entry, with module, action, level an
|
|
|
14
16
|
6. Extend or test: `write-custom-log-channel.md`, `test-logging-code.md`.
|
|
15
17
|
|
|
16
18
|
## Conventions and pitfalls
|
|
19
|
+
|
|
17
20
|
- `fatal` does not flush or exit by itself; the caller awaits `log.flush()` and then exits.
|
|
18
21
|
- File channels buffer, so an unflushed process exit loses the tail of the log.
|
|
19
22
|
- Configure redaction before logging request bodies or auth data; the default key set is a floor, not a guarantee.
|
|
@@ -37,10 +37,7 @@ The 11 things that are true in every logger use:
|
|
|
37
37
|
import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
|
|
38
38
|
|
|
39
39
|
log.configure({
|
|
40
|
-
channels: [
|
|
41
|
-
new ConsoleLog(),
|
|
42
|
-
new FileLog({ chunk: "daily", storagePath: "./storage/logs" }),
|
|
43
|
-
],
|
|
40
|
+
channels: [new ConsoleLog(), new FileLog({ chunk: "daily", storagePath: "./storage/logs" })],
|
|
44
41
|
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
45
42
|
});
|
|
46
43
|
|
|
@@ -51,12 +48,12 @@ log.error("payments", "charge", new Error("Card declined"));
|
|
|
51
48
|
## The six levels
|
|
52
49
|
|
|
53
50
|
```ts
|
|
54
|
-
log.debug("module", "action", "verbose detail");
|
|
55
|
-
log.info("module", "action", "neutral event");
|
|
56
|
-
log.warn("module", "action", "something off");
|
|
57
|
-
log.error("module", "action", error);
|
|
58
|
-
log.success("module", "action", "operation done");
|
|
59
|
-
log.fatal("module", "action", error);
|
|
51
|
+
log.debug("module", "action", "verbose detail"); // dev-only diagnostics
|
|
52
|
+
log.info("module", "action", "neutral event"); // user-visible event
|
|
53
|
+
log.warn("module", "action", "something off"); // recoverable concern
|
|
54
|
+
log.error("module", "action", error); // handled failure, app continues
|
|
55
|
+
log.success("module", "action", "operation done"); // explicit success
|
|
56
|
+
log.fatal("module", "action", error); // unrecoverable, app is going down
|
|
60
57
|
```
|
|
61
58
|
|
|
62
59
|
`fatal` is purely informational — it does NOT auto-flush or exit. The caller decides whether to `await log.flush()` and `process.exit(...)`. See the `capture-unhandled-errors` topic for the `uncaughtException` → `fatal` routing.
|
|
@@ -65,17 +62,17 @@ Every call signature is the same — `module`, `action`, `message`, optional `co
|
|
|
65
62
|
|
|
66
63
|
## Pick a skill
|
|
67
64
|
|
|
68
|
-
| If the task is about…
|
|
69
|
-
|
|
|
70
|
-
| Picking a channel — what each built-in does, when to use which
|
|
71
|
-
| Startup — registering channels, environment-based setup, the `configure` method | the `configure-logger` topic
|
|
72
|
-
| Filtering log output (`levels`, `filter`, per-channel routing, `minLevel`)
|
|
73
|
-
| Graceful shutdown — `flushSync`, `autoFlushOn`, signal behavior
|
|
74
|
-
| Extending `LogChannel` to build a custom sink (Slack, database, HTTP)
|
|
75
|
-
| Routing Node's `unhandledRejection` / `uncaughtException` through the logger
|
|
76
|
-
| `log.assert(...)` and `log.timer(...)` shorthand helpers
|
|
77
|
-
| Redacting secrets — logger floor + additive channel paths
|
|
78
|
-
| Tests that assert on log output, or code under test that logs
|
|
65
|
+
| If the task is about… | Load |
|
|
66
|
+
| ------------------------------------------------------------------------------- | --------------------------------------- |
|
|
67
|
+
| Picking a channel — what each built-in does, when to use which | the `pick-log-channel` topic |
|
|
68
|
+
| Startup — registering channels, environment-based setup, the `configure` method | the `configure-logger` topic |
|
|
69
|
+
| Filtering log output (`levels`, `filter`, per-channel routing, `minLevel`) | the `filter-log-entries` topic |
|
|
70
|
+
| Graceful shutdown — `flushSync`, `autoFlushOn`, signal behavior | the `flush-logs-on-shutdown` topic |
|
|
71
|
+
| Extending `LogChannel` to build a custom sink (Slack, database, HTTP) | the `write-custom-log-channel` topic |
|
|
72
|
+
| Routing Node's `unhandledRejection` / `uncaughtException` through the logger | the `capture-unhandled-errors` topic |
|
|
73
|
+
| `log.assert(...)` and `log.timer(...)` shorthand helpers | the `use-log-helpers` topic |
|
|
74
|
+
| Redacting secrets — logger floor + additive channel paths | the `redact-sensitive-log-fields` topic |
|
|
75
|
+
| Tests that assert on log output, or code under test that logs | the `test-logging-code` topic |
|
|
79
76
|
|
|
80
77
|
## Things NOT to do
|
|
81
78
|
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -32,52 +32,63 @@ Eleven task skills cover everything. Load the one that matches your job — most
|
|
|
32
32
|
### Foundations
|
|
33
33
|
|
|
34
34
|
#### `logger-basics`
|
|
35
|
+
|
|
35
36
|
Start here. The `log` singleton, the six levels (`debug` / `info` / `warn` / `error` / `success` / `fatal`), how fan-out works, the `module / action / message / context` shape every entry carries.
|
|
36
37
|
|
|
37
38
|
#### `configure-logger`
|
|
39
|
+
|
|
38
40
|
Wire channels at boot — `log.addChannel`, `log.setChannels`, `log.configure({ channels, autoFlushOn, redact, minLevel })`. Branch on `NODE_ENV`, replace the channel list, isolate a library's logger from the host singleton.
|
|
39
41
|
|
|
40
42
|
### Channels
|
|
41
43
|
|
|
42
44
|
#### `pick-log-channel`
|
|
45
|
+
|
|
43
46
|
Pick one of the four built-ins: `ConsoleLog` (terminal, colored), `FileLog` (plain `.log` on disk with rotation), `JSONFileLog` (structured JSON for aggregators — Datadog, Loki, ELK), `SentryLog` (errors + breadcrumbs to Sentry; `@sentry/node` is an optional peer).
|
|
44
47
|
|
|
45
48
|
#### `ship-logs-to-sentry`
|
|
49
|
+
|
|
46
50
|
The `SentryLog` channel in depth — event-vs-breadcrumb level mapping, dual init modes (reuse an existing client or pass `options`), draining via `Sentry.flush(timeout)`, behavior when the optional peer isn't installed.
|
|
47
51
|
|
|
48
52
|
#### `write-custom-log-channel`
|
|
53
|
+
|
|
49
54
|
Extend `LogChannel<Options>` for sinks the built-ins don't cover — Slack, HTTP endpoint, in-memory buffer, database. The lazy `init()` lifecycle (`setTimeout(0)`), the `terminal: true/false` ANSI-stripping behavior, and the difference between `flush()` (async, network) and `flushSync()` (sync, files) are subtle — read this skill before subclassing.
|
|
50
55
|
|
|
51
56
|
### Production concerns
|
|
52
57
|
|
|
53
58
|
#### `redact-sensitive-log-fields`
|
|
59
|
+
|
|
54
60
|
Strip secrets before they reach a sink. Logger-wide `setRedact({ paths, censor })` is the security floor; per-channel `redact` configs add paths (never remove). Dotted-glob paths (`*`, `**`); censor as string or function `(value, path) => any`.
|
|
55
61
|
|
|
56
62
|
#### `filter-log-entries`
|
|
63
|
+
|
|
57
64
|
Drop entries before they cost anything. Logger-wide `setMinLevel("info")` is the fast path; per-channel `levels` array + `filter` predicate for fine control.
|
|
58
65
|
|
|
59
66
|
#### `flush-logs-on-shutdown`
|
|
67
|
+
|
|
60
68
|
Buffered channels need explicit drain. `log.flushSync()` (sync) for file channels — also wired by `enableAutoFlush(['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK', 'SIGUSR2', 'beforeExit'])`. `await log.flush()` (async) for network/async channels like `SentryLog` — the only path that can await an HTTPS round-trip on a graceful shutdown.
|
|
61
69
|
|
|
62
70
|
#### `capture-unhandled-errors`
|
|
71
|
+
|
|
63
72
|
`captureAnyUnhandledRejection()` hooks `unhandledRejection` (→ `log.error("app", ...)`, process kept alive) and `uncaughtException` (→ `log.fatal("app", ...)` then `process.exit(1)` — restoring the non-zero exit the listener would otherwise suppress, so a fatal crash never becomes a silent `exit 0`; opt out with `{ exitOnUncaughtException: false }`). It flushes before exiting and falls back to `console.error` when no terminal channel is set. One call at startup.
|
|
64
73
|
|
|
65
74
|
### Ergonomics + testing
|
|
66
75
|
|
|
67
76
|
#### `use-log-helpers`
|
|
77
|
+
|
|
68
78
|
Two shortcuts every `Logger` exposes: `log.assert(condition, module, action, message, context?)` logs an error only when the condition is falsy (free on the happy path); `log.timer(module, action)` returns an end-function that emits `info` with a measured `durationMs`.
|
|
69
79
|
|
|
70
80
|
#### `test-logging-code`
|
|
81
|
+
|
|
71
82
|
Silence the logger globally in tests via `log.setChannels([])` in `setupFiles`. Assert specific entries with a capturing `LogChannel` subclass — it proves an entry was actually delivered through the pipeline (filters, redaction), not merely that a method was called, and it isolates cleanly by swapping `log.channels`.
|
|
72
83
|
|
|
73
84
|
## Built-in channels at a glance
|
|
74
85
|
|
|
75
|
-
| Channel
|
|
76
|
-
|
|
|
77
|
-
| `ConsoleLog`
|
|
78
|
-
| `FileLog`
|
|
79
|
-
| `JSONFileLog` | `.json` files
|
|
80
|
-
| `SentryLog`
|
|
86
|
+
| Channel | Sink | `terminal` | Buffered |
|
|
87
|
+
| ------------- | --------------------------- | ----------------------- | ----------------------------------------------------------------------- |
|
|
88
|
+
| `ConsoleLog` | `process.stdout` | `true` (colors kept) | no |
|
|
89
|
+
| `FileLog` | `.log` files | `false` (ANSI stripped) | yes (5s timer or 100-entry buffer) |
|
|
90
|
+
| `JSONFileLog` | `.json` files | `false` (ANSI stripped) | yes (same buffering) |
|
|
91
|
+
| `SentryLog` | Sentry events + breadcrumbs | `false` | via the Sentry SDK's own transport (`@sentry/node` is an optional peer) |
|
|
81
92
|
|
|
82
93
|
`terminal: true` is the flag that decides whether the channel sees raw colored messages or stripped plain text. Custom channels: pick `true` if you write to a terminal, `false` for anything else.
|
|
83
94
|
|
|
@@ -9,12 +9,12 @@ Four built-in channels (`SentryLog` needs the optional `@sentry/node` peer). A c
|
|
|
9
9
|
|
|
10
10
|
## The decision
|
|
11
11
|
|
|
12
|
-
| Need
|
|
13
|
-
|
|
14
|
-
| Local dev, colored output in the terminal
|
|
15
|
-
| Plain text `.log` files on disk — humans read them
|
|
12
|
+
| Need | Pick |
|
|
13
|
+
| --------------------------------------------------------------------------------- | ------------- |
|
|
14
|
+
| Local dev, colored output in the terminal | `ConsoleLog` |
|
|
15
|
+
| Plain text `.log` files on disk — humans read them | `FileLog` |
|
|
16
16
|
| Structured `.json` files — a log aggregator (Loki / Datadog / Elastic) reads them | `JSONFileLog` |
|
|
17
|
-
| Errors & warnings into Sentry (events + breadcrumbs)
|
|
17
|
+
| Errors & warnings into Sentry (events + breadcrumbs) | `SentryLog` |
|
|
18
18
|
|
|
19
19
|
Most production setups use **two** channels: `ConsoleLog` + one file channel. Dev uses `ConsoleLog` only.
|
|
20
20
|
|
|
@@ -32,6 +32,7 @@ new ConsoleLog();
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Properties:
|
|
35
|
+
|
|
35
36
|
- `name = "console"`, `terminal = true`
|
|
36
37
|
- Accepts `ConsoleLogConfig` — `levels`, `filter`, `dateFormat`, `showContext`, `contextDepth`
|
|
37
38
|
- If `message` is an object, a second `console.log(message)` is issued so Node's inspector can expand it
|
|
@@ -58,14 +59,26 @@ Plain text. Buffers in memory, flushes to disk periodically.
|
|
|
58
59
|
import { FileLog } from "@warlock.js/logger";
|
|
59
60
|
|
|
60
61
|
new FileLog({
|
|
61
|
-
storagePath: "./storage/logs",
|
|
62
|
-
name: "app",
|
|
63
|
-
extension: "log",
|
|
64
|
-
chunk: "daily",
|
|
65
|
-
rotate: true,
|
|
66
|
-
maxFileSize: 10 * 1024 * 1024,
|
|
67
|
-
maxMessagesToWrite: 100,
|
|
68
|
-
groupBy: ["level", "module"],
|
|
62
|
+
storagePath: "./storage/logs", // default: process.cwd() + "/storage/logs"
|
|
63
|
+
name: "app", // default: "app"
|
|
64
|
+
extension: "log", // default: "log"
|
|
65
|
+
chunk: "daily", // "single" (default) | "daily" | "hourly"
|
|
66
|
+
rotate: true, // default: true
|
|
67
|
+
maxFileSize: 10 * 1024 * 1024, // default: 10MB — triggers rotation
|
|
68
|
+
maxMessagesToWrite: 100, // default: 100 — flush threshold
|
|
69
|
+
groupBy: ["level", "module"], // optional subdirectory nesting
|
|
70
|
+
retention: { maxFiles: 14, maxAgeDays: 30 }, // optional pruning of old files
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Name dated files yourself with `fileName` — a pattern (`{YYYY}` `{MM}` `{DD}` `{HH}` `{mm}` `{name}`, local time) or a function of the date:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
new FileLog({ storagePath: "./storage/logs", fileName: "{name}-{YYYY}-{MM}-{DD}" }); // app-2026-10-17.log
|
|
78
|
+
new FileLog({
|
|
79
|
+
storagePath: "./storage/logs",
|
|
80
|
+
fileName: (date) => `audit-${date.getFullYear()}`,
|
|
81
|
+
retention: { maxFiles: 5, match: /^audit-\d{4}\.log$/ }, // a function fileName needs `match`
|
|
69
82
|
});
|
|
70
83
|
```
|
|
71
84
|
|
|
@@ -74,7 +87,8 @@ Line format: `[date time] [level] [module][action]: message` — or a `[trace]`
|
|
|
74
87
|
### Key gotchas
|
|
75
88
|
|
|
76
89
|
- **Buffers!** Messages sit in memory until either `maxMessagesToWrite` is reached, 5 seconds pass, or `flushSync()` is called. A process that crashes without flushing loses buffered entries.
|
|
77
|
-
- **`chunk: "daily"` picks a filename per day.** File name becomes `DD-MM-
|
|
90
|
+
- **`chunk: "daily"` picks a filename per day.** File name becomes `YYYY-MM-DD.log` (hourly: `YYYY-MM-DD-HH.log`), ignoring `name` unless `fileName` uses `{name}`. Since 5.31; keep the old `DD-MM-YYYY` with `fileName: "{DD}-{MM}-{YYYY}"`. Rotated archives are `<file name>-<rotateFileName>-<epoch ms>.<ext>`; `rotateFileName` is a pattern or function too (default `{YYYY}-{MM}-{DD}`).
|
|
91
|
+
- **`retention` only touches this channel's files.** On boot and whenever a new file starts it removes files in the log directory (each group directory with `groupBy`) whose names match the channel's pattern — beyond `maxFiles` (newest by modification time, the current file counts) or older than `maxAgeDays`. Failures go to `log.onChannelError(...)`, never to the call site. `await channel.prune()` runs it on demand.
|
|
78
92
|
- **`groupBy` nests directories.** `groupBy: ["level", "module"]` produces `storage/logs/error/payments/app.log`. Order matters.
|
|
79
93
|
- **Dispose channels you discard.** A live `FileLog` keeps a 5-second flush interval running. If you swap the channel list at runtime (reconfigure the logger), call `channel.dispose()` on the old instance — it clears that timer and drains the buffer one last time. Skipping it leaks one timer per discarded channel and keeps the event loop alive. (Channels that live for the whole process don't need this — process exit clears the timer.)
|
|
80
94
|
|
|
@@ -91,16 +105,14 @@ Subclass of `FileLog` — same buffering, chunking, rotation, grouping. Output i
|
|
|
91
105
|
"date": "15-03-2024 10:22:03",
|
|
92
106
|
"module": "payments",
|
|
93
107
|
"action": "charge",
|
|
94
|
-
"stack": [
|
|
95
|
-
"Error: Card declined",
|
|
96
|
-
" at chargeCard (/app/src/payments.ts:42:11)"
|
|
97
|
-
]
|
|
108
|
+
"stack": ["Error: Card declined", " at chargeCard (/app/src/payments.ts:42:11)"]
|
|
98
109
|
}
|
|
99
110
|
]
|
|
100
111
|
}
|
|
101
112
|
```
|
|
102
113
|
|
|
103
114
|
Differences from `FileLog`:
|
|
115
|
+
|
|
104
116
|
- `name = "fileJson"` (**not** `"json"` — use this exact string for `log.channel("fileJson")`)
|
|
105
117
|
- `extension` is always `"json"` — the option is silently ignored
|
|
106
118
|
- Error `stack` is stored as `string[]` (split on newlines) — easy to query in aggregators
|
|
@@ -127,8 +139,8 @@ Every channel constructor accepts at minimum:
|
|
|
127
139
|
|
|
128
140
|
```ts
|
|
129
141
|
type BasicLogConfigurations = {
|
|
130
|
-
levels?: LogLevel[];
|
|
131
|
-
filter?: (data: LoggingData) => boolean;
|
|
142
|
+
levels?: LogLevel[]; // whitelist — omit or [] to allow all
|
|
143
|
+
filter?: (data: LoggingData) => boolean; // custom predicate
|
|
132
144
|
dateFormat?: { date?: string; time?: string }; // Day.js format strings
|
|
133
145
|
context?: (data) => Promise<Record<string, any>>; // reserved — not yet read
|
|
134
146
|
};
|
|
@@ -139,10 +151,10 @@ Concrete file channels extend this with their storage/chunk/rotate/groupBy optio
|
|
|
139
151
|
## Picking a channel by name at runtime
|
|
140
152
|
|
|
141
153
|
```ts
|
|
142
|
-
log.channel("console");
|
|
143
|
-
log.channel("file");
|
|
144
|
-
log.channel("fileJson");
|
|
145
|
-
log.channel("sentry");
|
|
154
|
+
log.channel("console"); // → ConsoleLog | undefined
|
|
155
|
+
log.channel("file"); // → FileLog | undefined
|
|
156
|
+
log.channel("fileJson"); // ← note the name — NOT "json"
|
|
157
|
+
log.channel("sentry"); // → SentryLog | undefined
|
|
146
158
|
```
|
|
147
159
|
|
|
148
160
|
If two channels share a `name`, only one is reachable this way — the search returns the first match.
|