Skip to content

Commit 46db75f

Browse files
committed
slogtest errors if we fail to stringify a field value
1 parent f064b0d commit 46db75f

File tree

3 files changed

+61
-16
lines changed

3 files changed

+61
-16
lines changed

internal/entryhuman/entry.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,34 @@ func reset(w io.Writer, termW io.Writer) {
5656
}
5757
}
5858

59-
func formatValue(v interface{}) string {
59+
func formatValue(v interface{}) (string, error) {
6060
if vr, ok := v.(driver.Valuer); ok {
6161
var err error
6262
v, err = vr.Value()
6363
if err != nil {
64-
return fmt.Sprintf("error calling Value: %v", err)
64+
return "", xerrors.Errorf("error calling Value: %w", err)
6565
}
6666
}
6767
if v == nil {
68-
return "<nil>"
68+
return "<nil>", nil
6969
}
7070
typ := reflect.TypeOf(v)
7171
switch typ.Kind() {
7272
case reflect.Struct, reflect.Map:
7373
byt, err := json.Marshal(v)
7474
if err != nil {
7575
// don't panic
76-
return "!! Error while marshalling value !!"
76+
return "", xerrors.Errorf("error marshalling value: %w", err)
7777
}
78-
return string(byt)
78+
return string(byt), nil
7979
case reflect.Slice:
8080
// Byte slices are optimistically readable.
8181
if typ.Elem().Kind() == reflect.Uint8 {
82-
return fmt.Sprintf("%q", v)
82+
return fmt.Sprintf("%q", v), nil
8383
}
8484
fallthrough
8585
default:
86-
return quote(fmt.Sprintf("%+v", v))
86+
return quote(fmt.Sprintf("%+v", v)), nil
8787
}
8888
}
8989

@@ -175,6 +175,10 @@ func writeValueFast(w io.Writer, v interface{}) (bool, error) {
175175
}
176176
}
177177

178+
type Formatter struct {
179+
ErrorCallback func(slog.Field, error)
180+
}
181+
178182
// Fmt returns a human readable format for ent. Assumes we have a bytes.Buffer
179183
// which we will more easily be able to assume underlying reallocation of it's size is possible
180184
// if necessary than for an arbitrary io.Writer/io.StringWriter
@@ -188,6 +192,10 @@ func writeValueFast(w io.Writer, v interface{}) (bool, error) {
188192
// for extra lines in a log so if we did it here, the fields would be indented
189193
// twice in test logs. So the Stderr logger indents all the fields itself.
190194
func Fmt(buf *bytes.Buffer, termW io.Writer, ent slog.SinkEntry) {
195+
Formatter{}.Fmt(buf, termW, ent)
196+
}
197+
198+
func (f Formatter) Fmt(buf *bytes.Buffer, termW io.Writer, ent slog.SinkEntry) {
191199
reset(buf, termW)
192200

193201
// Timestamp + space
@@ -239,12 +247,12 @@ func Fmt(buf *bytes.Buffer, termW io.Writer, ent slog.SinkEntry) {
239247

240248
// Find a multiline field without mutating ent.Fields.
241249
multiIdx := -1
242-
for i, f := range ent.Fields {
250+
for i, fld := range ent.Fields {
243251
if multilineVal != "" {
244252
break
245253
}
246254
var s string
247-
switch v := f.Value.(type) {
255+
switch v := fld.Value.(type) {
248256
case string:
249257
s = v
250258
case error, xerrors.Formatter:
@@ -255,27 +263,35 @@ func Fmt(buf *bytes.Buffer, termW io.Writer, ent slog.SinkEntry) {
255263
continue
256264
}
257265
multiIdx = i
258-
multilineKey = f.Name
266+
multilineKey = fld.Name
259267
multilineVal = s
260268
break
261269
}
262270

263271
// Print fields (skip multiline field index).
264-
for i, f := range ent.Fields {
272+
for i, fld := range ent.Fields {
265273
if i == multiIdx {
266274
continue
267275
}
268276
if i < len(ent.Fields) {
269277
buf.WriteString(tab)
270278
}
271279

272-
buf.WriteString(render(termW, keyStyle, quoteKey(f.Name)))
280+
buf.WriteString(render(termW, keyStyle, quoteKey(fld.Name)))
273281
buf.WriteString(render(termW, equalsStyle, "="))
274282

275-
if ok, err := writeValueFast(buf, f.Value); err != nil {
276-
// return err
283+
if ok, err := writeValueFast(buf, fld.Value); err != nil && f.ErrorCallback != nil {
284+
f.ErrorCallback(fld, err)
277285
} else if !ok {
278-
buf.WriteString(formatValue(f.Value))
286+
s, err := formatValue(fld.Value)
287+
if err != nil {
288+
if f.ErrorCallback != nil {
289+
f.ErrorCallback(fld, err)
290+
}
291+
buf.WriteString(err.Error())
292+
continue
293+
}
294+
buf.WriteString(s)
279295
}
280296
}
281297

sloggers/slogtest/t.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ func (ts *testSink) LogEntry(_ context.Context, ent slog.SinkEntry) {
9999

100100
var sb bytes.Buffer
101101
// The testing package logs to stdout and not stderr.
102-
entryhuman.Fmt(&sb, os.Stdout, ent)
102+
f := entryhuman.Formatter{
103+
ErrorCallback: func(field slog.Field, err error) {
104+
ts.tb.Errorf("failed to log field %q: %v", field.Name, err)
105+
},
106+
}
107+
f.Fmt(&sb, os.Stdout, ent)
103108

104109
switch ent.Level {
105110
case slog.LevelDebug, slog.LevelInfo, slog.LevelWarn:

sloggers/slogtest/t_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package slogtest_test
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"testing"
78

89
"golang.org/x/xerrors"
@@ -175,13 +176,31 @@ func TestSkipCleanup(t *testing.T) {
175176
assert.Len(t, "no cleanups", 0, tb.cleanups)
176177
}
177178

179+
func TestUnmarshalable(t *testing.T) {
180+
t.Parallel()
181+
tb := &fakeTB{}
182+
l := slogtest.Make(tb, &slogtest.Options{})
183+
s := &selfRef{}
184+
s.Ref = s
185+
s2 := selfRef{Ref: s} // unmarshalable because it contains a cyclic ref
186+
l.Info(bg, "hello", slog.F("self", s2))
187+
assert.Equal(t, "errors", 1, tb.errors)
188+
assert.Len(t, "len errorfs", 1, tb.errorfs)
189+
assert.True(t, "errorfs", strings.Contains(tb.errorfs[0], "failed to log field \"self\":"))
190+
}
191+
192+
type selfRef struct {
193+
Ref *selfRef
194+
}
195+
178196
var bg = context.Background()
179197

180198
type fakeTB struct {
181199
testing.TB
182200

183201
logs int
184202
errors int
203+
errorfs []string
185204
fatals int
186205
cleanups []func()
187206
}
@@ -196,6 +215,11 @@ func (tb *fakeTB) Error(v ...interface{}) {
196215
tb.errors++
197216
}
198217

218+
func (tb *fakeTB) Errorf(msg string, v ...interface{}) {
219+
tb.errors++
220+
tb.errorfs = append(tb.errorfs, fmt.Sprintf(msg, v...))
221+
}
222+
199223
func (tb *fakeTB) Fatal(v ...interface{}) {
200224
tb.fatals++
201225
panic("")

0 commit comments

Comments
 (0)