Skip to content

Commit

Permalink
Allow callers to pass go context through to hooks
Browse files Browse the repository at this point in the history
Add Logger.{Trace,Debug,Info,Warn,...}Ctx() and similar functions to
allow go context to propagate to Hooks.  Add Event.GetContext() to make
the context retrievable by hooks.  Facilitates writing hooks which fetch
tracing context from the go context.
  • Loading branch information
danielbprice committed Jun 22, 2023
1 parent 9070d49 commit 9fc6f5e
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 19 deletions.
15 changes: 15 additions & 0 deletions event.go
@@ -1,6 +1,7 @@
package zerolog

import (
"context"
"fmt"
"net"
"os"
Expand All @@ -27,6 +28,7 @@ type Event struct {
stack bool // enable error stack trace
ch []Hook // hooks from context
skipFrame int // The number of additional frames to skip when printing the caller.
context context.Context
}

func putEvent(e *Event) {
Expand Down Expand Up @@ -64,6 +66,7 @@ func newEvent(w LevelWriter, level Level) *Event {
e.level = level
e.stack = false
e.skipFrame = 0
e.context = nil
return e
}

Expand Down Expand Up @@ -185,6 +188,18 @@ func Dict() *Event {
return newEvent(nil, 0)
}

// GetContext allows retrieval of the context.Context which is optionally
// stored in the event. This allows Hooks to retrieve values which are stored
// in the context.Context. This can be useful in tracing, where span
// information is commonly propagated in the context.Context.
// See also Logger.InfoCtx, etc.
func (e *Event) GetContext() context.Context {
if e == nil || e.context == nil {
return context.Background()
}
return e.context
}

// Array adds the field key with an array to the event context.
// Use zerolog.Arr() to create the array or pass a type that
// implement the LogArrayMarshaler interface.
Expand Down
25 changes: 25 additions & 0 deletions hook_test.go
Expand Up @@ -2,10 +2,15 @@ package zerolog

import (
"bytes"
"context"
"io/ioutil"
"testing"
)

type spanKeyType int

var spanKey spanKeyType

var (
levelNameHook = HookFunc(func(e *Event, level Level, msg string) {
levelName := level.String()
Expand All @@ -31,6 +36,12 @@ var (
discardHook = HookFunc(func(e *Event, level Level, message string) {
e.Discard()
})
contextHook = HookFunc(func(e *Event, level Level, message string) {
spanId, ok := e.GetContext().Value(spanKey).(string)
if ok {
e.Str("trace-span-id", spanId)
}
})
)

func TestHook(t *testing.T) {
Expand Down Expand Up @@ -120,6 +131,20 @@ func TestHook(t *testing.T) {
log = log.Hook(discardHook)
log.Log().Msg("test message")
}},
{"Context/Background", `{"level":"info","message":"test message"}` + "\n", func(log Logger) {
log = log.Hook(contextHook)
log.InfoCtx(context.Background()).Msg("test message")
}},
{"Context/nil", `{"level":"info","message":"test message"}` + "\n", func(log Logger) {
log = log.Hook(contextHook)
log.InfoCtx(nil).Msg("test message")
}},
{"Context/valid", `{"level":"info","trace-span-id":"12345abcdef","message":"test message"}` + "\n", func(log Logger) {
ctx := context.Background()
ctx = context.WithValue(ctx, spanKey, "12345abcdef")
log = log.Hook(contextHook)
log.InfoCtx(ctx).Msg("test message")
}},
{"None", `{"level":"error"}` + "\n", func(log Logger) {
log.Error().Msg("")
}},
Expand Down
149 changes: 130 additions & 19 deletions log.go
Expand Up @@ -82,7 +82,6 @@
// log.Warn().Msg("")
// // Output: {"level":"warn","severity":"warn"}
//
//
// Caveats
//
// There is no fields deduplication out-of-the-box.
Expand All @@ -99,6 +98,7 @@
package zerolog

import (
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -317,35 +317,75 @@ func (l Logger) Hook(h Hook) Logger {
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Trace() *Event {
return l.newEvent(TraceLevel, nil)
return l.newEvent(nil, TraceLevel, nil)
}

// TraceCtx starts a new message with trace level. ctx is recorded into the
// Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) TraceCtx(ctx context.Context) *Event {
return l.newEvent(ctx, TraceLevel, nil)
}

// Debug starts a new message with debug level.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Debug() *Event {
return l.newEvent(DebugLevel, nil)
return l.newEvent(nil, DebugLevel, nil)
}

// DebugCtx starts a new message with debug level. ctx is recorded into the
// Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) DebugCtx(ctx context.Context) *Event {
return l.newEvent(ctx, DebugLevel, nil)
}

// Info starts a new message with info level.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Info() *Event {
return l.newEvent(InfoLevel, nil)
return l.newEvent(nil, InfoLevel, nil)
}

// InfoCtx starts a new message with info level. ctx is recorded into the
// Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) InfoCtx(ctx context.Context) *Event {
return l.newEvent(ctx, InfoLevel, nil)
}

// Warn starts a new message with warn level.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Warn() *Event {
return l.newEvent(WarnLevel, nil)
return l.newEvent(nil, WarnLevel, nil)
}

// WarnCtx starts a new message with warn level. ctx is recorded into the
// Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) WarnCtx(ctx context.Context) *Event {
return l.newEvent(ctx, WarnLevel, nil)
}

// Error starts a new message with error level.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Error() *Event {
return l.newEvent(ErrorLevel, nil)
return l.newEvent(nil, ErrorLevel, nil)
}

// ErrorCtx starts a new message with error level. ctx is recorded into the
// Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) ErrorCtx(ctx context.Context) *Event {
return l.newEvent(ctx, ErrorLevel, nil)
}

