Repository navigation
feat(log): flush on default-handled SIGTERM/SIGINT and add batch option - #274
Conversation
A default-handled SIGTERM/SIGINT terminates the process without emitting exit, dropping whatever the batched logger still buffers. Install a signal-exit-style listener per signal that, when it turns out to be the only one, flushes synchronously and re-raises with the default disposition restored. When another listener exists (srvx graceful shutdown), it defers and the regular flush path delivers the lines. Also add `batch: false` to write each line synchronously, restoring the pre-batching guarantee that a logged line reaches the kernel before even a hard kill (SIGKILL, OOM killer) can drop it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. π βΉοΈ Recent review infoβοΈ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: π Files selected for processing (2)
π§ Files skipped from review as they are similar to previous changes (1)
π WalkthroughWalkthroughChangesThe logging middleware adds configurable batching, synchronous flushing for buffered output, signal-aware shutdown handling, documentation, and tests covering immediate writes, backpressure, listener cleanup, and process termination. Logging batching and shutdown behavior
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Poem
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
- Register the signal handlers with `prependListener`: a `once` cleanup handler registered before the logger self-removes ahead of dispatch, so an appended handler would miscount it as absent and re-raise while the cleanup is still in flight. - Cover SIGHUP, which default-terminates without `exit` exactly like SIGTERM/SIGINT (terminal close is its common delivery). - Guard `flush()` against firing mid-drain: a flush scheduled by a batched line could run after an unbatched write started a drain wait, feeding the full stream and stacking a duplicate drain listener. - Soften the `batch: false` wording: under backpressure lines still buffer for `drain`, and a piped stdout write is asynchronous, so a hard kill narrows to the write in flight rather than "never buffered". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Ran an independent clean-context review over the implementation; it confirmed the core mechanism but found four issues, fixed in b645cd4:
Full suite green after the changes (1215 passed), scenarios re-verified on Node, Deno and Bun. π€ Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 2
π§Ή Nitpick comments (4)
test/log.test.ts (2)
241-266: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winExercise
batch: falsethrough a real pipe and immediate exit.This mock proves call timing only; it cannot reveal asynchronous stdout buffering. Add a child scenario using oversized output followed by same-tick
process.exit()to validate the documented durability guarantee. (nodejs.org)π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/log.test.ts` around lines 241 - 266, Add a child-process scenario alongside the existing batch-disabled test that runs the real logging path through a pipe, emits oversized output, and calls process.exit() in the same tick. Assert the parent receives the complete log output, validating the durability guarantee for log({ batch: false }); retain the current call-timing assertions.
366-370: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winCover a one-shot signal listener registered before
log().The current persistent listener is added afterward, so it misses the ordering case where a prior
oncelistener removes itself before the logger checkslistenerCount. (nodejs.org)π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/log.test.ts` around lines 366 - 370, Add coverage for a SIGTERM one-shot listener registered before log() in the handled-sigterm test setup. Ensure the once listener executes and removes itself before the loggerβs persistent listener checks listenerCount, while preserving the expected URLs and successful exit code assertions.test/fixtures/log-crash.mjs (2)
6-10: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winAdd a batch-disabled immediate-exit scenario.
Instantiate
log({ batch: false }), emit enough data to exercise pipe buffering, and exit in the same tick so the child test validates actual persistence rather than mocked invocation timing.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/log-crash.mjs` around lines 6 - 10, Update the fixtureβs middleware setup to instantiate log with batch disabled, then emit enough requests or log data to exercise pipe buffering and terminate the child process in the same tick. Keep the scenario focused on validating persisted output rather than mocked invocation timing.
21-25: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winAdd a one-shot handler registered before
log().A scenario using
process.once("SIGTERM", ...)before constructing the middleware reproduces the listener-ordering failure and verifies graceful-shutdown deferral.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/fixtures/log-crash.mjs` around lines 21 - 25, Extend the "handled-sigterm" scenario in the log-crash fixture by registering a one-shot SIGTERM handler with process.once before constructing or invoking the middleware log(). Keep the existing graceful-shutdown assertions and ensure this pre-registered handler verifies the logger defers shutdown rather than force-killing the process.
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/log.ts`:
- Around line 125-149: Ensure the non-batched logging path does not rely on
asynchronous stdout buffering before exit: update writeNow()/flushSync() in
src/log.ts (lines 125-149) to use a synchronous write end-to-end, or narrow the
documented durability guarantee to batched logging. Update the guarantee wording
in docs/1.guide/4.middleware.md (lines 79-83) accordingly, and add or adjust the
batch:false crash coverage in test/log.test.ts (lines 241-266) and
test/fixtures/log-crash.mjs (lines 6-10) if synchronous durability remains
promised.
- Around line 173-191: Update the signal ownership logic in src/log.ts lines
173-191 so it detects listeners registered before the logger before one-shot
handlers can detach, rather than relying only on the current listenerCount(sig);
preserve the existing flush and re-raise behavior for logger-owned signals. Add
coverage for a once-before-log() handler in test/log.test.ts lines 366-370 and
test/fixtures/log-crash.mjs lines 21-25.
---
Nitpick comments:
In `@test/fixtures/log-crash.mjs`:
- Around line 6-10: Update the fixtureβs middleware setup to instantiate log
with batch disabled, then emit enough requests or log data to exercise pipe
buffering and terminate the child process in the same tick. Keep the scenario
focused on validating persisted output rather than mocked invocation timing.
- Around line 21-25: Extend the "handled-sigterm" scenario in the log-crash
fixture by registering a one-shot SIGTERM handler with process.once before
constructing or invoking the middleware log(). Keep the existing
graceful-shutdown assertions and ensure this pre-registered handler verifies the
logger defers shutdown rather than force-killing the process.
In `@test/log.test.ts`:
- Around line 241-266: Add a child-process scenario alongside the existing
batch-disabled test that runs the real logging path through a pipe, emits
oversized output, and calls process.exit() in the same tick. Assert the parent
receives the complete log output, validating the durability guarantee for log({
batch: false }); retain the current call-timing assertions.
- Around line 366-370: Add coverage for a SIGTERM one-shot listener registered
before log() in the handled-sigterm test setup. Ensure the once listener
executes and removes itself before the loggerβs persistent listener checks
listenerCount, while preserving the expected URLs and successful exit code
assertions.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8dbfae63-eca0-4032-bb16-891c2e4fd445
π Files selected for processing (4)
docs/1.guide/4.middleware.mdsrc/log.tstest/fixtures/log-crash.mjstest/log.test.ts
Also track SIGHUP in the test listener snapshot β the harden commit added the hook but not the cleanup, so repeated log.ts imports tripped the max-listeners warning during the suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follows up on the crash-safety concern raised on #266 ("missed logs prior to a crash"). I stress-tested the batched logger against real process deaths (child processes on Node 22/24, Deno, Bun) and closed the gaps that are actually closable.
What the stress test showed
Scenarios where every buffered line already arrived before this PR: clean exit, same-tick
process.exit(), uncaught exception, unhandled rejection (Node runsexithooks on its default crash path), and SIGTERM/SIGINT with srvx graceful shutdown active.Scenarios that lost the final event-loop turn of lines:
exit. Happens when graceful shutdown is off (explicitfalse, the CI/TEST env carve-out, orlog()used standalone with a non-srvx server). Fixable β fixed here.Changes
hookExit()now also registers a SIGHUP/SIGTERM/SIGINT listener (signal-exit style): if it turns out to be the only listener at delivery time, it flushes synchronously, removes itself and re-raises β the process still dies by the signal with exit semantics untouched (verified: parent observessignal: SIGTERM, not an exit code). When any other listener exists (e.g. srvx graceful shutdown), it defers entirely and the regular flush path delivers the lines. Registration usesprependListenerso aprocess.once(sig, cleanup)registered earlier is still counted at delivery time βoncewrappers self-remove before dispatch, and an appended handler would miscount them as absent and re-raise mid-cleanup. The listeners are unref'd on all three runtimes, so an idle process still exits naturally (covered by a test).batch: falseoption. Writes each line in the request's own turn instead of coalescing per event-loop turn β narrows a hard kill's loss window to the write in flight, matching the pre-perf(log): batch stdout writes and cache the timestampΒ #266 logger (verified: lines survive a same-tick SIGKILL on Node, Deno and Bun). It funnels through the shared buffer + drain logic, so it respects backpressure and can never overtake lines a batched logger is still holding;flush()gained a mid-drain guard so a stale scheduled flush can't feed a full stream when both modes are mixed.Verification
All child-process scenarios re-run after the change β buffered lines delivered in every runnable path:
once)process.exit(), uncaught exception, natural endbatch: falseTests spawn
test/fixtures/log-crash.mjsand assert on what actually reaches the pipe plus how the process died β crash-flush behavior only shows in a process that really terminates. Suite also gainedbatch: falsewrite/backpressure/mixed-mode cases and listener cleanup so repeatedlog.tsimports don't trip the max-listeners warning.An independent clean-context review validated the design and surfaced the
oncerace, the missing SIGHUP, the mid-drain guard and a doc overclaim β all addressed in the second commit (details in the comment below).pnpm testgreen (1215 passed), lint and types clean.π€ Generated with Claude Code
Summary by CodeRabbit
New Features
batchoption for request logging.batch: falsewrites each request log immediately.Documentation
Bug Fixes