Skip to content

Commit 9e61a39

Browse files
authored
CheckWriteHook: Don't allow Noop action (#1089)
In #1088, we deprecated the CheckWriteAction enum in favor of the new fully customizable CheckWriteHook. Unfortunately, this introduced a minor regression: it's now possible to set log.Fatal to no-op with WriteThenNoop. Such a configuration will break code that looks like the following: ```go f, err := os.Open(..) if err != nil { log.Fatal("Cannot open file", zap.Error(err)) } // Control flow expects that if we get here, // f is valid and non-nil. // That's not the case if Fatal no-ops. fmt.Println(f.Name()) ``` This change fixes the regression by turning Noops into WriteThenFatal when logging Fatal log messages. This matches the old behavior. It further clarifies the documentation for CheckWriteHook so that users know to call runtime.Goexit or os.Exit in them. It's still possible to write a custom hook that no-ops the log.Fatal, but it requires a little extra effort from the user to do so.
1 parent 71ecc42 commit 9e61a39

File tree

4 files changed

+52
-6
lines changed

4 files changed

+52
-6
lines changed

logger.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,18 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
288288
ce = ce.Should(ent, zapcore.WriteThenPanic)
289289
case zapcore.FatalLevel:
290290
onFatal := log.onFatal
291-
// nil is the default value for CheckWriteAction, and it leads to
292-
// continued execution after a Fatal which is unexpected.
293-
if onFatal == nil {
291+
// nil or WriteThenNoop will lead to continued execution after
292+
// a Fatal log entry, which is unexpected. For example,
293+
//
294+
// f, err := os.Open(..)
295+
// if err != nil {
296+
// log.Fatal("cannot open", zap.Error(err))
297+
// }
298+
// fmt.Println(f.Name())
299+
//
300+
// The f.Name() will panic if we continue execution after the
301+
// log.Fatal.
302+
if onFatal == nil || onFatal == zapcore.WriteThenNoop {
294303
onFatal = zapcore.WriteThenFatal
295304
}
296305
ce = ce.After(ent, onFatal)

logger_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,17 @@ func TestLoggerConcurrent(t *testing.T) {
535535
})
536536
}
537537

538+
func TestLoggerFatalOnNoop(t *testing.T) {
539+
exitStub := exit.Stub()
540+
defer exitStub.Unstub()
541+
core, _ := observer.New(InfoLevel)
542+
543+
// We don't allow a no-op fatal hook.
544+
New(core, WithFatalHook(zapcore.WriteThenNoop)).Fatal("great sadness")
545+
assert.True(t, exitStub.Exited, "must exit for WriteThenNoop")
546+
assert.Equal(t, 1, exitStub.Code, "must exit with status 1 for WriteThenNoop")
547+
}
548+
538549
func TestLoggerCustomOnFatal(t *testing.T) {
539550
tests := []struct {
540551
msg string

options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ func OnFatal(action zapcore.CheckWriteAction) Option {
139139
}
140140

141141
// WithFatalHook sets a CheckWriteHook to run on fatal logs.
142+
// Zap will call this hook after writing a log statement with a Fatal level.
143+
//
144+
// For example, the following builds a logger that will exit the current
145+
// goroutine after writing a fatal log message, but it will not exit the
146+
// program.
147+
//
148+
// zap.New(core, zap.WithFatalHook(zapcore.WriteThenGoexit))
149+
//
150+
// It is important that the provided CheckWriteHook stops the control flow at
151+
// the current statement to meet expectations of callers of the logger.
152+
// We recommend calling os.Exit or runtime.Goexit inside custom hooks at
153+
// minimum.
142154
func WithFatalHook(hook zapcore.CheckWriteHook) Option {
143155
return optionFunc(func(log *Logger) {
144156
log.onFatal = hook

zapcore/entry.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,24 @@ type Entry struct {
151151
Stack string
152152
}
153153

154-
// CheckWriteHook allows to customize the action to take after a Fatal log entry
155-
// is processed.
154+
// CheckWriteHook is a custom action that may be executed after an entry is
155+
// written.
156+
//
157+
// Register one on a CheckedEntry with the After method.
158+
//
159+
// if ce := logger.Check(...); ce != nil {
160+
// ce = ce.After(hook)
161+
// ce.Write(...)
162+
// }
163+
//
164+
// You can configure the hook for Fatal log statements at the logger level with
165+
// the zap.WithFatalHook option.
156166
type CheckWriteHook interface {
157-
// OnWrite gets invoked when an entry is written
167+
// OnWrite is invoked with the CheckedEntry that was written and a list
168+
// of fields added with that entry.
169+
//
170+
// The list of fields DOES NOT include fields that were already added
171+
// to the logger with the With method.
158172
OnWrite(*CheckedEntry, []Field)
159173
}
160174

0 commit comments

Comments
 (0)