Skip to content

Commit 3baaae2

Browse files
committed
CheckWriteHook: Don't allow Noop action
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 3baaae2

File tree

4 files changed

+58
-14
lines changed

4 files changed

+58
-14
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: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,12 @@ import (
3232
"go.uber.org/zap/internal/exit"
3333
)
3434

35-
var (
36-
_cePool = sync.Pool{New: func() interface{} {
37-
// Pre-allocate some space for cores.
38-
return &CheckedEntry{
39-
cores: make([]Core, 4),
40-
}
41-
}}
42-
)
35+
var _cePool = sync.Pool{New: func() interface{} {
36+
// Pre-allocate some space for cores.
37+
return &CheckedEntry{
38+
cores: make([]Core, 4),
39+
}
40+
}}
4341

4442
func getCheckedEntry() *CheckedEntry {
4543
ce := _cePool.Get().(*CheckedEntry)
@@ -151,10 +149,24 @@ type Entry struct {
151149
Stack string
152150
}
153151

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

0 commit comments

Comments
 (0)