Skip to content

Commit 71ecc42

Browse files
cardilsywhangabhinav
authored
Support arbitrary hook for fatal logs (#1088)
This adds a new WithFatalHook option that allows specifying arbitrary behavior overrides messages logged with FatalLevel. This supersedes the previous OnFatal option that relied on a CheckWriteAction enum. The enum is replaced by a CheckWriteHook--a hook that runs after write. Refs #1086 Co-authored-by: Sung Yoon Whang <[email protected]> Co-authored-by: Abhinav Gupta <[email protected]>
1 parent 75dacb4 commit 71ecc42

File tree

7 files changed

+114
-35
lines changed

7 files changed

+114
-35
lines changed

internal/exit/exit.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,32 @@ package exit
2424

2525
import "os"
2626

27-
var real = func() { os.Exit(1) }
27+
var _exit = os.Exit
2828

2929
// Exit normally terminates the process by calling os.Exit(1). If the package
3030
// is stubbed, it instead records a call in the testing spy.
31+
// Deprecated: use With() instead.
3132
func Exit() {
32-
real()
33+
With(1)
34+
}
35+
36+
// With terminates the process by calling os.Exit(code). If the package is
37+
// stubbed, it instead records a call in the testing spy.
38+
func With(code int) {
39+
_exit(code)
3340
}
3441

3542
// A StubbedExit is a testing fake for os.Exit.
3643
type StubbedExit struct {
3744
Exited bool
38-
prev func()
45+
Code int
46+
prev func(code int)
3947
}
4048

4149
// Stub substitutes a fake for the call to os.Exit(1).
4250
func Stub() *StubbedExit {
43-
s := &StubbedExit{prev: real}
44-
real = s.exit
51+
s := &StubbedExit{prev: _exit}
52+
_exit = s.exit
4553
return s
4654
}
4755

@@ -56,9 +64,10 @@ func WithStub(f func()) *StubbedExit {
5664

5765
// Unstub restores the previous exit function.
5866
func (se *StubbedExit) Unstub() {
59-
real = se.prev
67+
_exit = se.prev
6068
}
6169

62-
func (se *StubbedExit) exit() {
70+
func (se *StubbedExit) exit(code int) {
6371
se.Exited = true
72+
se.Code = code
6473
}

internal/exit/exit_test.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,32 @@
1818
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
// THE SOFTWARE.
2020

21-
package exit
21+
package exit_test
2222

2323
import (
2424
"testing"
2525

2626
"github.com/stretchr/testify/assert"
27+
"go.uber.org/zap/internal/exit"
2728
)
2829

2930
func TestStub(t *testing.T) {
31+
type want struct {
32+
exit bool
33+
code int
34+
}
3035
tests := []struct {
3136
f func()
32-
want bool
37+
want want
3338
}{
34-
{Exit, true},
35-
{func() {}, false},
39+
{func() { exit.With(42) }, want{exit: true, code: 42}},
40+
{exit.Exit, want{exit: true, code: 1}},
41+
{func() {}, want{}},
3642
}
3743

3844
for _, tt := range tests {
39-
s := WithStub(tt.f)
40-
assert.Equal(t, tt.want, s.Exited, "Stub captured unexpected exit value.")
45+
s := exit.WithStub(tt.f)
46+
assert.Equal(t, tt.want.exit, s.Exited, "Stub captured unexpected exit value.")
47+
assert.Equal(t, tt.want.code, s.Code, "Stub captured unexpected exit value.")
4148
}
4249
}

logger.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type Logger struct {
4242

4343
development bool
4444
addCaller bool
45-
onFatal zapcore.CheckWriteAction // default is WriteThenFatal
45+
onFatal zapcore.CheckWriteHook // default is WriteThenFatal
4646

4747
name string
4848
errorOutput zapcore.WriteSyncer
@@ -288,12 +288,12 @@ 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-
// Noop is the default value for CheckWriteAction, and it leads to
291+
// nil is the default value for CheckWriteAction, and it leads to
292292
// continued execution after a Fatal which is unexpected.
293-
if onFatal == zapcore.WriteThenNoop {
293+
if onFatal == nil {
294294
onFatal = zapcore.WriteThenFatal
295295
}
296-
ce = ce.Should(ent, onFatal)
296+
ce = ce.After(ent, onFatal)
297297
case zapcore.DPanicLevel:
298298
if log.development {
299299
ce = ce.Should(ent, zapcore.WriteThenPanic)

logger_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,23 @@ func TestLoggerCustomOnFatal(t *testing.T) {
579579
}
580580
}
581581

582+
type customWriteHook struct {
583+
called bool
584+
}
585+
586+
func (h *customWriteHook) OnWrite(_ *zapcore.CheckedEntry, _ []Field) {
587+
h.called = true
588+
}
589+
590+
func TestLoggerWithFatalHook(t *testing.T) {
591+
var h customWriteHook
592+
withLogger(t, InfoLevel, opts(WithFatalHook(&h)), func(logger *Logger, logs *observer.ObservedLogs) {
593+
logger.Fatal("great sadness")
594+
assert.True(t, h.called)
595+
assert.Equal(t, 1, logs.FilterLevelExact(FatalLevel).Len())
596+
})
597+
}
598+
582599
func TestNopLogger(t *testing.T) {
583600
logger := NewNop()
584601

options.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,15 @@ func IncreaseLevel(lvl zapcore.LevelEnabler) Option {
133133
}
134134

135135
// OnFatal sets the action to take on fatal logs.
136+
// Deprecated: Use WithFatalHook instead.
136137
func OnFatal(action zapcore.CheckWriteAction) Option {
138+
return WithFatalHook(action)
139+
}
140+
141+
// WithFatalHook sets a CheckWriteHook to run on fatal logs.
142+
func WithFatalHook(hook zapcore.CheckWriteHook) Option {
137143
return optionFunc(func(log *Logger) {
138-
log.onFatal = action
144+
log.onFatal = hook
139145
})
140146
}
141147

zapcore/entry.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ import (
2727
"sync"
2828
"time"
2929

30+
"go.uber.org/multierr"
3031
"go.uber.org/zap/internal/bufferpool"
3132
"go.uber.org/zap/internal/exit"
32-
33-
"go.uber.org/multierr"
3433
)
3534

3635
var (
@@ -152,6 +151,13 @@ type Entry struct {
152151
Stack string
153152
}
154153

154+
// CheckWriteHook allows to customize the action to take after a Fatal log entry
155+
// is processed.
156+
type CheckWriteHook interface {
157+
// OnWrite gets invoked when an entry is written
158+
OnWrite(*CheckedEntry, []Field)
159+
}
160+
155161
// CheckWriteAction indicates what action to take after a log entry is
156162
// processed. Actions are ordered in increasing severity.
157163
type CheckWriteAction uint8
@@ -164,10 +170,25 @@ const (
164170
WriteThenGoexit
165171
// WriteThenPanic causes a panic after Write.
166172
WriteThenPanic
167-
// WriteThenFatal causes a fatal os.Exit after Write.
173+
// WriteThenFatal causes an os.Exit(1) after Write.
168174
WriteThenFatal
169175
)
170176

177+
// OnWrite implements the OnWrite method to keep CheckWriteAction compatible
178+
// with the new CheckWriteHook interface which deprecates CheckWriteAction.
179+
func (a CheckWriteAction) OnWrite(ce *CheckedEntry, _ []Field) {
180+
switch a {
181+
case WriteThenGoexit:
182+
runtime.Goexit()
183+
case WriteThenPanic:
184+
panic(ce.Message)
185+
case WriteThenFatal:
186+
exit.Exit()
187+
}
188+
}
189+
190+
var _ CheckWriteHook = CheckWriteAction(0)
191+
171192
// CheckedEntry is an Entry together with a collection of Cores that have
172193
// already agreed to log it.
173194
//
@@ -178,15 +199,15 @@ type CheckedEntry struct {
178199
Entry
179200
ErrorOutput WriteSyncer
180201
dirty bool // best-effort detection of pool misuse
181-
should CheckWriteAction
202+
after CheckWriteHook
182203
cores []Core
183204
}
184205

185206
func (ce *CheckedEntry) reset() {
186207
ce.Entry = Entry{}
187208
ce.ErrorOutput = nil
188209
ce.dirty = false
189-
ce.should = WriteThenNoop
210+
ce.after = nil
190211
for i := range ce.cores {
191212
// don't keep references to cores
192213
ce.cores[i] = nil
@@ -224,17 +245,11 @@ func (ce *CheckedEntry) Write(fields ...Field) {
224245
ce.ErrorOutput.Sync()
225246
}
226247

227-
should, msg := ce.should, ce.Message
228-
putCheckedEntry(ce)
229-
230-
switch should {
231-
case WriteThenPanic:
232-
panic(msg)
233-
case WriteThenFatal:
234-
exit.Exit()
235-
case WriteThenGoexit:
236-
runtime.Goexit()
248+
hook := ce.after
249+
if hook != nil {
250+
hook.OnWrite(ce, fields)
237251
}
252+
putCheckedEntry(ce)
238253
}
239254

240255
// AddCore adds a Core that has agreed to log this CheckedEntry. It's intended to be
@@ -252,11 +267,19 @@ func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
252267
// Should sets this CheckedEntry's CheckWriteAction, which controls whether a
253268
// Core will panic or fatal after writing this log entry. Like AddCore, it's
254269
// safe to call on nil CheckedEntry references.
270+
// Deprecated: Use After(ent Entry, after CheckWriteHook) instead.
255271
func (ce *CheckedEntry) Should(ent Entry, should CheckWriteAction) *CheckedEntry {
272+
return ce.After(ent, should)
273+
}
274+
275+
// After sets this CheckEntry's CheckWriteHook, which will be called after this
276+
// log entry has been written. It's safe to call this on nil CheckedEntry
277+
// references.
278+
func (ce *CheckedEntry) After(ent Entry, hook CheckWriteHook) *CheckedEntry {
256279
if ce == nil {
257280
ce = getCheckedEntry()
258281
ce.Entry = ent
259282
}
260-
ce.should = should
283+
ce.after = hook
261284
return ce
262285
}

zapcore/entry_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func TestPutNilEntry(t *testing.T) {
6464
assert.NotNil(t, ce, "Expected only non-nil CheckedEntries in pool.")
6565
assert.False(t, ce.dirty, "Unexpected dirty bit set.")
6666
assert.Nil(t, ce.ErrorOutput, "Non-nil ErrorOutput.")
67-
assert.Equal(t, WriteThenNoop, ce.should, "Unexpected terminal behavior.")
67+
assert.Nil(t, ce.after, "Unexpected terminal behavior.")
6868
assert.Equal(t, 0, len(ce.cores), "Expected empty slice of cores.")
6969
assert.True(t, cap(ce.cores) > 0, "Expected pooled CheckedEntries to pre-allocate slice of Cores.")
7070
}
@@ -128,5 +128,22 @@ func TestCheckedEntryWrite(t *testing.T) {
128128
ce.Write()
129129
})
130130
assert.True(t, stub.Exited, "Expected to exit when WriteThenFatal is set.")
131+
assert.Equal(t, 1, stub.Code, "Expected to exit when WriteThenFatal is set.")
131132
})
133+
134+
t.Run("After", func(t *testing.T) {
135+
var ce *CheckedEntry
136+
hook := &customHook{}
137+
ce = ce.After(Entry{}, hook)
138+
ce.Write()
139+
assert.True(t, hook.called, "Expected to call custom action after Write.")
140+
})
141+
}
142+
143+
type customHook struct {
144+
called bool
145+
}
146+
147+
func (c *customHook) OnWrite(_ *CheckedEntry, _ []Field) {
148+
c.called = true
132149
}

0 commit comments

Comments
 (0)