From 0cf9361666399346efc17bad59a1218118e80cba Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 5 Jan 2026 12:19:15 -0600 Subject: [PATCH] Fix missing context.Context in Object/EmbedObject (#737) Copies over event settings for ctx, hooks, and stack - Fix missing context for Event.appendObjects passes event's stack, ctx, and hooks (ch). - Add Array.Errs support. - Context.Errs, Event.Errs, use new Array.Errs() - Array.Err handles nil correctly. - Fix Event.Err code ignoring modified event in ErrorStackMarshaler handling. - Fix Fields.appendFieldList when ErrorMarshalFunc returns a nil. - Fields.appendFieldList does no longer adds the ErrorStackFieldName field when ErrorStackMarshaler returns nil. - Don't call the ErrorMarshalFunc on nil errors. - Removed unreachable code NOP in Context.Array - Rewrite ErrorMarshalFunc testing. - Made diode tests more stable. - Log copy error in test. - Address review comments - Added CreateArray() and CreateDict() for both Context and Event and deprecate the Arr() and Dict(). Array now carries then stack/context/hooks so they are available to LogObjectMarshal of Array elements. Added unit tests for error marshal that returns an interface{} --- array.go | 44 ++++- array_test.go | 4 +- binary_test.go | 56 ++++--- context.go | 70 ++++---- ctx_test.go | 8 - diode/diode_test.go | 35 +++- error_marshal_test.go | 224 +++++++++++++++++++++++++ event.go | 80 +++++---- event_test.go | 140 ++++++++++++---- fields.go | 54 +++--- fixtures_test.go | 2 +- log.go | 11 +- log_example_test.go | 64 +++---- log_test.go | 380 ++---------------------------------------- 14 files changed, 595 insertions(+), 577 deletions(-) create mode 100644 error_marshal_test.go diff --git a/array.go b/array.go index 2ce789f..ecbbb02 100644 --- a/array.go +++ b/array.go @@ -1,6 +1,7 @@ package zerolog import ( + "context" "net" "sync" "time" @@ -17,7 +18,10 @@ var arrayPool = &sync.Pool{ // Array is used to prepopulate an array of items // which can be re-used to add to log messages. type Array struct { - buf []byte + buf []byte + stack bool // enable error stack trace + ctx context.Context // Optional Go context + ch []Hook // hooks } func putArray(a *Array) { @@ -31,13 +35,22 @@ func putArray(a *Array) { if cap(a.buf) > maxSize { return } + a.stack = false + a.ctx = nil + a.ch = nil arrayPool.Put(a) } // Arr creates an array to be added to an Event or Context. +// WARNING: This function is deprecated because it does not preserve +// the stack, hooks, and context from the parent event. +// Deprecated: Use Event.CreateArray or Context.CreateArray instead. func Arr() *Array { a := arrayPool.Get().(*Array) a.buf = a.buf[:0] + a.stack = false + a.ctx = nil + a.ch = nil return a } @@ -59,7 +72,7 @@ func (a *Array) write(dst []byte) []byte { // Object marshals an object that implement the LogObjectMarshaler // interface and appends it to the array. func (a *Array) Object(obj LogObjectMarshaler) *Array { - a.buf = appendObject(enc.AppendArrayDelim(a.buf), obj) + a.buf = appendObject(enc.AppendArrayDelim(a.buf), obj, a.stack, a.ctx, a.ch) return a } @@ -90,12 +103,12 @@ func (a *Array) RawJSON(val []byte) *Array { // Err serializes and appends the err to the array. func (a *Array) Err(err error) *Array { switch m := ErrorMarshalFunc(err).(type) { + case nil: + a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf)) case LogObjectMarshaler: a = a.Object(m) case error: - if m == nil || isNilValue(m) { - a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf)) - } else { + if !isNilValue(m) { a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error()) } case string: @@ -107,6 +120,27 @@ func (a *Array) Err(err error) *Array { return a } +// Errs serializes and appends errors to the array. +func (a *Array) Errs(errs []error) *Array { + for _, err := range errs { + switch m := ErrorMarshalFunc(err).(type) { + case nil: + a = a.Interface(nil) + case LogObjectMarshaler: + a = a.Object(m) + case error: + if !isNilValue(m) { + a = a.Str(m.Error()) + } + case string: + a = a.Str(m) + default: + a = a.Interface(m) + } + } + return a +} + // Bool appends the val as a bool to the array. func (a *Array) Bool(b bool) *Array { a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b) diff --git a/array_test.go b/array_test.go index a584be8..1d1517e 100644 --- a/array_test.go +++ b/array_test.go @@ -47,14 +47,14 @@ func TestArray(t *testing.T) { ). Err(nil). Err(fmt.Errorf("failure")). - Err(errorObjectMarshalerImpl{fmt.Errorf("oops")}). + Err(loggableError{fmt.Errorf("oops")}). Object(logObjectMarshalerImpl{ name: "ZIT", age: 22, }). Type(3.14) - want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},{"longer":[1111,2222,3333,4444,5555]},"0001-01-01T00:00:00Z","192.168.0.10","127.0.0.0/24","01:23:45:67:89:ab",{"Pub":"A","tag":"j"},{"name":"zot","age":-35},0,{"bar":"baz","n":1},null,"failure",{"error":"OOPS"},{"name":"zit","age":-22},"float64"]` + want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},{"longer":[1111,2222,3333,4444,5555]},"0001-01-01T00:00:00Z","192.168.0.10","127.0.0.0/24","01:23:45:67:89:ab",{"Pub":"A","tag":"j"},{"name":"zot","age":-35},0,{"bar":"baz","n":1},null,"failure",{"l":"OOPS"},{"name":"zit","age":-22},"float64"]` if got := decodeObjectToStr(a.write([]byte{})); got != want { t.Errorf("Array.write()\ngot: %s\nwant: %s", got, want) } diff --git a/binary_test.go b/binary_test.go index f5d77f0..5450203 100644 --- a/binary_test.go +++ b/binary_test.go @@ -206,12 +206,13 @@ func ExampleEvent_Dict() { dst := bytes.Buffer{} log := New(&dst) - log.Log(). - Str("foo", "bar"). - Dict("dict", Dict(). - Str("bar", "baz"). - Int("n", 1), - ). + e := log.Log(). + Str("foo", "bar") + + e.Dict("dict", e.CreateDict(). + Str("bar", "baz"). + Int("n", 1), + ). Msg("hello world") fmt.Println(decodeIfBinaryToString(dst.Bytes())) @@ -243,12 +244,13 @@ func ExampleEvent_Array() { dst := bytes.Buffer{} log := New(&dst) - log.Log(). - Str("foo", "bar"). - Array("array", Arr(). - Str("baz"). - Int(1), - ). + e := log.Log(). + Str("foo", "bar") + + e.Array("array", e.CreateArray(). + Str("baz"). + Int(1), + ). Msg("hello world") fmt.Println(decodeIfBinaryToString(dst.Bytes())) @@ -414,14 +416,15 @@ func ExampleEvent_Fields_slice() { func ExampleContext_Dict() { dst := bytes.Buffer{} - log := New(&dst).With(). - Str("foo", "bar"). - Dict("dict", Dict(). - Str("bar", "baz"). - Int("n", 1), - ).Logger() + ctx := New(&dst).With(). + Str("foo", "bar") - log.Log().Msg("hello world") + logger := ctx.Dict("dict", ctx.CreateDict(). + Str("bar", "baz"). + Int("n", 1), + ).Logger() + + logger.Log().Msg("hello world") fmt.Println(decodeIfBinaryToString(dst.Bytes())) // Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"} @@ -429,14 +432,15 @@ func ExampleContext_Dict() { func ExampleContext_Array() { dst := bytes.Buffer{} - log := New(&dst).With(). - Str("foo", "bar"). - Array("array", Arr(). - Str("baz"). - Int(1), - ).Logger() + ctx := New(&dst).With(). + Str("foo", "bar") - log.Log().Msg("hello world") + logger := ctx.Array("array", ctx.CreateArray(). + Str("baz"). + Int(1), + ).Logger() + + logger.Log().Msg("hello world") fmt.Println(decodeIfBinaryToString(dst.Bytes())) // Output: {"foo":"bar","array":["baz",1],"message":"hello world"} diff --git a/context.go b/context.go index b8a3cea..0b6029f 100644 --- a/context.go +++ b/context.go @@ -3,7 +3,6 @@ package zerolog import ( "context" "fmt" - "io" "math" "net" "time" @@ -23,7 +22,7 @@ func (c Context) Logger() Logger { // Only map[string]interface{} and []interface{} are accepted. []interface{} must // alternate string keys and arbitrary values, and extraneous ones are ignored. func (c Context) Fields(fields interface{}) Context { - c.l.context = appendFields(c.l.context, fields, c.l.stack) + c.l.context = appendFields(c.l.context, fields, c.l.stack, c.l.ctx, c.l.hooks) return c } @@ -35,8 +34,28 @@ func (c Context) Dict(key string, dict *Event) Context { return c } +// CreateDict creates an Event to be used with the Context.Dict method. +// It preserves the stack, hooks, and context from the logger. +// Call usual field methods like Str, Int etc to add fields to this +// event and give it as argument the Context.Dict method. +func (c Context) CreateDict() *Event { + return newEvent(nil, DebugLevel, c.l.stack, c.l.ctx, c.l.hooks) +} + +// CreateArray creates an Array to be used with the Context.Array method. +// It preserves the stack, hooks, and context from the logger. +// Call usual field methods like Str, Int etc to add elements to this +// array and give it as argument the Context.Array method. +func (c Context) CreateArray() *Array { + a := Arr() + a.stack = c.l.stack + a.ctx = c.l.ctx + a.ch = c.l.hooks + return a +} + // Array adds the field key with an array to the event context. -// Use zerolog.Arr() to create the array or pass a type that +// Use c.CreateArray() to create the array or pass a type that // implement the LogArrayMarshaler interface. func (c Context) Array(key string, arr LogArrayMarshaler) Context { c.l.context = enc.AppendKey(c.l.context, key) @@ -44,20 +63,15 @@ func (c Context) Array(key string, arr LogArrayMarshaler) Context { c.l.context = arr.write(c.l.context) return c } - var a *Array - if aa, ok := arr.(*Array); ok { - a = aa - } else { - a = Arr() - arr.MarshalZerologArray(a) - } + a := c.CreateArray() + arr.MarshalZerologArray(a) c.l.context = a.write(c.l.context) return c } // Object marshals an object that implement the LogObjectMarshaler interface. func (c Context) Object(key string, obj LogObjectMarshaler) Context { - e := newEvent(LevelWriterAdapter{io.Discard}, 0) + e := c.l.scratchEvent() e.Object(key, obj) c.l.context = enc.AppendObjectData(c.l.context, e.buf) putEvent(e) @@ -66,7 +80,7 @@ func (c Context) Object(key string, obj LogObjectMarshaler) Context { // Object marshals an object that implement the LogObjectMarshaler interface. func (c Context) Objects(key string, objs []LogObjectMarshaler) Context { - e := newEvent(LevelWriterAdapter{io.Discard}, 0) + e := c.l.scratchEvent() e.Objects(key, objs) c.l.context = enc.AppendObjectData(c.l.context, e.buf) putEvent(e) @@ -75,7 +89,7 @@ func (c Context) Objects(key string, objs []LogObjectMarshaler) Context { // EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface. func (c Context) EmbedObject(obj LogObjectMarshaler) Context { - e := newEvent(LevelWriterAdapter{io.Discard}, 0) + e := c.l.scratchEvent() e.EmbedObject(obj) c.l.context = enc.AppendObjectData(c.l.context, e.buf) putEvent(e) @@ -139,6 +153,7 @@ func (c Context) RawJSON(key string, b []byte) Context { } // AnErr adds the field key with serialized err to the logger context. +// If err is nil, no field is added. func (c Context) AnErr(key string, err error) Context { switch m := ErrorMarshalFunc(err).(type) { case nil: @@ -146,11 +161,10 @@ func (c Context) AnErr(key string, err error) Context { case LogObjectMarshaler: return c.Object(key, m) case error: - if m == nil || isNilValue(m) { + if isNilValue(m) { return c - } else { - return c.Str(key, m.Error()) } + return c.Str(key, m.Error()) case string: return c.Str(key, m) default: @@ -161,24 +175,7 @@ func (c Context) AnErr(key string, err error) Context { // Errs adds the field key with errs as an array of serialized errors to the // logger context. func (c Context) Errs(key string, errs []error) Context { - arr := Arr() - for _, err := range errs { - switch m := ErrorMarshalFunc(err).(type) { - case LogObjectMarshaler: - arr = arr.Object(m) - case error: - if m == nil || isNilValue(m) { - arr = arr.Interface(nil) - } else { - arr = arr.Str(m.Error()) - } - case string: - arr = arr.Str(m) - default: - arr = arr.Interface(m) - } - } - + arr := c.CreateArray().Errs(errs) return c.Array(key, arr) } @@ -187,12 +184,11 @@ func (c Context) Err(err error) Context { if c.l.stack && ErrorStackMarshaler != nil { switch m := ErrorStackMarshaler(err).(type) { case nil: + // do nothing case LogObjectMarshaler: c = c.Object(ErrorStackFieldName, m) case error: - if m != nil && !isNilValue(m) { - c = c.Str(ErrorStackFieldName, m.Error()) - } + c = c.Str(ErrorStackFieldName, m.Error()) case string: c = c.Str(ErrorStackFieldName, m) default: diff --git a/ctx_test.go b/ctx_test.go index a51d51d..447faf7 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -82,14 +82,6 @@ func (t logObjectMarshalerImpl) MarshalZerologObject(e *Event) { e.Str("name", strings.ToLower(t.name)).Int("age", -t.age) } -type errorObjectMarshalerImpl struct { - error -} - -func (t errorObjectMarshalerImpl) MarshalZerologObject(e *Event) { - e.Str("error", strings.ToUpper(t.error.Error())) -} - func Test_InterfaceLogObjectMarshaler(t *testing.T) { var buf bytes.Buffer log := New(&buf) diff --git a/diode/diode_test.go b/diode/diode_test.go index 473076c..cbef60b 100644 --- a/diode/diode_test.go +++ b/diode/diode_test.go @@ -7,6 +7,7 @@ import ( "log" "os" "os/exec" + "sync" "testing" "time" @@ -60,15 +61,25 @@ func TestFatal(t *testing.T) { if err != nil { t.Fatal(err) } - slurp, err := io.ReadAll(stderr) - if err != nil { - t.Fatal(err) - } + + var stderrBuf bytes.Buffer + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if _, err := io.Copy(&stderrBuf, stderr); err != nil { + t.Errorf("failed to copy stderr: %v", err) + } + }() + err = cmd.Wait() if err == nil { t.Error("Expected log.Fatal to exit with non-zero status") } + wg.Wait() // Wait for the goroutine to finish copying + slurp := stderrBuf.Bytes() + want := "{\"level\":\"fatal\",\"message\":\"test\"}\n" got := cbor.DecodeIfBinaryToString(slurp) if got != want { @@ -112,15 +123,23 @@ func TestFatalWithFilteredLevelWriter(t *testing.T) { if err != nil { t.Fatal(err) } - slurp, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } + + var stdoutBuf bytes.Buffer + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _, _ = io.Copy(&stdoutBuf, stdout) + }() + err = cmd.Wait() if err == nil { t.Error("Expected log.Fatal to exit with non-zero status") } + wg.Wait() // Wait for the goroutine to finish copying + slurp := stdoutBuf.Bytes() + got := cbor.DecodeIfBinaryToString(slurp) want := "{\"level\":\"fatal\",\"message\":\"test\"}\n" if got != want { diff --git a/error_marshal_test.go b/error_marshal_test.go new file mode 100644 index 0000000..bae363c --- /dev/null +++ b/error_marshal_test.go @@ -0,0 +1,224 @@ +package zerolog + +import ( + "bytes" + "fmt" + "strings" + "testing" +) + +type loggableError struct { + error +} + +func (l loggableError) MarshalZerologObject(e *Event) { + if l.error == nil { + return + } + e.Str("l", strings.ToUpper(l.error.Error())) +} + +type nonLoggableError struct { + error + line int +} + +type wrappedError struct { + error + msg string +} + +func (w wrappedError) Error() string { + if w.error == nil { + return w.msg + } + return w.error.Error() + ": " + w.msg +} + +type interfaceError struct { + val string +} + +func TestArrayErrorMarshalFunc(t *testing.T) { + prefixed := func(s, prefix string) string { + if s == "null" { + return "" + } + return prefix + s + `,` + } + errs := []error{ + nil, + fmt.Errorf("failure"), + loggableError{fmt.Errorf("whoops")}, + nonLoggableError{fmt.Errorf("oops"), 402}, + } + type testCase struct { + name string + marshal func(err error) interface{} + want []string + } + testCases := []testCase{ + { + name: "default", + marshal: nil, + want: []string{`null`, `"failure"`, `{"l":"WHOOPS"}`, `"oops"`}, + }, + { + name: "string", + marshal: func(err error) interface{} { + if err == nil { + return nil + } + return err.Error() + }, + want: []string{`null`, `"failure"`, `"whoops"`, `"oops"`}, + }, + { + name: "loggable", + marshal: func(err error) interface{} { + if err == nil { + return nil + } + return loggableError{err} + }, + want: []string{`null`, `{"l":"FAILURE"}`, `{"l":"WHOOPS"}`, `{"l":"OOPS"}`}, + }, + { + name: "non-loggable", + marshal: func(err error) interface{} { + if err == nil { + return nil + } + return nonLoggableError{err, 404} + }, + want: []string{`null`, `"failure"`, `"whoops"`, `"oops"`}, + }, + { + name: "interface", + marshal: func(err error) interface{} { + var some interfaceError + if err != nil { + some.val = err.Error() + } + var interfaceErr interface{} = some + return interfaceErr + }, + want: []string{`{}`, `{}`, `{}`, `{}`}, + }, + { + name: "nilError", + marshal: func(err error) interface{} { + var errNil error = nil + return errNil + }, + want: []string{`null`, `null`, `null`, `null`}, + }, + { + name: "wrapped error", + marshal: func(err error) interface{} { + if err == nil { + return nil + } else if we, ok := err.(wrappedError); ok { + return we + } else { + return wrappedError{err, "addendum"} + } + }, + want: []string{`null`, `"failure: addendum"`, `"whoops: addendum"`, `"oops: addendum"`}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + originalErrorMarshalFunc := ErrorMarshalFunc + defer func() { + ErrorMarshalFunc = originalErrorMarshalFunc + }() + + if tc.marshal != nil { + ErrorMarshalFunc = tc.marshal + } + + t.Run("Err", func(t *testing.T) { + for i, err := range errs { + want := tc.want[i] + t.Run("Arr", func(t *testing.T) { + wants := `[` + want + `]` + a := Arr().Err(err) + if got := decodeObjectToStr(a.write([]byte{})); got != wants { + t.Errorf("%s %d Array.Err(%v)\ngot: %s\nwant: %s", tc.name, i, err, got, wants) + } + }) + t.Run("Ctx", func(t *testing.T) { + wants := `{` + prefixed(want, `"error":`) + `"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out).With().Err(err).Logger() + logger.Log().Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s %d Ctx.Err(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants) + } + }) + t.Run("Event", func(t *testing.T) { + wants := `{` + prefixed(want, `"error":`) + `"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out) + logger.Log().Err(err).Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s %d Event.Err(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants) + } + }) + t.Run("Fields", func(t *testing.T) { + if i == 0 && tc.want[i] == "{}" { + want = `null` + } + wants := `{"err":` + want + `,"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out) + logger.Log().Fields(map[string]interface{}{"err": err}).Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s %d Event.Fields(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants) + } + }) + } + }) + + t.Run("Errs", func(t *testing.T) { + want := `[` + strings.Join(tc.want, ",") + `]` + t.Run("Arr", func(t *testing.T) { + a := Arr().Errs(errs) + if got := decodeObjectToStr(a.write([]byte{})); got != want { + t.Errorf("%s Array.Errs()\ngot: %s\nwant: %s", tc.name, got, want) + } + }) + + t.Run("Ctx", func(t *testing.T) { + wants := `{"e":` + want + `,"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out).With().Errs("e", errs).Logger() + logger.Log().Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants) + } + }) + t.Run("Event", func(t *testing.T) { + wants := `{"e":` + want + `,"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out) + logger.Log().Errs("e", errs).Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants) + } + }) + t.Run("Fields", func(t *testing.T) { + wants := `{"e":` + want + `,"message":"msg"}` + "\n" + out := &bytes.Buffer{} + logger := New(out) + logger.Log().Fields(map[string]interface{}{"e": errs}).Msg("msg") + if got := decodeIfBinaryToString(out.Bytes()); got != wants { + t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants) + } + }) + }) + }) + } +} diff --git a/event.go b/event.go index c84e935..0cbd1b3 100644 --- a/event.go +++ b/event.go @@ -57,14 +57,15 @@ type LogArrayMarshaler interface { MarshalZerologArray(a *Array) } -func newEvent(w LevelWriter, level Level) *Event { +func newEvent(w LevelWriter, level Level, stack bool, ctx context.Context, hooks []Hook) *Event { e := eventPool.Get().(*Event) e.buf = e.buf[:0] - e.ch = nil + e.stack = stack + e.ctx = ctx + e.ch = hooks e.buf = enc.AppendBeginMarker(e.buf) e.w = w e.level = level - e.stack = false e.skipFrame = 0 return e } @@ -164,12 +165,12 @@ func (e *Event) Fields(fields interface{}) *Event { if e == nil { return e } - e.buf = appendFields(e.buf, fields, e.stack) + e.buf = appendFields(e.buf, fields, e.stack, e.ctx, e.ch) return e } // Dict adds the field key with a dict to the event context. -// Use zerolog.Dict() to create the dictionary. +// Use e.CreateDict() to create the dictionary. func (e *Event) Dict(key string, dict *Event) *Event { if e == nil { return e @@ -180,15 +181,43 @@ func (e *Event) Dict(key string, dict *Event) *Event { return e } +// CreateDict creates an Event to be used with the *Event.Dict method. +// It preserves the stack, hooks, and context from the parent event. +// Call usual field methods like Str, Int etc to add fields to this +// event and give it as argument the *Event.Dict method. +func (e *Event) CreateDict() *Event { + if e == nil { + return newEvent(nil, DebugLevel, false, nil, nil) + } + return newEvent(nil, DebugLevel, e.stack, e.ctx, e.ch) +} + // Dict creates an Event to be used with the *Event.Dict method. // Call usual field methods like Str, Int etc to add fields to this // event and give it as argument the *Event.Dict method. +// NOTE: This function is deprecated because it does not preserve +// the stack, hooks, and context from the parent event. +// Deprecated: Use Event.CreateDict instead. func Dict() *Event { - return newEvent(nil, 0) + return newEvent(nil, DebugLevel, false, nil, nil) +} + +// CreateArray creates an Array to be used with the *Event.Array method. +// It preserves the stack, hooks, and context from the parent event. +// Call usual field methods like Str, Int etc to add elements to this +// array and give it as argument the *Event.Array method. +func (e *Event) CreateArray() *Array { + a := Arr() + if e != nil { + a.stack = e.stack + a.ctx = e.ctx + a.ch = e.ch + } + return a } // Array adds the field key with an array to the event context. -// Use zerolog.Arr() to create the array or pass a type that +// Use e.CreateArray() to create the array or pass a type that // implement the LogArrayMarshaler interface. func (e *Event) Array(key string, arr LogArrayMarshaler) *Event { if e == nil { @@ -199,7 +228,7 @@ func (e *Event) Array(key string, arr LogArrayMarshaler) *Event { if aa, ok := arr.(*Array); ok { a = aa } else { - a = Arr() + a = e.CreateArray() arr.MarshalZerologArray(a) } e.buf = a.write(e.buf) @@ -235,7 +264,7 @@ func (e *Event) Objects(key string, objs []LogObjectMarshaler) *Event { } e.buf = enc.AppendArrayStart(enc.AppendKey(e.buf, key)) for i, obj := range objs { - e.buf = appendObject(e.buf, obj) + e.buf = appendObject(e.buf, obj, e.stack, e.ctx, e.ch) if i < (len(objs) - 1) { e.buf = enc.AppendArrayDelim(e.buf) } @@ -360,11 +389,10 @@ func (e *Event) AnErr(key string, err error) *Event { case LogObjectMarshaler: return e.Object(key, m) case error: - if m == nil || isNilValue(m) { + if isNilValue(m) { return e - } else { - return e.Str(key, m.Error()) } + return e.Str(key, m.Error()) case string: return e.Str(key, m) default: @@ -378,20 +406,7 @@ func (e *Event) Errs(key string, errs []error) *Event { if e == nil { return e } - arr := Arr() - for _, err := range errs { - switch m := ErrorMarshalFunc(err).(type) { - case LogObjectMarshaler: - arr = arr.Object(m) - case error: - arr = arr.Err(m) - case string: - arr = arr.Str(m) - default: - arr = arr.Interface(m) - } - } - + arr := e.CreateArray().Errs(errs) return e.Array(key, arr) } @@ -407,21 +422,22 @@ func (e *Event) Err(err error) *Event { if e == nil { return e } + if e.stack && ErrorStackMarshaler != nil { switch m := ErrorStackMarshaler(err).(type) { case nil: + // do nothing case LogObjectMarshaler: - e.Object(ErrorStackFieldName, m) + e = e.Object(ErrorStackFieldName, m) case error: - if m != nil && !isNilValue(m) { - e.Str(ErrorStackFieldName, m.Error()) - } + e = e.Str(ErrorStackFieldName, m.Error()) case string: - e.Str(ErrorStackFieldName, m) + e = e.Str(ErrorStackFieldName, m) default: - e.Interface(ErrorStackFieldName, m) + e = e.Interface(ErrorStackFieldName, m) } } + return e.AnErr(ErrorFieldName, err) } diff --git a/event_test.go b/event_test.go index b601b87..f3f8261 100644 --- a/event_test.go +++ b/event_test.go @@ -1,9 +1,11 @@ +//go:build !binary_log // +build !binary_log package zerolog import ( "bytes" + "context" "errors" "io" "os" @@ -14,7 +16,7 @@ import ( type nilError struct{} func (nilError) Error() string { - return "" + return "nope" } func TestEvent_AnErr(t *testing.T) { @@ -30,9 +32,13 @@ func TestEvent_AnErr(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer - e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) - e.AnErr("err", tt.err) - _ = e.write() + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil) + e = e.AnErr("err", tt.err) + err := e.write() + if err != nil { + t.Errorf("Event.AnErr() error: %v", err) + } + if got, want := strings.TrimSpace(buf.String()), tt.want; got != want { t.Errorf("Event.AnErr() = %v, want %v", got, want) } @@ -50,30 +56,104 @@ func TestEvent_writeWithNil(t *testing.T) { } } -func TestEvent_ObjectWithNil(t *testing.T) { - var buf bytes.Buffer - e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) - _ = e.Object("obj", nil) - _ = e.write() - - want := `{"obj":null}` - got := strings.TrimSpace(buf.String()) - if got != want { - t.Errorf("Event.Object() = %q, want %q", got, want) - } +type loggableObject struct { + member string } -func TestEvent_EmbedObjectWithNil(t *testing.T) { - var buf bytes.Buffer - e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) - _ = e.EmbedObject(nil) - _ = e.write() +func (o loggableObject) MarshalZerologObject(e *Event) { + e.Str("member", o.member) +} - want := "{}" - got := strings.TrimSpace(buf.String()) - if got != want { - t.Errorf("Event.EmbedObject() = %q, want %q", got, want) - } +func TestEvent_Object(t *testing.T) { + t.Run("ObjectWithNil", func(t *testing.T) { + var buf bytes.Buffer + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil) + e = e.Object("obj", nil) + err := e.write() + if err != nil { + t.Errorf("Event.Object() error: %v", err) + } + + want := `{"obj":null}` + got := strings.TrimSpace(buf.String()) + if got != want { + t.Errorf("Event.Object()\ngot: %s\nwant: %s", got, want) + } + }) + + t.Run("EmbedObjectWithNil", func(t *testing.T) { + var buf bytes.Buffer + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil) + e = e.EmbedObject(nil) + err := e.write() + if err != nil { + t.Errorf("Event.EmbedObject() error: %v", err) + } + + want := "{}" + got := strings.TrimSpace(buf.String()) + if got != want { + t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want) + } + }) + + type contextKeyType struct{} + var contextKey = contextKeyType{} + + called := false + ctxHook := HookFunc(func(e *Event, level Level, message string) { + called = true + ctx := e.GetCtx() + if ctx == nil { + t.Errorf("expected context to be set in Event") + } + val := ctx.Value(contextKey) + if val == nil { + t.Errorf("expected context value, got %v", val) + } + e.Str("ctxValue", val.(string)) + e.Bool("stackValue", e.stack) + }) + + t.Run("ObjectWithFullContext", func(t *testing.T) { + called = false + ctx := context.WithValue(context.Background(), contextKey, "ctx-object") + + var buf bytes.Buffer + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, true, ctx, []Hook{ctxHook}) + e = e.Object("obj", loggableObject{member: "object-value"}) + e.Msg("hello") + + if !called { + t.Errorf("hook was not called") + } + + want := `{"obj":{"member":"object-value"},"ctxValue":"ctx-object","stackValue":true,"message":"hello"}` + got := strings.TrimSpace(buf.String()) + if got != want { + t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want) + } + }) + + t.Run("EmbedObjectWithFullContext", func(t *testing.T) { + called = false + ctx := context.WithValue(context.Background(), contextKey, "ctx-embed") + + var buf bytes.Buffer + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, ctx, []Hook{ctxHook}) + e = e.EmbedObject(loggableObject{member: "embedded-value"}) + e.Msg("hello") + + if !called { + t.Errorf("hook was not called") + } + + want := `{"member":"embedded-value","ctxValue":"ctx-embed","stackValue":false,"message":"hello"}` + got := strings.TrimSpace(buf.String()) + if got != want { + t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want) + } + }) } func TestEvent_WithNilEvent(t *testing.T) { @@ -83,7 +163,7 @@ func TestEvent_WithNilEvent(t *testing.T) { fixtures := makeFieldFixtures() types := map[string]func() *Event{ "Array": func() *Event { - arr := Arr() + arr := e.CreateArray() return e.Array("k", arr) }, "Bool": func() *Event { @@ -198,7 +278,7 @@ func TestEvent_WithNilEvent(t *testing.T) { return e.Times("k", fixtures.Times) }, "Dict": func() *Event { - d := Dict() + d := e.CreateDict() d.Str("greeting", "hello") return e.Dict("k", d) }, @@ -289,7 +369,7 @@ func TestEvent_WithNilEvent(t *testing.T) { func TestEvent_MsgFunc(t *testing.T) { var buf bytes.Buffer - e := newEvent(LevelWriterAdapter{&buf}, DebugLevel) + e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil) called := false e.MsgFunc(func() string { @@ -308,7 +388,7 @@ func TestEvent_MsgFunc(t *testing.T) { } func TestEvent_DoneHandler(t *testing.T) { - e := newEvent(nil, InfoLevel) + e := newEvent(nil, InfoLevel, false, nil, nil) // Set up a done handler to capture calls var called bool @@ -351,7 +431,7 @@ func TestEvent_Msg_ErrorHandlerNil(t *testing.T) { // Create a LevelWriter that always returns an error mockWriter := &badLevelWriter{err: errors.New("write error")} - e := newEvent(mockWriter, InfoLevel) + e := newEvent(mockWriter, InfoLevel, false, nil, nil) if e == nil { t.Fatal("Event should not be nil") } diff --git a/fields.go b/fields.go index 3d35ef3..b77a57a 100644 --- a/fields.go +++ b/fields.go @@ -1,7 +1,9 @@ package zerolog import ( + "context" "encoding/json" + "io" "net" "sort" "time" @@ -12,13 +14,13 @@ func isNilValue(i interface{}) bool { return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0 } -func appendFields(dst []byte, fields interface{}, stack bool) []byte { +func appendFields(dst []byte, fields interface{}, stack bool, ctx context.Context, hooks []Hook) []byte { switch fields := fields.(type) { case []interface{}: if n := len(fields); n&0x1 == 1 { // odd number fields = fields[:n-1] } - dst = appendFieldList(dst, fields, stack) + dst = appendFieldList(dst, fields, stack, ctx, hooks) case map[string]interface{}: keys := make([]string, 0, len(fields)) for key := range fields { @@ -28,14 +30,14 @@ func appendFields(dst []byte, fields interface{}, stack bool) []byte { kv := make([]interface{}, 2) for _, key := range keys { kv[0], kv[1] = key, fields[key] - dst = appendFieldList(dst, kv, stack) + dst = appendFieldList(dst, kv, stack, ctx, hooks) } } return dst } -func appendObject(dst []byte, obj LogObjectMarshaler) []byte { - e := newEvent(nil, 0) +func appendObject(dst []byte, obj LogObjectMarshaler, stack bool, ctx context.Context, hooks []Hook) []byte { + e := newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, stack, ctx, hooks) e.buf = e.buf[:0] // discard the beginning marker added by newEvent e.appendObject(obj) dst = append(dst, e.buf...) @@ -43,7 +45,7 @@ func appendObject(dst []byte, obj LogObjectMarshaler) []byte { return dst } -func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { +func appendFieldList(dst []byte, kvList []interface{}, stack bool, ctx context.Context, hooks []Hook) []byte { for i, n := 0, len(kvList); i < n; i += 2 { key, val := kvList[i], kvList[i+1] if key, ok := key.(string); ok { @@ -51,10 +53,6 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { } else { continue } - if val, ok := val.(LogObjectMarshaler); ok { - dst = appendObject(dst, val) - continue - } switch val := val.(type) { case string: dst = enc.AppendString(dst, val) @@ -62,12 +60,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { dst = enc.AppendBytes(dst, val) case error: switch m := ErrorMarshalFunc(val).(type) { + case nil: + dst = enc.AppendNil(dst) case LogObjectMarshaler: - dst = appendObject(dst, m) + dst = appendObject(dst, m, stack, ctx, hooks) case error: - if m == nil || isNilValue(m) { - dst = enc.AppendNil(dst) - } else { + if !isNilValue(m) { dst = enc.AppendString(dst, m.Error()) } case string: @@ -77,16 +75,20 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { } if stack && ErrorStackMarshaler != nil { - dst = enc.AppendKey(dst, ErrorStackFieldName) switch m := ErrorStackMarshaler(val).(type) { case nil: + // do nothing + case LogObjectMarshaler: + dst = enc.AppendKey(dst, ErrorStackFieldName) + dst = appendObject(dst, m, stack, ctx, hooks) case error: - if m != nil && !isNilValue(m) { - dst = enc.AppendString(dst, m.Error()) - } + dst = enc.AppendKey(dst, ErrorStackFieldName) + dst = enc.AppendString(dst, m.Error()) case string: + dst = enc.AppendKey(dst, ErrorStackFieldName) dst = enc.AppendString(dst, m) default: + dst = enc.AppendKey(dst, ErrorStackFieldName) dst = enc.AppendInterface(dst, m) } } @@ -94,12 +96,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { dst = enc.AppendArrayStart(dst) for i, err := range val { switch m := ErrorMarshalFunc(err).(type) { + case nil: + dst = enc.AppendNil(dst) case LogObjectMarshaler: - dst = appendObject(dst, m) + dst = appendObject(dst, m, stack, ctx, hooks) case error: - if m == nil || isNilValue(m) { - dst = enc.AppendNil(dst) - } else { + if !isNilValue(m) { dst = enc.AppendString(dst, m.Error()) } case string: @@ -116,7 +118,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { case []LogObjectMarshaler: dst = enc.AppendArrayStart(dst) for i, obj := range val { - dst = appendObject(dst, obj) + dst = appendObject(dst, obj, stack, ctx, hooks) if i < (len(val) - 1) { dst = enc.AppendArrayDelim(dst) } @@ -294,7 +296,11 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { case json.RawMessage: dst = appendJSON(dst, val) default: - dst = enc.AppendInterface(dst, val) + if lom, ok := val.(LogObjectMarshaler); ok { + dst = appendObject(dst, lom, stack, ctx, hooks) + } else { + dst = enc.AppendInterface(dst, val) + } } } return dst diff --git a/fixtures_test.go b/fixtures_test.go index a7c32a7..0d35db2 100644 --- a/fixtures_test.go +++ b/fixtures_test.go @@ -120,7 +120,7 @@ func makeFieldFixtures() *fieldFixtures { ipPfxV6 := net.IPNet{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x00}, Mask: net.CIDRMask(64, 128)} ipPfxs := []net.IPNet{ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6} macAddr := net.HardwareAddr{0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E} - errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e"), nil, errorObjectMarshalerImpl{fmt.Errorf("oops")}} + errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e"), nil, loggableError{fmt.Errorf("oops")}} ctx := context.Background() stringers := []fmt.Stringer{ipAddrs[0], durations[0]} rawJSONs := [][]byte{[]byte(`{"some":"json"}`), []byte(`{"longer":[1111,2222,3333,4444,5555]}`)} diff --git a/log.go b/log.go index ea2cd62..f615ee1 100644 --- a/log.go +++ b/log.go @@ -487,22 +487,21 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event { } return nil } - e := newEvent(l.w, level) + e := newEvent(l.w, level, l.stack, l.ctx, l.hooks) e.done = done - e.ch = l.hooks - e.ctx = l.ctx if level != NoLevel && LevelFieldName != "" { e.Str(LevelFieldName, LevelFieldMarshalFunc(level)) } if len(l.context) > 1 { e.buf = enc.AppendObjectData(e.buf, l.context) } - if l.stack { - e.Stack() - } return e } +func (l *Logger) scratchEvent() *Event { + return newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, l.stack, l.ctx, l.hooks) +} + // should returns true if the log event should be logged. func (l *Logger) should(lvl Level) bool { if l.w == nil { diff --git a/log_example_test.go b/log_example_test.go index 7b7af53..41c0639 100644 --- a/log_example_test.go +++ b/log_example_test.go @@ -192,12 +192,13 @@ func ExampleLogger_Log() { func ExampleEvent_Dict() { log := zerolog.New(os.Stdout) - log.Log(). - Str("foo", "bar"). - Dict("dict", zerolog.Dict(). - Str("bar", "baz"). - Int("n", 1), - ). + e := log.Log(). + Str("foo", "bar") + + e.Dict("dict", e.CreateDict(). + Str("bar", "baz"). + Int("n", 1), + ). Msg("hello world") // Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"} @@ -242,16 +243,17 @@ func (uu Users) MarshalZerologArray(a *zerolog.Array) { func ExampleEvent_Array() { log := zerolog.New(os.Stdout) - log.Log(). - Str("foo", "bar"). - Array("array", zerolog.Arr(). - Str("baz"). - Int(1). - Dict(zerolog.Dict(). - Str("bar", "baz"). - Int("n", 1), - ), - ). + e := log.Log(). + Str("foo", "bar") + + e.Array("array", e.CreateArray(). + Str("baz"). + Int(1). + Dict(e.CreateDict(). + Str("bar", "baz"). + Int("n", 1), + ), + ). Msg("hello world") // Output: {"foo":"bar","array":["baz",1,{"bar":"baz","n":1}],"message":"hello world"} @@ -380,27 +382,29 @@ func ExampleEvent_Fields_slice() { } func ExampleContext_Dict() { - log := zerolog.New(os.Stdout).With(). - Str("foo", "bar"). - Dict("dict", zerolog.Dict(). - Str("bar", "baz"). - Int("n", 1), - ).Logger() + ctx := zerolog.New(os.Stdout).With(). + Str("foo", "bar") - log.Log().Msg("hello world") + logger := ctx.Dict("dict", ctx.CreateDict(). + Str("bar", "baz"). + Int("n", 1), + ).Logger() + + logger.Log().Msg("hello world") // Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"} } func ExampleContext_Array() { - log := zerolog.New(os.Stdout).With(). - Str("foo", "bar"). - Array("array", zerolog.Arr(). - Str("baz"). - Int(1), - ).Logger() + ctx := zerolog.New(os.Stdout).With(). + Str("foo", "bar") - log.Log().Msg("hello world") + logger := ctx.Array("array", ctx.CreateArray(). + Str("baz"). + Int(1), + ).Logger() + + logger.Log().Msg("hello world") // Output: {"foo":"bar","array":["baz",1],"message":"hello world"} } diff --git a/log_test.go b/log_test.go index 7efde9f..2456634 100644 --- a/log_test.go +++ b/log_test.go @@ -163,7 +163,7 @@ func TestWithPlurals(t *testing.T) { Strs("strings_nil", nil). Stringers("stringers", []fmt.Stringer{net.IP{127, 0, 0, 1}, nil}). Stringers("stringers_nil", nil). - Errs("errs", []error{errors.New("some error"), errors.New("some other error"), nil, errorObjectMarshalerImpl{fmt.Errorf("oops")}}). + Errs("errs", []error{errors.New("some error"), errors.New("some other error"), nil, loggableError{fmt.Errorf("oops")}, nonLoggableError{fmt.Errorf("whoops"), 401}}). Bools("bool", []bool{true, false}). Ints("int", []int{1, 2}). Ints8("int8", []int8{2, 3}). @@ -182,7 +182,7 @@ func TestWithPlurals(t *testing.T) { log := ctx.Logger() log.Log().Msg("") if got, want := decodeIfBinaryToString(out.Bytes()), - `{"strings":["foo","bar"],"strings_nil":[],"stringers":["127.0.0.1",null],"stringers_nil":null,"errs":["some error","some other error",null,{"error":"OOPS"}],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { + `{"strings":["foo","bar"],"strings_nil":[],"stringers":["127.0.0.1",null],"stringers_nil":null,"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -268,7 +268,7 @@ func TestFieldsMap_Arrays(t *testing.T) { "times": []time.Time{{}}, "objs": []fixtureObj{{"a", "b", 1}}, }).Msg("") - // special case: []uint8 is logged as base64 string so we use "," for 44 + // special case: []uint8 are logged as base64 string so we use "," for 44 if got, want := decodeIfBinaryToString(out.Bytes()), `{"bools":[true],"durs":[1000],"errors":["some error"],"floats32":[11],"floats64":[12],"ints":[1],"ints16":[3],"ints32":[4],"ints64":[5],"ints8":[1],"ipnets":["2001:db8:85a3::8a2e:370:7334/64"],"ipv6s":["2001:db8:85a3::8a2e:370:7334"],"macaddrs":["ABorPE1e"],"objs":[{"Pub":"a","tag":"b"}],"strings":["foo"],"times":["0001-01-01T00:00:00Z"],"uint8s":",","uints":[6],"uints16":[8],"uints32":[9],"uints64":[10]}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } @@ -278,13 +278,13 @@ func TestFieldsErr(t *testing.T) { out := &bytes.Buffer{} log := New(out) log.Log().Fields(map[string]interface{}{ - "nil": nil, - "nilerror": err, - "error": errors.New("some error"), - "loggable": loggableError{errors.New("loggable")}, - "marshal": errorObjectMarshalerImpl{fmt.Errorf("oops")}, + "nil": nil, + "nilerror": err, + "error": errors.New("some error"), + "loggable": loggableError{errors.New("loggable")}, + "non-loggable": nonLoggableError{fmt.Errorf("oops"), 401}, }).Msg("") - if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"some error","loggable":{"message":"loggable loggableError"},"marshal":{"error":"OOPS"},"nil":null,"nilerror":null}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"some error","loggable":{"l":"LOGGABLE"},"nil":null,"nilerror":null,"non-loggable":"oops"}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -293,9 +293,9 @@ func TestFieldsErrs(t *testing.T) { out := &bytes.Buffer{} log := New(out) log.Log().Fields(map[string]interface{}{ - "errors": []error{errors.New("some error"), nil, err, loggableError{errors.New("loggable")}, errorObjectMarshalerImpl{fmt.Errorf("oops")}}, + "errors": []error{errors.New("some error"), nil, err, loggableError{errors.New("loggable")}, nonLoggableError{fmt.Errorf("oops"), 404}}, }).Msg("") - if got, want := decodeIfBinaryToString(out.Bytes()), `{"errors":["some error",null,null,{"message":"loggable loggableError"},{"error":"OOPS"}]}`+"\n"; got != want { + if got, want := decodeIfBinaryToString(out.Bytes()), `{"errors":["some error",null,null,{"l":"LOGGABLE"},"oops"]}`+"\n"; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } } @@ -606,6 +606,7 @@ func TestFieldsDisabled(t *testing.T) { Ctx(context.Background()). Any("any", struct{ A string }{"test"}). Interface("interface", fixtureObj{"a", "z", 1}). + Err(errors.New("some error")). Msg("") if got, want := decodeIfBinaryToString(out.Bytes()), ""; got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -863,363 +864,6 @@ func TestOutputWithTimestamp(t *testing.T) { } } -type loggableError struct { - error -} - -func (l loggableError) MarshalZerologObject(e *Event) { - if l.error == nil { - return - } - e.Str("message", l.error.Error()+" loggableError") -} - -type nonLoggable struct { - where string - how int - what string -} -type wrappedError struct { - error - msg string -} - -func (w wrappedError) Error() string { - if w.error == nil { - return w.msg - } - return w.error.Error() + ": " + w.msg -} - -func TestErrorMarshalFunc_Default(t *testing.T) { - // test default behaviour - testErrorMarshalFunc(t, nil, "default", - "", - "null", - - `"zot"`, - `"zot"`, - - `{"message":"log loggableError"}`, - `{"message":"log loggableError"}`, - - "[]", - `"foo","bar"`, - ) -} -func TestErrorMarshalFunc_AppendedString(t *testing.T) { - // test returning an appended string from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - if err == nil { - return nil - } - return err.Error() + ": marshaled" - }, "appended string", - "", - "null", - - `"zot: marshaled"`, - `"zot: marshaled"`, - - `"log: marshaled"`, - `{"message":"log loggableError"}`, - - "[]", - `"foo: marshaled","bar: marshaled"`, - ) -} -func TestErrorMarshalFunc_NilError(t *testing.T) { - // test returning an nil error from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - return error(nil) - }, "nil error", - "", - "null", - - "", - "null", - - "", - `{"message":"log loggableError"}`, - - "[]", - "null,null", - ) -} - -func TestErrorMarshalFunc_UntypedNil(t *testing.T) { - // test returning an untyped nil from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - return nil - }, "untyped nil", - "", - "null", - - "", - "null", - - "", - `{"message":"log loggableError"}`, - - "[]", - "null,null", - ) -} - -func TestErrorMarshalFunc_WrappedError(t *testing.T) { - // test returning an wrapped error from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - if err == nil { - return nil - } else if we, ok := err.(wrappedError); ok { - return we - } else { - return wrappedError{err, "addendum"} - } - }, "wrapped error", - "", - "null", - - `"zot: addendum"`, - `"zot: addendum"`, - - `"log: addendum"`, - `{"message":"log loggableError"}`, - - "[]", - `"foo: addendum","bar: addendum"`, - ) -} - -func TestErrorMarshalFunc_LoggableType(t *testing.T) { - // test returning a loggable type from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - return loggableError{err} - }, "loggable type", - "{}", - "null", - - `{"message":"zot loggableError"}`, - `{"message":"zot loggableError"}`, - - `{"message":"log loggableError"}`, - `{"message":"log loggableError"}`, - - "[]", - `{"message":"foo loggableError"},{"message":"bar loggableError"}`, - ) -} -func TestErrorMarshalFunc_String(t *testing.T) { - // test returning a string type from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - return "i'm an err" // return just a string - }, "string type", - `"i'm an err"`, - "null", - - `"i'm an err"`, - `"i'm an err"`, - - `"i'm an err"`, - `{"message":"log loggableError"}`, - - "[]", - `"i'm an err","i'm an err"`, - ) -} - -func TestErrorMarshalFunc_NonLoggableType(t *testing.T) { - // test returning a non-loggable type from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - if err == nil { - return nil - } - return nonLoggable{where: "here", how: 42, what: err.Error()} - }, "non-loggable type", - "", - "null", - - "{}", - "{}", - - "{}", - `{"message":"log loggableError"}`, - - "[]", - "{},{}", - ) -} - -func TestErrorMarshalFunc_NilMarshal(t *testing.T) { - // test returning a nil from ErrorMarshalFunc - testErrorMarshalFunc(t, func(err error) interface{} { - var nilerr error = nil - return nilerr - }, "nil-marshal", - "", - "null", - - "", - "null", - - "", - `{"message":"log loggableError"}`, - - "[]", - "null,null", - ) -} - -func testErrorMarshalFunc(t *testing.T, - errFunc func(err error) interface{}, - testcase string, - - whenNil string, - whenNilField string, - - hasValue string, - hasValueField string, - - whenLoggable string, - whenLoggableField string, - - errsNilValue string, - errsValue string, -) { - const suffix = `"message":"msg"}` - - if errFunc != nil { - originalErrorMarshalFunc := ErrorMarshalFunc - defer func() { - ErrorMarshalFunc = originalErrorMarshalFunc - }() - - ErrorMarshalFunc = errFunc - } - - var err error - wantNil := `{` - if whenNil != "" { - wantNil = wantNil + `"error":` + whenNil + `,` - } - wantNil = wantNil + suffix - - wantNilField := `{"err":` + whenNilField + `,` + suffix - err = nil - testContextErrorMarshalFunc(t, testcase+" / context.Err(nil)", wantNil, func(ctx Context) Context { - ctx = ctx.Err(err) - return ctx - }) - testEventErrorMarshalFunc(t, testcase+" / event.Err(nil)", wantNil, func(e *Event) *Event { - e.Err(err) - return e - }) - testFieldsErrorMarshalFunc(t, testcase+" / field.Err(nil)", wantNilField, func(e *Event) *Event { - e.Fields(map[string]interface{}{"err": err}) - return e - }) - - wantErr := `{` - if hasValue != "" { - wantErr = wantErr + `"error":` + hasValue + `,` - } - wantErr = wantErr + suffix - wantErrField := `{"err":` + hasValueField + `,` + suffix - err = errors.New("zot") - testContextErrorMarshalFunc(t, testcase+" / context.Err(error)", wantErr, func(ctx Context) Context { - ctx = ctx.Err(err) - return ctx - }) - testEventErrorMarshalFunc(t, testcase+" / event.Err(error)", wantErr, func(e *Event) *Event { - e.Err(err) - return e - }) - testFieldsErrorMarshalFunc(t, testcase+" / field.Err(error)", wantErrField, func(e *Event) *Event { - e.Fields(map[string]interface{}{"err": err}) - return e - }) - - wantLoggable := `{` - if whenLoggable != "" { - wantLoggable = wantLoggable + `"error":` + whenLoggable + `,` - } - wantLoggable = wantLoggable + suffix - wantLoggableField := `{"err":` + whenLoggableField + `,` + suffix - err = loggableError{errors.New("log")} - testContextErrorMarshalFunc(t, testcase+" / context.Err(loggable)", wantLoggable, func(ctx Context) Context { - ctx = ctx.Err(err) - return ctx - }) - testEventErrorMarshalFunc(t, testcase+" / event.Err(loggable)", wantLoggable, func(e *Event) *Event { - e.Err(err) - return e - }) - testFieldsErrorMarshalFunc(t, testcase+" / field.Err(loggable)", wantLoggableField, func(e *Event) *Event { - e.Fields(map[string]interface{}{"err": err}) - return e - }) - - wantErrsNil := `{"errs":` + errsNilValue + `,` + suffix - var errs []error = nil - testContextErrorMarshalFunc(t, testcase+" / context.Errs(nil)", wantErrsNil, func(ctx Context) Context { - ctx = ctx.Errs("errs", errs) - return ctx - }) - testEventErrorMarshalFunc(t, testcase+" / event.Errs(nil)", wantErrsNil, func(e *Event) *Event { - e.Errs("errs", errs) - return e - }) - testFieldsErrorMarshalFunc(t, testcase+" / field.Errs(nil)", wantErrsNil, func(e *Event) *Event { - e.Fields(map[string]interface{}{"errs": errs}) - return e - }) - - wantErrs := `{"errs":[` + errsValue + `],` + suffix - errs = []error{errors.New("foo"), errors.New("bar")} - testContextErrorMarshalFunc(t, testcase+" / context.Errs([])", wantErrs, func(ctx Context) Context { - ctx = ctx.Errs("errs", []error{errors.New("foo"), errors.New("bar")}) - return ctx - }) - testEventErrorMarshalFunc(t, testcase+" / event.Errs([])", wantErrs, func(e *Event) *Event { - e.Errs("errs", errs) - return e - }) - testFieldsErrorMarshalFunc(t, testcase+" / field.Errs([])", wantErrs, func(e *Event) *Event { - e.Fields(map[string]interface{}{"errs": errs}) - return e - }) -} - -func testEventErrorMarshalFunc(t *testing.T, testcase string, wants string, test func(e *Event) *Event) { - out := &bytes.Buffer{} - logger := New(out) - test(logger.Log()).Msg("msg") - if got, want := decodeIfBinaryToString(out.Bytes()), wants+"\n"; got != want { - t.Errorf("%s output:\ngot: %v\nwant: %v", testcase, got, want) - } -} - -func testFieldsErrorMarshalFunc(t *testing.T, testcase string, wants string, test func(e *Event) *Event) { - out := &bytes.Buffer{} - logger := New(out) - test(logger.Log()).Msg("msg") - if got, want := decodeIfBinaryToString(out.Bytes()), wants+"\n"; got != want { - t.Errorf("%s output:\ngot: %v\nwant: %v", testcase, got, want) - } -} - -func testContextErrorMarshalFunc(t *testing.T, testcase string, wants string, test func(ctx Context) Context) { - out := &bytes.Buffer{} - ctx := New(out).With() - - ctx = test(ctx) - logger := ctx.Logger() - logger.Log().Msg("msg") - if got, want := decodeIfBinaryToString(out.Bytes()), wants+"\n"; got != want { - t.Errorf("%s output:\ngot: %v\nwant: %v", testcase, got, want) - } -} - func TestCallerMarshalFunc(t *testing.T) { out := &bytes.Buffer{} log := New(out)