Skip to content

Add ExitFunc for FatalLevel log messages #717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,19 +221,25 @@ func (l Level) MarshalText() ([]byte, error) {
return []byte(LevelFieldMarshalFunc(l)), nil
}

// ExitFunc is a function type that takes an integer exit code and terminates the program.
// By default, it is set to os.Exit, but it can be overridden to customize the behavior
// of program termination (for example, for graceful shutdown or testing purposes).
type ExitFunc func(int)

// A Logger represents an active logging object that generates lines
// of JSON output to an io.Writer. Each logging operation makes a single
// call to the Writer's Write method. There is no guarantee on access
// serialization to the Writer. If your Writer is not thread safe,
// you may consider a sync wrapper.
type Logger struct {
w LevelWriter
level Level
sampler Sampler
context []byte
hooks []Hook
stack bool
ctx context.Context
w LevelWriter
level Level
sampler Sampler
context []byte
hooks []Hook
stack bool
ctx context.Context
exitFunc ExitFunc
}

// New creates a root logger with given output writer. If the output writer implements
Expand Down Expand Up @@ -335,6 +341,12 @@ func (l Logger) Hook(hooks ...Hook) Logger {
return l
}

// ExitFunc returns a logger with the e ExitFunc.
func (l Logger) ExitFunc(e ExitFunc) Logger {
l.exitFunc = e
return l
}

// Trace starts a new message with trace level.
//
// You must call Msg on the returned event in order to send the event.
Expand Down Expand Up @@ -382,18 +394,23 @@ func (l *Logger) Err(err error) *Event {
return l.Info()
}

// Fatal starts a new message with fatal level. The os.Exit(1) function
// is called by the Msg method, which terminates the program immediately.
// Fatal starts a new message with fatal level. The ExitFunc(1) function
// (os.Exit by default) is called by the Msg method, which terminates the
// program immediately.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Fatal() *Event {
return l.newEvent(FatalLevel, func(msg string) {
if closer, ok := l.w.(io.Closer); ok {
// Close the writer to flush any buffered message. Otherwise the message
// will be lost as os.Exit() terminates the program immediately.
// will be lost as ExitFunc() (os.Exit by default) terminates the program immediately.
closer.Close()
}
os.Exit(1)
exitFunc := l.exitFunc
if exitFunc == nil {
exitFunc = os.Exit
}
exitFunc(1)
})
}

Expand Down
54 changes: 54 additions & 0 deletions log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"reflect"
"runtime"
"strconv"
Expand Down Expand Up @@ -78,6 +81,57 @@ func TestInfo(t *testing.T) {
})
}

func TestFatal(t *testing.T) {
t.Run("should exit", func(t *testing.T) {
if os.Getenv("TEST_FATAL") == "1" {
log := New(os.Stderr)
log.Fatal().Msg("")
return
}

cmd := exec.Command(os.Args[0], "-test.run=TestFatal/should_exit")
cmd.Env = append(os.Environ(), "TEST_FATAL=1")
stderr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
out, err := io.ReadAll(stderr)
if err != nil {
t.Fatal(err)
}
err = cmd.Wait()
if err == nil {
t.Error("Expected log Fatal to exit with non-zero status")
}
Comment on lines +106 to +109

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should take a look at

This way you could to something like this

Suggested change
err = cmd.Wait()
if err == nil {
t.Error("Expected log Fatal to exit with non-zero status")
}
err = cmd.Wait()
if err == nil {
t.Error("Expected test to fail")
}
var errExit exec.ExitError
if !errors.As(err, &errExit) {
t.Error("Expected an os.ExitError")
}
if errExit.ExitCode() != 1 {
t.Error("Expected log Fatal to exit with non-zero status")
}


if got, want := decodeIfBinaryToString(out), `{"level":"fatal"}`+"\n"; got != want {
t.Errorf("invalid log output:\n got: %v\nwant: %v", got, want)
}
})

t.Run("should not exit", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).ExitFunc(func(_ int) {})
log.Fatal().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"level":"fatal"}`+"\n"; got != want {
t.Errorf("invalid log output:\n got: %v\nwant: %v", got, want)
}
Comment on lines +117 to +122

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding such a thing would help to validate the exiter is called

Suggested change
out := &bytes.Buffer{}
log := New(out).ExitFunc(func(_ int) {})
log.Fatal().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"level":"fatal"}`+"\n"; got != want {
t.Errorf("invalid log output:\n got: %v\nwant: %v", got, want)
}
out := &bytes.Buffer{}
var receivedCode int
log := New(out).ExitFunc(func(code int) {
receivedCode = code
})
log.Fatal().Msg("")
if got, want := receivedCode, 1; got != want {
t.Errorf("invalid exitCode:\n got: %v\nwant: %v", got, want)
}
if got, want := decodeIfBinaryToString(out.Bytes()), `{"level":"fatal"}`+"\n"; got != want {
t.Errorf("invalid log output:\n got: %v\nwant: %v", got, want)
}

})

t.Run("with level fatal should not exit", func(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.WithLevel(FatalLevel).Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"level":"fatal"}`+"\n"; got != want {
t.Errorf("invalid log output:\n got: %v\nwant: %v", got, want)
}
})
Comment on lines +125 to +132

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not setting the exiter

I'm unsure what you are testing here

}

func TestEmptyLevelFieldName(t *testing.T) {
fieldName := LevelFieldName
LevelFieldName = ""
Expand Down