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

Conversation

FriedEgg
Copy link

resolves #397

inspiration from logrus...

This change is primarily motivated by the need to facilitate easier testing in applications that frequently use Fatal(). Additionally, it could benefit scenarios where applications require custom shutdown procedures for cleanup or other special handling but still choose to rely on zerolog to perform process termination.

Copy link

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

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

While I understand the need, I'm afraid the implementation could be improved

Please accept my feedbacks and review

globals.go Outdated
@@ -120,6 +121,9 @@ var (
// be thread safe and non-blocking.
ErrorHandler func(err error)

// ExitFunc is the fucntion called to exit the application, defaults to `os.Exit()`
Copy link

@ccoVeille ccoVeille Mar 20, 2025

Choose a reason for hiding this comment

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

🖊️ typo

Suggested change
// ExitFunc is the fucntion called to exit the application, defaults to `os.Exit()`
// ExitFunc is the function called to exit the application, defaults to `os.Exit()`

Please note this is pointless if you follow my later suggestion

globals.go Outdated
@@ -120,6 +121,9 @@ var (
// be thread safe and non-blocking.
ErrorHandler func(err error)

// ExitFunc is the fucntion called to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc = os.Exit
Copy link

@ccoVeille ccoVeille Mar 20, 2025

Choose a reason for hiding this comment

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

You are not using exitFunc except here and you didn't add a comment.

So I don't think you use this type, so I would like to suggest this

Suggested change
ExitFunc exitFunc = os.Exit
ExitFunc func(int) = os.Exit

So I'm also suggesting to delete the exitFunc type here

Please note this is pointless if you follow my later suggestion

globals.go Outdated
@@ -163,6 +167,8 @@ var (
disableSampling = new(int32)
)

type exitFunc func(int)
Copy link

@ccoVeille ccoVeille Mar 20, 2025

Choose a reason for hiding this comment

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

Please see here why I suggest to remove this

#717 (comment)

Suggested change
type exitFunc func(int)

Also having an non-exported type on ExitFunc variable that is a globally exported func is a bad thing

Please note this is pointless if you follow my later suggestion

globals.go Outdated
@@ -120,6 +121,9 @@ var (
// be thread safe and non-blocking.
ErrorHandler func(err error)

// ExitFunc is the fucntion called to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc = os.Exit
Copy link

@ccoVeille ccoVeille Mar 20, 2025

Choose a reason for hiding this comment

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

I see a real problem here, with the implementation

You are doing this change for test purpose, but you are introducing a global exiter that anyone can overload.

It shouldn't be global and exported.

This would lead to unpredictable behavior with tests, but also with code.

Think about someone anywhere in log that would do a zerolog.ExitFunc = func(_ int) {}

That's why I'm suggesting this

log.go Outdated
Comment on lines 397 to 402
if ExitFunc == nil {
ExitFunc = os.Exit
}
ExitFunc(1)
})
}

Choose a reason for hiding this comment

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

About the problem I raised here, here is what I suggest you to do

type ExitFunc func(int)

type Logger struct {
        w       LevelWriter
        level   Level
        sampler Sampler
        context []byte
        hooks   []Hook
        stack   bool
        ctx     context.Context
        exiter ExitFunc
}
Suggested change
if ExitFunc == nil {
ExitFunc = os.Exit
}
ExitFunc(1)
})
}
exitFunc = l.exitFunc
if exitFunc == nil {
exitFunc = os.Exit
}
exitFunc(1)
})
}
func(l *logger) SetTestExiter(t *testing.T, exitFunc ExitFunc) {
l.exitFunc = exitFunc
t.Cleanup(func() {
l.exitFunc = nil
})
}

This is a safe way, there won't be data race, no global overlapping between packages calling module

Copy link
Author

Choose a reason for hiding this comment

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

super valid criticisms, thanks for the feedback

Copy link
Author

Choose a reason for hiding this comment

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

for the setter, it seems more inline with other functions to have a function such as the following, to return a new child Logger instance with the desired ExitFunc than to modify it?

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

the intention wasn't to make this functionality exclusively available for testing purposes so I am not sure I see a reason to add the suggest SetTestExiter? Would having both make sense in your eyes?

Copy link

@ccoVeille ccoVeille Mar 20, 2025

Choose a reason for hiding this comment

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

Good remark and questions

Having both doesn't make sense I think

For me, having an exiter without test seems to be bad idea, as it could be used for unplanned thing.

But the fact I'm thinking that doesn't mean my voice is more valuable than anyone else.

I suggested the implementation with testing.T and t.Cleanup because it allows to create one logger and reset it after the test with a cleanup.

I feel it's safer, as the method:

  • is clearly identified for test purpose, as it requires a testing.T argument
  • cannot be used for anything else than test.

Please note I'm not against using a different name for the method I suggested, or a signature that return a new logger

  • func(l *Logger) ExitFunc(t *testing.T, exitFunc ExitFunc)
  • func(l *Logger) ExitFuncT(t *testing.T, exitFunc ExitFunc)
  • func(l Logger) ExitFunc(t *testing.T, exitFunc ExitFunc) Logger
  • func(l Logger) ExitFuncT(t *testing.T, exitFunc ExitFunc) Logger

We could also consider this

func(l Logger) ExitFunc(_ *testing.T, exitFunc ExitFunc) Logger {
   l.exitFunc = exitFunc
   return l
}

Here we would only enforce the existing of testing.T

The following solution is also possible:

func(l Logger) ExitFunc(exitFunc ExitFunc) Logger {
   if !testing.Testing() {
      panic("ExitFunc can only be used for test purpose")
   }
   l.exitFunc = exitFunc
   return l
}

I dislike panic, but I suggest it as a possible solution.

But I wouldn't mind either if you decide to use this for consistency

  • ExitFunc(exitFunc ExitFunc) Logger

I'm a random Gopher, jumping on your PR. I'm not a maintainer of the repository and my voice in only one among many others.

So feel fre to choose what you think to be a good solution, and let's wait for a maintainer feedback.

Copy link
Author

Choose a reason for hiding this comment

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

I have pushed some modifications based on your feedback. For the time being I have chosen to not tie the setting of l.exitFunc to testing and will wait for additional feedback from others.

Comment on lines +117 to +122
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)
}

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)
}

Comment on lines +125 to +132
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)
}
})

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

Comment on lines +106 to +109
err = cmd.Wait()
if err == nil {
t.Error("Expected log Fatal to exit with non-zero status")
}

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")
}

@rs
Copy link
Owner

rs commented Mar 20, 2025

Thanks for the proposal.

A Fatal should not be used in most cases and certainly not in testable code.

Adding a func pointer in the logger is not something I’m willing to add for this as it adds more weight to a pass by copy type.

Sorry to disappoint but I’m not convinced this is a necessary addition.

@FriedEgg
Copy link
Author

No disappointment here it can certainly be lived without especially if only for the benefit of testing.

@FriedEgg FriedEgg closed this Mar 21, 2025
@ccoVeille
Copy link

I accept the argument of memory consumption. It would add extra memory for anyone using the lib. For a feature that would be rarely used.

@FriedEgg good PR and discussion anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add ability to override exit function
3 participants