@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/CHANGELOG.md +6 -0
- package/README.md +153 -153
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.mjs.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/esm/utils/clear-message.mjs.map +1 -1
- package/llms-full.txt +1413 -1413
- package/package.json +2 -2
- package/skills/capture-unhandled-errors/SKILL.md +111 -111
- package/skills/configure-logger/SKILL.md +105 -105
- package/skills/filter-log-entries/SKILL.md +120 -120
- package/skills/flush-logs-on-shutdown/SKILL.md +117 -117
- package/skills/overview/SKILL.md +93 -93
- package/skills/pick-log-channel/SKILL.md +155 -155
- package/skills/redact-sensitive-log-fields/SKILL.md +156 -156
- package/skills/ship-logs-to-sentry/SKILL.md +118 -118
- package/skills/test-logging-code/SKILL.md +177 -177
- package/skills/use-log-helpers/SKILL.md +66 -66
- package/skills/write-custom-log-channel/SKILL.md +190 -190
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: 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
|
-
---
|
|
5
|
-
|
|
6
|
-
# Filtering — `levels` + `filter` predicate + `minLevel`
|
|
7
|
-
|
|
8
|
-
Every channel can silently drop entries it doesn't care about. Three mechanisms stack: a logger-wide `minLevel` floor (cheapest), then per-channel `levels` whitelist, then per-channel `filter` predicate.
|
|
9
|
-
|
|
10
|
-
## 1. `levels` — the per-channel whitelist
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
new FileLog({ levels: ["error", "warn"] });
|
|
14
|
-
// debug/info/success entries → skipped
|
|
15
|
-
// error/warn entries → written
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
- Omitting `levels` (or passing `[]`) means **allow all six**.
|
|
19
|
-
- No regex / no range — it's a literal whitelist of `LogLevel` strings.
|
|
20
|
-
|
|
21
|
-
## 2. `filter` — the per-channel custom predicate
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
new ConsoleLog({
|
|
25
|
-
filter: (data) => data.module !== "healthcheck",
|
|
26
|
-
});
|
|
27
|
-
// Every entry is passed to the predicate; return false → skip.
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- `data` is the full `LoggingData`: `{ type, module, action, message, context? }`.
|
|
31
|
-
- Predicate runs **after** `levels` — an entry blocked by `levels` never reaches `filter`.
|
|
32
|
-
|
|
33
|
-
## 3. `minLevel` — the logger-wide severity floor
|
|
34
|
-
|
|
35
|
-
For the common "drop everything below X" case, skip the per-channel `levels` array and use the logger-wide fast path:
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
log.setMinLevel("info");
|
|
39
|
-
// debug entries are dropped before fan-out — no channel ever sees them.
|
|
40
|
-
|
|
41
|
-
log.configure({ minLevel: "warn" }); // shorthand inside configure()
|
|
42
|
-
```
|
|
43
|
-
|
|
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).
|
|
45
|
-
|
|
46
|
-
Pass `undefined` to clear:
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
log.setMinLevel(undefined); // accept everything again
|
|
50
|
-
```
|
|
51
|
-
|
|
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.
|
|
53
|
-
|
|
54
|
-
## Combining — real patterns
|
|
55
|
-
|
|
56
|
-
### Route errors to a dedicated file
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
log.setChannels([
|
|
60
|
-
new ConsoleLog(),
|
|
61
|
-
new FileLog({
|
|
62
|
-
name: "errors",
|
|
63
|
-
levels: ["error", "warn"],
|
|
64
|
-
chunk: "daily",
|
|
65
|
-
}),
|
|
66
|
-
]);
|
|
67
|
-
// ConsoleLog sees everything; errors.log only grows with warnings and errors.
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Silence a noisy module
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
new ConsoleLog({
|
|
74
|
-
filter: (data) => data.module !== "socket.io",
|
|
75
|
-
});
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Keep the dev terminal focused
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
// Only surface the subsystem you're actively working on
|
|
82
|
-
new ConsoleLog({
|
|
83
|
-
filter: (data) => data.module === "auth",
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Errors always pass, info only for one module
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
new ConsoleLog({
|
|
91
|
-
filter: (data) => data.type === "error" || data.module === "payments",
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Where filtering happens
|
|
96
|
-
|
|
97
|
-
`LogChannel.shouldBeLogged(data)` runs both checks in order:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// levels check — fast path
|
|
101
|
-
if (this.config("levels")?.length && !this.config("levels").includes(data.type)) return false;
|
|
102
|
-
|
|
103
|
-
// filter predicate — only runs if levels allowed it
|
|
104
|
-
const filter = this.config("filter");
|
|
105
|
-
if (filter) return filter(data);
|
|
106
|
-
|
|
107
|
-
return true;
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
If you extend `LogChannel` to write a custom channel, call `this.shouldBeLogged(data)` first thing inside your `log(data)` method — you inherit both mechanisms for free. See [`@warlock.js/logger/write-custom-log-channel/SKILL.md`](@warlock.js/logger/write-custom-log-channel/SKILL.md).
|
|
111
|
-
|
|
112
|
-
## Logger-wide custom filtering — not a thing
|
|
113
|
-
|
|
114
|
-
There is no `logger.setGlobalFilter()`. Each channel filters itself. If you want the same predicate everywhere, pass it to every channel constructor (or wrap your channels in a helper).
|
|
115
|
-
|
|
116
|
-
## Performance note
|
|
117
|
-
|
|
118
|
-
Filters run on **every** entry per channel. A synchronous, cheap predicate is fine. Avoid `await` inside — the channel receives a fully-formed `LoggingData` and the filter is sync-only (type: `(data: LoggingData) => boolean`).
|
|
119
|
-
|
|
120
|
-
The `minLevel` check is the fastest of the three (single comparison before fan-out), so prefer it when "drop everything below X uniformly" matches your need.
|
|
1
|
+
---
|
|
2
|
+
name: 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
|
+
---
|
|
5
|
+
|
|
6
|
+
# Filtering — `levels` + `filter` predicate + `minLevel`
|
|
7
|
+
|
|
8
|
+
Every channel can silently drop entries it doesn't care about. Three mechanisms stack: a logger-wide `minLevel` floor (cheapest), then per-channel `levels` whitelist, then per-channel `filter` predicate.
|
|
9
|
+
|
|
10
|
+
## 1. `levels` — the per-channel whitelist
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
new FileLog({ levels: ["error", "warn"] });
|
|
14
|
+
// debug/info/success entries → skipped
|
|
15
|
+
// error/warn entries → written
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Omitting `levels` (or passing `[]`) means **allow all six**.
|
|
19
|
+
- No regex / no range — it's a literal whitelist of `LogLevel` strings.
|
|
20
|
+
|
|
21
|
+
## 2. `filter` — the per-channel custom predicate
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
new ConsoleLog({
|
|
25
|
+
filter: (data) => data.module !== "healthcheck",
|
|
26
|
+
});
|
|
27
|
+
// Every entry is passed to the predicate; return false → skip.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `data` is the full `LoggingData`: `{ type, module, action, message, context? }`.
|
|
31
|
+
- Predicate runs **after** `levels` — an entry blocked by `levels` never reaches `filter`.
|
|
32
|
+
|
|
33
|
+
## 3. `minLevel` — the logger-wide severity floor
|
|
34
|
+
|
|
35
|
+
For the common "drop everything below X" case, skip the per-channel `levels` array and use the logger-wide fast path:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
log.setMinLevel("info");
|
|
39
|
+
// debug entries are dropped before fan-out — no channel ever sees them.
|
|
40
|
+
|
|
41
|
+
log.configure({ minLevel: "warn" }); // shorthand inside configure()
|
|
42
|
+
```
|
|
43
|
+
|
|
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).
|
|
45
|
+
|
|
46
|
+
Pass `undefined` to clear:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
log.setMinLevel(undefined); // accept everything again
|
|
50
|
+
```
|
|
51
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
## Combining — real patterns
|
|
55
|
+
|
|
56
|
+
### Route errors to a dedicated file
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
log.setChannels([
|
|
60
|
+
new ConsoleLog(),
|
|
61
|
+
new FileLog({
|
|
62
|
+
name: "errors",
|
|
63
|
+
levels: ["error", "warn"],
|
|
64
|
+
chunk: "daily",
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
// ConsoleLog sees everything; errors.log only grows with warnings and errors.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Silence a noisy module
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
new ConsoleLog({
|
|
74
|
+
filter: (data) => data.module !== "socket.io",
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Keep the dev terminal focused
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Only surface the subsystem you're actively working on
|
|
82
|
+
new ConsoleLog({
|
|
83
|
+
filter: (data) => data.module === "auth",
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Errors always pass, info only for one module
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
new ConsoleLog({
|
|
91
|
+
filter: (data) => data.type === "error" || data.module === "payments",
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Where filtering happens
|
|
96
|
+
|
|
97
|
+
`LogChannel.shouldBeLogged(data)` runs both checks in order:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// levels check — fast path
|
|
101
|
+
if (this.config("levels")?.length && !this.config("levels").includes(data.type)) return false;
|
|
102
|
+
|
|
103
|
+
// filter predicate — only runs if levels allowed it
|
|
104
|
+
const filter = this.config("filter");
|
|
105
|
+
if (filter) return filter(data);
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If you extend `LogChannel` to write a custom channel, call `this.shouldBeLogged(data)` first thing inside your `log(data)` method — you inherit both mechanisms for free. See [`@warlock.js/logger/write-custom-log-channel/SKILL.md`](@warlock.js/logger/write-custom-log-channel/SKILL.md).
|
|
111
|
+
|
|
112
|
+
## Logger-wide custom filtering — not a thing
|
|
113
|
+
|
|
114
|
+
There is no `logger.setGlobalFilter()`. Each channel filters itself. If you want the same predicate everywhere, pass it to every channel constructor (or wrap your channels in a helper).
|
|
115
|
+
|
|
116
|
+
## Performance note
|
|
117
|
+
|
|
118
|
+
Filters run on **every** entry per channel. A synchronous, cheap predicate is fine. Avoid `await` inside — the channel receives a fully-formed `LoggingData` and the filter is sync-only (type: `(data: LoggingData) => boolean`).
|
|
119
|
+
|
|
120
|
+
The `minLevel` check is the fastest of the three (single comparison before fan-out), so prefer it when "drop everything below X uniformly" matches your need.
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
---
|
|
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 — `@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
|
-
---
|
|
5
|
-
|
|
6
|
-
# Lifecycle — flushing buffered channels before exit
|
|
7
|
-
|
|
8
|
-
`FileLog` and `JSONFileLog` buffer entries in memory. A process that exits without draining loses the buffer.
|
|
9
|
-
|
|
10
|
-
## The easy way — `autoFlushOn`
|
|
11
|
-
|
|
12
|
-
Tell the logger which process events should trigger a flush. It installs the handlers for you.
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
log.configure({
|
|
16
|
-
channels: [new ConsoleLog(), new FileLog({ chunk: "daily" })],
|
|
17
|
-
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
18
|
-
});
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### What each event does
|
|
22
|
-
|
|
23
|
-
| Event | Behavior |
|
|
24
|
-
|---|---|
|
|
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` | Flush in place. Node continues its natural exit. |
|
|
27
|
-
|
|
28
|
-
### Default recommendation
|
|
29
|
-
|
|
30
|
-
`["SIGINT", "SIGTERM", "beforeExit"]` covers:
|
|
31
|
-
- Local `Ctrl+C` (SIGINT)
|
|
32
|
-
- Container orchestrators (`docker stop`, Kubernetes sending SIGTERM)
|
|
33
|
-
- Natural exit (Node finished all work)
|
|
34
|
-
|
|
35
|
-
Add `"SIGHUP"` if you care about terminal disconnects. Add `"SIGUSR2"` if you use nodemon or pm2 restart.
|
|
36
|
-
|
|
37
|
-
### Idempotency
|
|
38
|
-
|
|
39
|
-
Calling `enableAutoFlush` twice **replaces** previous handlers — it does not stack. `disableAutoFlush()` removes every handler this logger instance registered; safe to call when nothing is registered.
|
|
40
|
-
|
|
41
|
-
## The manual way — your own handler
|
|
42
|
-
|
|
43
|
-
Use this when you need async work (close an HTTP server, drain a queue) **before** flushing:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
async function gracefulShutdown() {
|
|
47
|
-
await httpServer.close();
|
|
48
|
-
await queue.drain();
|
|
49
|
-
log.flushSync(); // still sync — guarantees disk write before exit
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
process.once("SIGINT", gracefulShutdown);
|
|
54
|
-
process.once("SIGTERM", gracefulShutdown);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**If you go manual for a signal, skip it in `autoFlushOn`** — otherwise both handlers fire and ours re-raises the signal mid-way through your async work.
|
|
58
|
-
|
|
59
|
-
## Async drain — `log.flush()`
|
|
60
|
-
|
|
61
|
-
`flushSync()` blocks the event loop with synchronous I/O. That's correct for the file channels and required inside the handlers `autoFlushOn` installs (a re-raised signal kills the process before any promise could settle). But a channel whose delivery is **async** — a network transport, an async disk write — can't drain synchronously. For those, `await` the async sibling on a graceful path you control:
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
async function gracefulShutdown() {
|
|
65
|
-
await httpServer.close();
|
|
66
|
-
await log.flush(); // awaits every channel's async flush() to completion
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
process.once("SIGTERM", gracefulShutdown);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
`log.flush()` fans out to every channel that implements `flush()` and awaits them together (`Promise.allSettled`). Each channel is isolated — one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. Channels without `flush()` are skipped.
|
|
74
|
-
|
|
75
|
-
| | `flushSync()` | `flush()` |
|
|
76
|
-
|---|---|---|
|
|
77
|
-
| I/O | synchronous — blocks the loop | asynchronous — awaited |
|
|
78
|
-
| Safe in a re-raising signal handler | yes | no — the signal exits before the promise settles |
|
|
79
|
-
| Used by `autoFlushOn` | yes | no |
|
|
80
|
-
| Reach for it when | file channels, last-resort durability | network/async channels, manual `await` before `process.exit` |
|
|
81
|
-
| `FileLog` / `JSONFileLog` | ✓ | ✓ (async write) |
|
|
82
|
-
|
|
83
|
-
`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
|
-
## What `flushSync()` actually does
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
log.flushSync();
|
|
89
|
-
// For every registered channel:
|
|
90
|
-
// if (channel.flushSync) channel.flushSync();
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- Synchronous I/O — blocks the event loop.
|
|
94
|
-
- Channels without `flushSync` (e.g. `ConsoleLog` — nothing to flush) are skipped silently.
|
|
95
|
-
- Works with and without `groupBy` on `FileLog` / `JSONFileLog`.
|
|
96
|
-
- No-op if every channel's buffer is empty.
|
|
97
|
-
|
|
98
|
-
`ConsoleLog` has no `flushSync` — it writes synchronously on every entry. `FileLog` and `JSONFileLog` both implement it.
|
|
99
|
-
|
|
100
|
-
## Unhandled errors
|
|
101
|
-
|
|
102
|
-
The `uncaughtException` path in [`captureAnyUnhandledRejection()`](@warlock.js/logger/capture-unhandled-errors/SKILL.md) 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
|
-
```ts
|
|
105
|
-
log.configure({
|
|
106
|
-
channels: [new FileLog({ levels: ["error"] })],
|
|
107
|
-
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
captureAnyUnhandledRejection();
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## What NOT to do
|
|
114
|
-
|
|
115
|
-
- **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
|
-
- **Don't call `process.exit()` inside `autoFlushOn` handlers** — signal handlers here already re-raise the signal. Forcing an exit breaks exit codes.
|
|
117
|
-
- **Don't rely on the 5-second flush interval for shutdown safety.** It's a throughput optimization, not a durability guarantee.
|
|
1
|
+
---
|
|
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 — `@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
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lifecycle — flushing buffered channels before exit
|
|
7
|
+
|
|
8
|
+
`FileLog` and `JSONFileLog` buffer entries in memory. A process that exits without draining loses the buffer.
|
|
9
|
+
|
|
10
|
+
## The easy way — `autoFlushOn`
|
|
11
|
+
|
|
12
|
+
Tell the logger which process events should trigger a flush. It installs the handlers for you.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
log.configure({
|
|
16
|
+
channels: [new ConsoleLog(), new FileLog({ chunk: "daily" })],
|
|
17
|
+
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### What each event does
|
|
22
|
+
|
|
23
|
+
| Event | Behavior |
|
|
24
|
+
|---|---|
|
|
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` | Flush in place. Node continues its natural exit. |
|
|
27
|
+
|
|
28
|
+
### Default recommendation
|
|
29
|
+
|
|
30
|
+
`["SIGINT", "SIGTERM", "beforeExit"]` covers:
|
|
31
|
+
- Local `Ctrl+C` (SIGINT)
|
|
32
|
+
- Container orchestrators (`docker stop`, Kubernetes sending SIGTERM)
|
|
33
|
+
- Natural exit (Node finished all work)
|
|
34
|
+
|
|
35
|
+
Add `"SIGHUP"` if you care about terminal disconnects. Add `"SIGUSR2"` if you use nodemon or pm2 restart.
|
|
36
|
+
|
|
37
|
+
### Idempotency
|
|
38
|
+
|
|
39
|
+
Calling `enableAutoFlush` twice **replaces** previous handlers — it does not stack. `disableAutoFlush()` removes every handler this logger instance registered; safe to call when nothing is registered.
|
|
40
|
+
|
|
41
|
+
## The manual way — your own handler
|
|
42
|
+
|
|
43
|
+
Use this when you need async work (close an HTTP server, drain a queue) **before** flushing:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
async function gracefulShutdown() {
|
|
47
|
+
await httpServer.close();
|
|
48
|
+
await queue.drain();
|
|
49
|
+
log.flushSync(); // still sync — guarantees disk write before exit
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.once("SIGINT", gracefulShutdown);
|
|
54
|
+
process.once("SIGTERM", gracefulShutdown);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**If you go manual for a signal, skip it in `autoFlushOn`** — otherwise both handlers fire and ours re-raises the signal mid-way through your async work.
|
|
58
|
+
|
|
59
|
+
## Async drain — `log.flush()`
|
|
60
|
+
|
|
61
|
+
`flushSync()` blocks the event loop with synchronous I/O. That's correct for the file channels and required inside the handlers `autoFlushOn` installs (a re-raised signal kills the process before any promise could settle). But a channel whose delivery is **async** — a network transport, an async disk write — can't drain synchronously. For those, `await` the async sibling on a graceful path you control:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
async function gracefulShutdown() {
|
|
65
|
+
await httpServer.close();
|
|
66
|
+
await log.flush(); // awaits every channel's async flush() to completion
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.once("SIGTERM", gracefulShutdown);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`log.flush()` fans out to every channel that implements `flush()` and awaits them together (`Promise.allSettled`). Each channel is isolated — one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. Channels without `flush()` are skipped.
|
|
74
|
+
|
|
75
|
+
| | `flushSync()` | `flush()` |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| I/O | synchronous — blocks the loop | asynchronous — awaited |
|
|
78
|
+
| Safe in a re-raising signal handler | yes | no — the signal exits before the promise settles |
|
|
79
|
+
| Used by `autoFlushOn` | yes | no |
|
|
80
|
+
| Reach for it when | file channels, last-resort durability | network/async channels, manual `await` before `process.exit` |
|
|
81
|
+
| `FileLog` / `JSONFileLog` | ✓ | ✓ (async write) |
|
|
82
|
+
|
|
83
|
+
`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
|
+
## What `flushSync()` actually does
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
log.flushSync();
|
|
89
|
+
// For every registered channel:
|
|
90
|
+
// if (channel.flushSync) channel.flushSync();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Synchronous I/O — blocks the event loop.
|
|
94
|
+
- Channels without `flushSync` (e.g. `ConsoleLog` — nothing to flush) are skipped silently.
|
|
95
|
+
- Works with and without `groupBy` on `FileLog` / `JSONFileLog`.
|
|
96
|
+
- No-op if every channel's buffer is empty.
|
|
97
|
+
|
|
98
|
+
`ConsoleLog` has no `flushSync` — it writes synchronously on every entry. `FileLog` and `JSONFileLog` both implement it.
|
|
99
|
+
|
|
100
|
+
## Unhandled errors
|
|
101
|
+
|
|
102
|
+
The `uncaughtException` path in [`captureAnyUnhandledRejection()`](@warlock.js/logger/capture-unhandled-errors/SKILL.md) 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
|
+
```ts
|
|
105
|
+
log.configure({
|
|
106
|
+
channels: [new FileLog({ levels: ["error"] })],
|
|
107
|
+
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
captureAnyUnhandledRejection();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## What NOT to do
|
|
114
|
+
|
|
115
|
+
- **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
|
+
- **Don't call `process.exit()` inside `autoFlushOn` handlers** — signal handlers here already re-raise the signal. Forcing an exit breaks exit codes.
|
|
117
|
+
- **Don't rely on the 5-second flush interval for shutdown safety.** It's a throughput optimization, not a durability guarantee.
|