Repository navigation
Conversation
HandlePanic delegated recover() to an inner helper (recoverPanic). Go only honours recover() when it is called directly by the function that was deferred, not by any function that function calls. Because callers use "defer logger.HandlePanic(tag)", recover() ran one frame too deep, always returned nil, and the panic escaped unhandled -- so the structured "Panic: ..." log and os.Exit(2) never ran and the process crashed with a raw runtime trace instead. Call recover() directly inside both logger.HandlePanic and asyncLogger.HandlePanic, and move only the formatting into a logPanic helper that takes the already-recovered value. Add an osExit seam and internal tests covering both loggers; the tests fail against the previous delegated implementation. This regression dates back to the async logger refactor (f8bb60f, 2016).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
Walkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
🟢 Approval recommended
The implementation correctly addresses Go’s direct-recovery requirement and includes focused regression coverage.
0 open findings
What changed in this PR
Fixes panic recovery for synchronous and asynchronous loggers by calling recover() directly in deferred handlers.
Changes:
- Extracts panic formatting into
logPanic. - Adds a testable
osExitseam. - Adds coverage for recovery, logging, async flushing, and exit behavior.
| File | Description |
|---|---|
logger/logger.go |
Corrects synchronous panic recovery and exit handling. |
logger/async.go |
Corrects async recovery while preserving flush-before-exit. |
logger/handle_panic_internal_test.go |
Tests panic and non-panic behavior for both logger types. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| l.log.logPanic(tag, e) | ||
| l.FlushTimeout(time.Second * 30) //nolint:errcheck | ||
| os.Exit(2) | ||
| osExit(2) |
There was a problem hiding this comment.
This seam is a bit weird - I think we could use some gexec to make it better so that we don't have to insert it into production code. We could either compile a small fixture that panics and check the exit code like in #148, or use gexec to launch a sub-process in which we check the exit code.
There was a problem hiding this comment.
Hi @selzoc,
Thanks for the review!
I was unsure how to handle this correctly. I followed the approach in the referenced PR and I think it looks better now.
Replace the package-level osExit seam with a real subprocess test: HandlePanic now calls os.Exit(2) directly, and a small fixture binary (handle_panic_fixture) is compiled and run via gexec to assert the real exit code and panic log on stderr, for both the sync and async loggers. This matches the existing gexec pattern in the main/ suite and keeps test-only scaffolding out of production code.
selzoc
left a comment
There was a problem hiding this comment.
Nice! I would ask that you update the PR description, which still described the earlier approach.
Summary
logger.HandlePanicdoes not recover panics when used as documented (defer logger.HandlePanic(tag)). The panic escapes unhandled: the intendedPanic: ...log line andos.Exit(2)never run, and the process instead dies with a raw Go runtime trace.asyncLogger.HandlePanichas the same problem (and itsFlushTimeoutbefore exit never runs either).Root cause
Go only honours
recover()when it is called directly by the function that was deferred — not by any function that deferred function calls. The current code delegatesrecover()to a shared helper:With
defer logger.HandlePanic(tag), the deferred frame isHandlePanic, butrecover()is called fromrecoverPanic. The runtime's recover check requires the caller ofrecover()to be the deferred frame, so it returnsnil,recoverPanicreturnsfalse,os.Exit(2)is skipped, and the panic propagates.Fix
Call
recover()directly inside each deferred function and keep only the formatting in a helper that receives the already-recovered value:asyncLogger.HandlePanicis fixed the same way and still flushes before exit. AnosExitseam (defaulting toos.Exit) lets the new tests assert the exit behaviour without terminating the test process.Tests
Added internal tests (
logger/handle_panic_internal_test.go) covering both the sync and async loggers: a deferredHandlePanicmust recover the panic, logPanic: ..., and callos.Exit(2). The tests fail against the previous delegated implementation and pass with this change. Existing suite passes under-race.History / impact
The regression dates to the async logger refactor (commit
f8bb60f0, 2016), which extracted the recover logic into the shared helper. It went unnoticed because every caller usesHandlePanicto log-and-exit at a top-leveldefer: both the fixed and broken paths exit with status 2 and print a panic trace to stderr, so the only observable difference was the log format (and the loss of structured logging / async flush on panic).