| name | flush-logs-on-shutdown |
| 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')`. |
Lifecycle — flushing buffered channels before exit
FileLog and JSONFileLog buffer entries in memory. A process that exits without draining loses the buffer.
The easy way — autoFlushOn
Tell the logger which process events should trigger a flush. It installs the handlers for you.
log.configure({
channels: [new ConsoleLog(), new FileLog({ chunk: "daily" })],
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
});
What each event does
| Event | Behavior |
|---|
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). |
beforeExit | Flush in place. Node continues its natural exit. |
Default recommendation
["SIGINT", "SIGTERM", "beforeExit"] covers:
- Local
Ctrl+C (SIGINT)
- Container orchestrators (
docker stop, Kubernetes sending SIGTERM)
- Natural exit (Node finished all work)
Add "SIGHUP" if you care about terminal disconnects. Add "SIGUSR2" if you use nodemon or pm2 restart.
Idempotency
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.
The manual way — your own handler
Use this when you need async work (close an HTTP server, drain a queue) before flushing:
async function gracefulShutdown() {
await httpServer.close();
await queue.drain();
log.flushSync();
process.exit(0);
}
process.once("SIGINT", gracefulShutdown);
process.once("SIGTERM", gracefulShutdown);
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.
Async drain — log.flush()
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:
async function gracefulShutdown() {
await httpServer.close();
await log.flush();
process.exit(0);
}
process.once("SIGTERM", gracefulShutdown);
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.
| flushSync() | flush() |
|---|
| I/O | synchronous — blocks the loop | asynchronous — awaited |
| Safe in a re-raising signal handler | yes | no — the signal exits before the promise settles |
Used by autoFlushOn | yes | no |
| Reach for it when | file channels, last-resort durability | network/async channels, manual await before process.exit |
FileLog / JSONFileLog | ✓ | ✓ (async write) |
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.
What flushSync() actually does
log.flushSync();
- Synchronous I/O — blocks the event loop.
- Channels without
flushSync (e.g. ConsoleLog — nothing to flush) are skipped silently.
- Works with and without
groupBy on FileLog / JSONFileLog.
- No-op if every channel's buffer is empty.
ConsoleLog has no flushSync — it writes synchronously on every entry. FileLog and JSONFileLog both implement it.
Unhandled errors
The uncaughtException path in captureAnyUnhandledRejection() 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.
log.configure({
channels: [new FileLog({ levels: ["error"] })],
autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
});
captureAnyUnhandledRejection();
What NOT to do
- 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); }.
- Don't call
process.exit() inside autoFlushOn handlers — signal handlers here already re-raise the signal. Forcing an exit breaks exit codes.
- Don't rely on the 5-second flush interval for shutdown safety. It's a throughput optimization, not a durability guarantee.