// Err starts a new message with error level with err as a field if not nil or
Expand All @@ -360,20 +400,53 @@ func (l *Logger) Err(err error) *Event {
return l.Info()
}

// ErrCtx starts a new message with error level with err as a field if not nil
// or with info level if err is nil. ctx is recorded into the Event, allowing
// hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) ErrCtx(ctx context.Context, err error) *Event {
if err != nil {
return l.ErrorCtx(ctx).Err(err)
}

return l.InfoCtx(ctx)
}

// Fatal starts a new message with fatal level. The os.Exit(1) function
// 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) { os.Exit(1) })
return l.newEvent(nil, FatalLevel, func(msg string) { os.Exit(1) })
}

// FatalCtx starts a new message with fatal level. The os.Exit(1) function
// is called by the Msg method, which terminates the program immediately.
// ctx is recorded into the Event, allowing hooks to access the go context of
// the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) FatalCtx(ctx context.Context) *Event {
return l.newEvent(ctx, FatalLevel, func(msg string) { os.Exit(1) })
}

// Panic starts a new message with panic level. The panic() function
// is called by the Msg method, which stops the ordinary flow of a goroutine.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Panic() *Event {
return l.newEvent(PanicLevel, func(msg string) { panic(msg) })
return l.newEvent(nil, PanicLevel, func(msg string) { panic(msg) })
}

// PanicCtx starts a new message with panic level. The panic() function
// is called by the Msg method, which stops the ordinary flow of a goroutine.
// ctx is recorded into the Event, allowing hooks to access the go context of
// the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) PanicCtx(ctx context.Context) *Event {
return l.newEvent(ctx, PanicLevel, func(msg string) { panic(msg) })
}

// WithLevel starts a new message with level. Unlike Fatal and Panic
Expand All @@ -382,27 +455,37 @@ func (l *Logger) Panic() *Event {
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) WithLevel(level Level) *Event {
return l.WithLevelCtx(nil, level)
}

// WithLevelCtx starts a new message with level. Unlike Fatal and Panic
// methods, WithLevel does not terminate the program or stop the ordinary flow
// of a goroutine when used with their respective levels. ctx is recorded
// into the Event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) WithLevelCtx(ctx context.Context, level Level) *Event {
switch level {
case TraceLevel:
return l.Trace()
return l.TraceCtx(ctx)
case DebugLevel:
return l.Debug()
return l.DebugCtx(ctx)
case InfoLevel:
return l.Info()
return l.InfoCtx(ctx)
case WarnLevel:
return l.Warn()
return l.WarnCtx(ctx)
case ErrorLevel:
return l.Error()
return l.ErrorCtx(ctx)
case FatalLevel:
return l.newEvent(FatalLevel, nil)
return l.newEvent(ctx, FatalLevel, nil)
case PanicLevel:
return l.newEvent(PanicLevel, nil)
return l.newEvent(ctx, PanicLevel, nil)
case NoLevel:
return l.Log()
return l.LogCtx(ctx)
case Disabled:
return nil
default:
return l.newEvent(level, nil)
return l.newEvent(ctx, level, nil)
}
}

Expand All @@ -411,7 +494,16 @@ func (l *Logger) WithLevel(level Level) *Event {
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) Log() *Event {
return l.newEvent(NoLevel, nil)
return l.newEvent(nil, NoLevel, nil)
}

// LogCtx starts a new message with no level. Setting GlobalLevel to Disabled
// will still disable events produced by this method. ctx is recorded into
// the event, allowing hooks to access the go context of the call site.
//
// You must call Msg on the returned event in order to send the event.
func (l *Logger) LogCtx(ctx context.Context) *Event {
return l.newEvent(ctx, NoLevel, nil)
}

// Print sends a log event using debug level and no extra field.
Expand All @@ -422,6 +514,15 @@ func (l *Logger) Print(v ...interface{}) {
}
}

// PrintCtx sends a log event using debug level and no extra field. Arguments
// are handled in the manner of fmt.Print. ctx is recorded into the Event,
// allowing hooks to access the go context of the call site.
func (l *Logger) PrintCtx(ctx context.Context, v ...interface{}) {
if e := l.DebugCtx(ctx); e.Enabled() {
e.CallerSkipFrame(1).Msg(fmt.Sprint(v...))
}
}

// Printf sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Printf(format string, v ...interface{}) {
Expand All @@ -430,6 +531,15 @@ func (l *Logger) Printf(format string, v ...interface{}) {
}
}

// PrintfCtx sends a log event using debug level and no extra field. Arguments
// are handled in the manner of fmt.Printf. ctx is recorded into the Event,
// allowing hooks to access the go context of the call site.
func (l *Logger) PrintfCtx(ctx context.Context, format string, v ...interface{}) {
if e := l.DebugCtx(ctx); e.Enabled() {
e.CallerSkipFrame(1).Msg(fmt.Sprintf(format, v...))
}
}

// Write implements the io.Writer interface. This is useful to set as a writer
// for the standard library log.
func (l Logger) Write(p []byte) (n int, err error) {
Expand All @@ -442,7 +552,7 @@ func (l Logger) Write(p []byte) (n int, err error) {
return
}

func (l *Logger) newEvent(level Level, done func(string)) *Event {
func (l *Logger) newEvent(ctx context.Context, level Level, done func(string)) *Event {
enabled := l.should(level)
if !enabled {
if done != nil {
Expand All @@ -453,6 +563,7 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event {
e := newEvent(l.w, level)
e.done = done
e.ch = l.hooks
e.context = ctx
if level != NoLevel && LevelFieldName != "" {
e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
}
Expand Down

0 comments on commit 9fc6f5e

Please sign in to comment.