Skip to content
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

Add support for logger-local error handlers #528

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
20 changes: 14 additions & 6 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ var eventPool = &sync.Pool{
// Event represents a log event. It is instanced by one of the level method of
// Logger and finalized by the Msg or Msgf method.
type Event struct {
buf []byte
w LevelWriter
level Level
done func(msg string)
stack bool // enable error stack trace
ch []Hook // hooks from context
skipFrame int // The number of additional frames to skip when printing the caller.
eh func(err error)
buf []byte
ch []Hook
skipFrame int
level Level
stack bool
}

func putEvent(e *Event) {
Expand Down Expand Up @@ -147,9 +148,16 @@ func (e *Event) msg(msg string) {
defer e.done(msg)
}
if err := e.write(); err != nil {
handlerRan := false
if e.eh != nil {
e.eh(err)
handlerRan = true
}
if ErrorHandler != nil {
ErrorHandler(err)
} else {
handlerRan = true
}
if !handlerRan {
fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err)
}
}
Expand Down
1 change: 1 addition & 0 deletions event_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build !binary_log
// +build !binary_log

package zerolog
Expand Down
5 changes: 3 additions & 2 deletions globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ var (
DurationFieldInteger = false

// ErrorHandler is called whenever zerolog fails to write an event on its
// output. If not set, an error is printed on the stderr. This handler must
// be thread safe and non-blocking.
// output, even if the logger has its own error handler configured. If not
// set, an error is printed on the stderr. This handler must be thread safe
// and non-blocking.
ErrorHandler func(err error)

// DefaultContextLogger is returned from Ctx() if there is no logger associated
Expand Down
115 changes: 64 additions & 51 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,96 @@
//
// A global Logger can be use for simple logging:
//
// import "github.com/rs/zerolog/log"
// import "github.com/rs/zerolog/log"
//
// log.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
// log.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
//
// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log".
//
// Fields can be added to log messages:
//
// log.Info().Str("foo", "bar").Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
// log.Info().Str("foo", "bar").Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
//
// Create logger instance to manage different outputs:
//
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// logger.Info().
// Str("foo", "bar").
// Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// logger.Info().
// Str("foo", "bar").
// Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
//
// Sub-loggers let you chain loggers with additional context:
//
// sublogger := log.With().Str("component": "foo").Logger()
// sublogger.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
// sublogger := log.With().Str("component": "foo").Logger()
// sublogger.Info().Msg("hello world")
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
//
// Level logging
//
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
//
// log.Debug().Msg("filtered out message")
// log.Info().Msg("routed message")
// log.Debug().Msg("filtered out message")
// log.Info().Msg("routed message")
//
// if e := log.Debug(); e.Enabled() {
// // Compute log output only if enabled.
// value := compute()
// e.Str("foo": value).Msg("some debug message")
// }
// // Output: {"level":"info","time":1494567715,"routed message"}
// if e := log.Debug(); e.Enabled() {
// // Compute log output only if enabled.
// value := compute()
// e.Str("foo": value).Msg("some debug message")
// }
// // Output: {"level":"info","time":1494567715,"routed message"}
//
// Customize automatic field names:
//
// log.TimestampFieldName = "t"
// log.LevelFieldName = "p"
// log.MessageFieldName = "m"
// log.TimestampFieldName = "t"
// log.LevelFieldName = "p"
// log.MessageFieldName = "m"
//
// log.Info().Msg("hello world")
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
// log.Info().Msg("hello world")
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
//
// Log with no level and message:
//
// log.Log().Str("foo","bar").Msg("")
// // Output: {"time":1494567715,"foo":"bar"}
// log.Log().Str("foo","bar").Msg("")
// // Output: {"time":1494567715,"foo":"bar"}
//
// Add contextual fields to global Logger:
//
// log.Logger = log.With().Str("foo", "bar").Logger()
// log.Logger = log.With().Str("foo", "bar").Logger()
//
// Sample logs:
//
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
// sampled.Info().Msg("will be logged every 10 messages")
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
// sampled.Info().Msg("will be logged every 10 messages")
//
// Log with contextual hooks:
//
// // Create the hook:
// type SeverityHook struct{}
// // Create the hook:
// type SeverityHook struct{}
//
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
// if level != zerolog.NoLevel {
// e.Str("severity", level.String())
// }
// }
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
// if level != zerolog.NoLevel {
// e.Str("severity", level.String())
// }
// }
//
// // And use it:
// var h SeverityHook
// log := zerolog.New(os.Stdout).Hook(h)
// log.Warn().Msg("")
// // Output: {"level":"warn","severity":"warn"}
// // And use it:
// var h SeverityHook
// log := zerolog.New(os.Stdout).Hook(h)
// log.Warn().Msg("")
// // Output: {"level":"warn","severity":"warn"}
//
//
// Caveats
// # Caveats
//
// There is no fields deduplication out-of-the-box.
// Using the same key multiple times creates new key in final JSON each time.
//
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// logger.Info().
// Timestamp().
// Msg("dup")
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// logger.Info().
// Timestamp().
// Msg("dup")
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
//
// In this case, many consumers will take the last value,
// but this is not guaranteed; check yours if in doubt.
Expand Down Expand Up @@ -215,6 +214,7 @@ type Logger struct {
w LevelWriter
level Level
sampler Sampler
eh func(err error)
context []byte
hooks []Hook
stack bool
Expand All @@ -238,6 +238,18 @@ func New(w io.Writer) Logger {
return Logger{w: lw, level: TraceLevel}
}

// NewWithErrorHandler creates a root logger with given output writer. If the
// output writer implements the LevelWriter interface, the WriteLevel method
// will be called instead of the Write one.
//
// If the logger fails to write an event to its output, the error handler is
// invoked.
func NewWithErrorHandler(w io.Writer, eh func(err error)) Logger {
l := New(w)
l.eh = eh
return l
}

// Nop returns a disabled logger for which all operation are no-op.
func Nop() Logger {
return New(nil).Level(Disabled)
Expand Down Expand Up @@ -451,6 +463,7 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event {
return nil
}
e := newEvent(l.w, level)
e.eh = l.eh
e.done = done
e.ch = l.hooks
if level != NoLevel && LevelFieldName != "" {
Expand Down
34 changes: 33 additions & 1 deletion log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ func (w errWriter) Write(p []byte) (n int, err error) {
return 0, w.error
}

func TestErrorHandler(t *testing.T) {
func TestGlobalErrorHandler(t *testing.T) {
var got error
want := errors.New("write error")
ErrorHandler = func(err error) {
Expand All @@ -866,6 +866,38 @@ func TestErrorHandler(t *testing.T) {
}
}

func TestLocalErrorHandler(t *testing.T) {
var got error
want := errors.New("write error")
eh := func(err error) {
got = err
}
log := NewWithErrorHandler(errWriter{want}, eh)
log.Log().Msg("test")
if got != want {
t.Errorf("LocalErrorHandler err = %#v, want %#v", got, want)
}
}

func TestLocalAndGlobalErrorHandler(t *testing.T) {
var gotGlobal, gotLocal error
want := errors.New("write error")
ErrorHandler = func(err error) {
gotGlobal = err
}
eh := func(err error) {
gotLocal = err
}
log := NewWithErrorHandler(errWriter{want}, eh)
log.Log().Msg("test")
if gotGlobal != want {
t.Errorf("ErrorHandler err = %#v, want %#v", gotGlobal, want)
}
if gotLocal != want {
t.Errorf("LocalErrorHandler err = %#v, want %#v", gotLocal, want)
}
}

func TestUpdateEmptyContext(t *testing.T) {
var buf bytes.Buffer
log := New(&buf)
Expand Down