slog: backport changes from stdlib
The standard library version of slog has diverged from this
version somewhat; bring them back into sync, as much as possible.
Since moving to the standard library, a Handler rule has changed: a
Handler should ignore the zero slog.Attr, but not an Attr whose key is
empty but whose value is non-zero. Change the check in slogtest to
reflect that.
Fixes golang/go#59727.
Change-Id: Idd2084b84a3faf9b1796051af1578aee6558ed02
Reviewed-on: https://go-review.googlesource.com/c/exp/+/486795
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/attr.go b/slog/attr.go
index 29431cb..cd3bacc 100644
--- a/slog/attr.go
+++ b/slog/attr.go
@@ -82,3 +82,9 @@
func (a Attr) String() string {
return fmt.Sprintf("%s=%s", a.Key, a.Value)
}
+
+// isEmpty reports whether a has an empty key and a nil value.
+// That can be written as Attr{} or Any("", nil).
+func (a Attr) isEmpty() bool {
+ return a.Key == "" && a.Value.num == 0 && a.Value.any == nil
+}
diff --git a/slog/benchmarks/handlers.go b/slog/benchmarks/handlers.go
index 6bacee0..90c7aed 100644
--- a/slog/benchmarks/handlers.go
+++ b/slog/benchmarks/handlers.go
@@ -48,11 +48,12 @@
buf.WriteByte(' ')
buf.WriteString("msg=")
buf.WriteString(r.Message)
- r.Attrs(func(a slog.Attr) {
+ r.Attrs(func(a slog.Attr) bool {
buf.WriteByte(' ')
buf.WriteString(a.Key)
buf.WriteByte('=')
h.appendValue(buf, a.Value)
+ return true
})
buf.WriteByte('\n')
_, err := h.w.Write(*buf)
diff --git a/slog/benchmarks/handlers_test.go b/slog/benchmarks/handlers_test.go
index f29eae9..9847f67 100644
--- a/slog/benchmarks/handlers_test.go
+++ b/slog/benchmarks/handlers_test.go
@@ -38,6 +38,6 @@
func attrSlice(r slog.Record) []slog.Attr {
var as []slog.Attr
- r.Attrs(func(a slog.Attr) { as = append(as, a) })
+ r.Attrs(func(a slog.Attr) bool { as = append(as, a); return true })
return as
}
diff --git a/slog/example_depth_test.go b/slog/example_wrap_test.go
similarity index 73%
rename from slog/example_depth_test.go
rename to slog/example_wrap_test.go
index 599cf16..c7bfbb7 100644
--- a/slog/example_depth_test.go
+++ b/slog/example_wrap_test.go
@@ -17,24 +17,21 @@
// Infof is an example of a user-defined logging function that wraps slog.
// The log record contains the source position of the caller of Infof.
-func Infof(format string, args ...any) {
- l := slog.Default()
- if !l.Enabled(context.Background(), slog.LevelInfo) {
+func Infof(logger *slog.Logger, format string, args ...any) {
+ if !logger.Enabled(context.Background(), slog.LevelInfo) {
return
}
var pcs [1]uintptr
runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
- _ = l.Handler().Handle(context.Background(), r)
+ _ = logger.Handler().Handle(context.Background(), r)
}
func Example_wrapping() {
- defer func(l *slog.Logger) { slog.SetDefault(l) }(slog.Default())
-
replace := func(groups []string, a slog.Attr) slog.Attr {
// Remove time.
if a.Key == slog.TimeKey && len(groups) == 0 {
- a.Key = ""
+ return slog.Attr{}
}
// Remove the directory from the source's filename.
if a.Key == slog.SourceKey {
@@ -43,9 +40,8 @@
return a
}
logger := slog.New(slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}.NewTextHandler(os.Stdout))
- slog.SetDefault(logger)
- Infof("message, %s", "formatted")
+ Infof(logger, "message, %s", "formatted")
// Output:
- // level=INFO source=example_depth_test.go:47 msg="message, formatted"
+ // level=INFO source=example_wrap_test.go:43 msg="message, formatted"
}
diff --git a/slog/handler.go b/slog/handler.go
index c1b9037..862fb3a 100644
--- a/slog/handler.go
+++ b/slog/handler.go
@@ -51,8 +51,9 @@
// Handle methods that produce output should observe the following rules:
// - If r.Time is the zero time, ignore the time.
// - If r.PC is zero, ignore it.
- // - If an Attr's key is the empty string and the value is not a group,
- // ignore the Attr.
+ // - Attr's values should be resolved.
+ // - If an Attr's key and value are both the zero value, ignore the Attr.
+ // This can be tested with attr.Equal(Attr{}).
// - If a group's key is empty, inline the group's Attrs.
// - If a group has no Attrs (even if it has a non-empty key),
// ignore it.
@@ -61,7 +62,6 @@
// WithAttrs returns a new Handler whose attributes consist of
// both the receiver's attributes and the arguments.
// The Handler owns the slice: it may retain, modify or discard it.
- // [Logger.With] will resolve the Attrs.
WithAttrs(attrs []Attr) Handler
// WithGroup returns a new Handler with the given group appended to
@@ -333,8 +333,9 @@
defer s.prefix.Free()
s.prefix.WriteString(s.h.groupPrefix)
s.openGroups()
- r.Attrs(func(a Attr) {
+ r.Attrs(func(a Attr) bool {
s.appendAttr(a)
+ return true
})
if s.h.json {
// Close all open groups.
@@ -440,26 +441,22 @@
// It handles replacement and checking for an empty key.
// after replacement).
func (s *handleState) appendAttr(a Attr) {
- v := a.Value
- // Elide a non-group with an empty key.
- if a.Key == "" && v.Kind() != KindGroup {
- return
- }
- if rep := s.h.opts.ReplaceAttr; rep != nil && v.Kind() != KindGroup {
+ if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup {
var gs []string
if s.groups != nil {
gs = *s.groups
}
- a = rep(gs, Attr{a.Key, v})
- if a.Key == "" {
- return
- }
- // Although all attributes in the Record are already resolved,
- // This one came from the user, so it may not have been.
- v = a.Value.Resolve()
+ // Resolve before calling ReplaceAttr, so the user doesn't have to.
+ a.Value = a.Value.Resolve()
+ a = rep(gs, a)
}
- if v.Kind() == KindGroup {
- attrs := v.Group()
+ a.Value = a.Value.Resolve()
+ // Elide empty Attrs.
+ if a.isEmpty() {
+ return
+ }
+ if a.Value.Kind() == KindGroup {
+ attrs := a.Value.Group()
// Output only non-empty groups.
if len(attrs) > 0 {
// Inline a group with an empty key.
@@ -475,7 +472,7 @@
}
} else {
s.appendKey(a.Key)
- s.appendValue(v)
+ s.appendValue(a.Value)
}
}
diff --git a/slog/handler_test.go b/slog/handler_test.go
index 8b1b669..6c39025 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -109,12 +109,10 @@
func TestJSONAndTextHandlers(t *testing.T) {
ctx := context.Background()
- // ReplaceAttr functions
-
// remove all Attrs
removeAll := func(_ []string, a Attr) Attr { return Attr{} }
- attrs := []Attr{String("a", "one"), Int("b", 2), Any("", "ignore me")}
+ attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
preAttrs := []Attr{Int("pre", 3), String("x", "y")}
for _, test := range []struct {
@@ -133,6 +131,12 @@
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
},
{
+ name: "empty key",
+ attrs: append(slices.Clip(attrs), Any("", "v")),
+ wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
+ wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
+ },
+ {
name: "cap keys",
replace: upperCaseKey,
attrs: attrs,
@@ -232,6 +236,16 @@
wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
},
{
+ // Test resolution when there is no ReplaceAttr function.
+ name: "resolve",
+ attrs: []Attr{
+ Any("", &replace{Value{}}), // should be elided
+ Any("name", logValueName{"Ren", "Hoek"}),
+ },
+ wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
+ wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
+ },
+ {
name: "with-group",
replace: removeKeys(TimeKey, LevelKey),
with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
@@ -297,7 +311,7 @@
wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
},
} {
- r := NewRecord(testTime, LevelInfo, "message", 1)
+ r := NewRecord(testTime, LevelInfo, "message", 0)
r.AddAttrs(test.attrs...)
var buf bytes.Buffer
opts := HandlerOptions{ReplaceAttr: test.replace}
diff --git a/slog/internal/testutil/testutil.go b/slog/internal/testutil/testutil.go
index 4f59d0a..540fefb 100644
--- a/slog/internal/testutil/testutil.go
+++ b/slog/internal/testutil/testutil.go
@@ -12,7 +12,7 @@
// to make example output deterministic.
func RemoveTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
- a.Key = ""
+ return slog.Attr{}
}
return a
}
diff --git a/slog/json_handler.go b/slog/json_handler.go
index 1cd1f59..047cb74 100644
--- a/slog/json_handler.go
+++ b/slog/json_handler.go
@@ -80,7 +80,7 @@
// Values are formatted as with encoding/json.Marshal, with the following
// exceptions:
// - Floating-point NaNs and infinities are formatted as one of the strings
-// "NaN", "+Inf" or "-Inf".
+// "NaN", "Infinity" or "-Infinity".
// - Levels are formatted as with Level.String.
// - HTML characters are not escaped.
//
@@ -114,9 +114,9 @@
// json.Marshal fails on special floats, so handle them here.
switch {
case math.IsInf(f, 1):
- s.buf.WriteString(`"+Inf"`)
+ s.buf.WriteString(`"Infinity"`)
case math.IsInf(f, -1):
- s.buf.WriteString(`"-Inf"`)
+ s.buf.WriteString(`"-Infinity"`)
case math.IsNaN(f):
s.buf.WriteString(`"NaN"`)
default:
@@ -136,13 +136,14 @@
s.appendTime(v.Time())
case KindAny:
a := v.Any()
- if err, ok := a.(error); ok {
+ _, jm := a.(json.Marshaler)
+ if err, ok := a.(error); ok && !jm {
s.appendString(err.Error())
} else {
return appendJSONMarshal(s.buf, a)
}
default:
- panic(fmt.Sprintf("bad kind: %d", v.Kind()))
+ panic(fmt.Sprintf("bad kind: %s", v.Kind()))
}
return nil
}
diff --git a/slog/json_handler_test.go b/slog/json_handler_test.go
index 64b3b4d..fb93713 100644
--- a/slog/json_handler_test.go
+++ b/slog/json_handler_test.go
@@ -13,6 +13,7 @@
"io"
"math"
"os"
+ "path/filepath"
"strings"
"testing"
"time"
@@ -67,6 +68,12 @@
return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
}
+type jsonMarshalerError struct {
+ jsonMarshaler
+}
+
+func (jsonMarshalerError) Error() string { return "oops" }
+
func TestAppendJSONValue(t *testing.T) {
// On most values, jsonAppendAttrValue should agree with json.Marshal.
for _, value := range []any{
@@ -82,6 +89,7 @@
time.Minute,
testTime,
jsonMarshaler{"xyz"},
+ jsonMarshalerError{jsonMarshaler{"pqr"}},
} {
got := jsonValueString(t, AnyValue(value))
want, err := marshalJSON(value)
@@ -111,8 +119,8 @@
want string
}{
{math.NaN(), `"NaN"`},
- {math.Inf(+1), `"+Inf"`},
- {math.Inf(-1), `"-Inf"`},
+ {math.Inf(+1), `"Infinity"`},
+ {math.Inf(-1), `"-Infinity"`},
{LevelWarn, `"WARN"`},
} {
got := jsonValueString(t, AnyValue(test.value))
@@ -202,7 +210,7 @@
}),
}
- outFile, err := os.Create("/tmp/bench.log")
+ outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
if err != nil {
b.Fatal(err)
}
diff --git a/slog/logger.go b/slog/logger.go
index 8f021e6..173b34f 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -89,7 +89,7 @@
func (l *Logger) Handler() Handler { return l.handler }
// With returns a new Logger that includes the given arguments, converted to
-// Attrs as in [Logger.Log] and resolved.
+// Attrs as in [Logger.Log].
// The Attrs will be added to each output from the Logger.
// The new Logger shares the old Logger's context.
// The new Logger's handler is the result of calling WithAttrs on the receiver's
@@ -110,6 +110,8 @@
// WithGroup returns a new Logger that starts a group. The keys of all
// attributes added to the Logger will be qualified by the given name.
+// (How that qualification happens depends on the [Handler.WithGroup]
+// method of the Logger's Handler.)
// The new Logger shares the old Logger's context.
//
// The new Logger's handler is the result of calling WithGroup on the receiver's
diff --git a/slog/logger_test.go b/slog/logger_test.go
index 2f04540..9694b54 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -13,6 +13,7 @@
"regexp"
"runtime"
"strings"
+ "sync"
"testing"
"time"
@@ -398,12 +399,15 @@
}
type captureHandler struct {
+ mu sync.Mutex
r Record
attrs []Attr
groups []string
}
func (h *captureHandler) Handle(ctx context.Context, r Record) error {
+ h.mu.Lock()
+ defer h.mu.Unlock()
h.r = r
return nil
}
@@ -411,14 +415,22 @@
func (*captureHandler) Enabled(context.Context, Level) bool { return true }
func (c *captureHandler) WithAttrs(as []Attr) Handler {
- c2 := *c
- c2.attrs = concat(c2.attrs, as)
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ var c2 captureHandler
+ c2.r = c.r
+ c2.groups = c.groups
+ c2.attrs = concat(c.attrs, as)
return &c2
}
func (c *captureHandler) WithGroup(name string) Handler {
- c2 := *c
- c2.groups = append(slices.Clip(c2.groups), name)
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ var c2 captureHandler
+ c2.r = c.r
+ c2.attrs = c.attrs
+ c2.groups = append(slices.Clip(c.groups), name)
return &c2
}
diff --git a/slog/record.go b/slog/record.go
index 6911c6c..e791b2e 100644
--- a/slog/record.go
+++ b/slog/record.go
@@ -87,27 +87,29 @@
}
// Attrs calls f on each Attr in the Record.
-// The Attrs are already resolved.
-func (r Record) Attrs(f func(Attr)) {
+// Iteration stops if f returns false.
+func (r Record) Attrs(f func(Attr) bool) {
for i := 0; i < r.nFront; i++ {
- f(r.front[i])
+ if !f(r.front[i]) {
+ return
+ }
}
for _, a := range r.back {
- f(a)
+ if !f(a) {
+ return
+ }
}
}
// AddAttrs appends the given Attrs to the Record's list of Attrs.
-// It resolves the Attrs before doing so.
func (r *Record) AddAttrs(attrs ...Attr) {
- resolveAttrs(attrs)
n := copy(r.front[r.nFront:], attrs)
r.nFront += n
// Check if a copy was modified by slicing past the end
// and seeing if the Attr there is non-zero.
if cap(r.back) > len(r.back) {
end := r.back[:len(r.back)+1][len(r.back)]
- if end != (Attr{}) {
+ if !end.isEmpty() {
panic("copies of a slog.Record were both modified")
}
}
@@ -116,7 +118,6 @@
// Add converts the args to Attrs as described in [Logger.Log],
// then appends the Attrs to the Record's list of Attrs.
-// It resolves the Attrs before doing so.
func (r *Record) Add(args ...any) {
var a Attr
for len(args) > 0 {
@@ -150,7 +151,7 @@
// argsToAttr turns a prefix of the nonempty args slice into an Attr
// and returns the unconsumed portion of the slice.
-// If args[0] is an Attr, it returns it, resolved.
+// If args[0] is an Attr, it returns it.
// If args[0] is a string, it treats the first two elements as
// a key-value pair.
// Otherwise, it treats args[0] as a value with a missing key.
@@ -160,12 +161,9 @@
if len(args) == 1 {
return String(badKey, x), nil
}
- a := Any(x, args[1])
- a.Value = a.Value.Resolve()
- return a, args[2:]
+ return Any(x, args[1]), args[2:]
case Attr:
- x.Value = x.Value.Resolve()
return x, args[1:]
default:
diff --git a/slog/record_test.go b/slog/record_test.go
index 4727a08..fa907f3 100644
--- a/slog/record_test.go
+++ b/slog/record_test.go
@@ -24,6 +24,17 @@
if got := attrsSlice(r); !attrsEqual(got, as) {
t.Errorf("got %v, want %v", got, as)
}
+
+ // Early return.
+ var got []Attr
+ r.Attrs(func(a Attr) bool {
+ got = append(got, a)
+ return len(got) < 2
+ })
+ want := as[:2]
+ if !attrsEqual(got, want) {
+ t.Errorf("got %v, want %v", got, want)
+ }
}
func TestRecordSourceLine(t *testing.T) {
@@ -103,7 +114,7 @@
func attrsSlice(r Record) []Attr {
s := make([]Attr, 0, r.NumAttrs())
- r.Attrs(func(a Attr) { s = append(s, a) })
+ r.Attrs(func(a Attr) bool { s = append(s, a); return true })
return s
}
@@ -158,7 +169,7 @@
for j := 0; j < nAttrs; j++ {
r.AddAttrs(Int("k", j))
}
- r.Attrs(func(b Attr) { a = b })
+ r.Attrs(func(b Attr) bool { a = b; return true })
}
_ = a
}
diff --git a/slog/slogtest/slogtest.go b/slog/slogtest/slogtest.go
index 442a6c7..610575a 100644
--- a/slog/slogtest/slogtest.go
+++ b/slog/slogtest/slogtest.go
@@ -67,9 +67,9 @@
},
},
{
- explanation: withSource("a Handler should ignore an Attr with an empty key"),
+ explanation: withSource("a Handler should ignore an empty Attr"),
f: func(l *slog.Logger) {
- l.Info("msg", "a", "b", "", "ignore", "c", "d")
+ l.Info("msg", "a", "b", "", nil, "c", "d")
},
checks: []check{
hasAttr("a", "b"),
diff --git a/slog/text_handler.go b/slog/text_handler.go
index 0faa367..4981eb6 100644
--- a/slog/text_handler.go
+++ b/slog/text_handler.go
@@ -68,7 +68,7 @@
// If the AddSource option is set and source information is available,
// the key is "source" and the value is output as FILE:LINE.
//
-// The message's key "msg".
+// The message's key is "msg".
//
// To modify these or other attributes, or remove them from the output, use
// [HandlerOptions.ReplaceAttr].
@@ -80,9 +80,13 @@
// characters, non-printing characters, '"' or '='.
//
// Keys inside groups consist of components (keys or group names) separated by
-// dots. No further escaping is performed. If it is necessary to reconstruct the
-// group structure of a key even in the presence of dots inside components, use
-// [HandlerOptions.ReplaceAttr] to escape the keys.
+// dots. No further escaping is performed.
+// Thus there is no way to determine from the key "a.b.c" whether there
+// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c",
+// or single group "a" and a key "b.c".
+// If it is necessary to reconstruct the group structure of a key
+// even in the presence of dots inside components, use
+// [HandlerOptions.ReplaceAttr] to encode that information in the key.
//
// Each call to Handle results in a single serialized call to
// io.Writer.Write.
@@ -134,10 +138,15 @@
}
func needsQuoting(s string) bool {
+ if len(s) == 0 {
+ return true
+ }
for i := 0; i < len(s); {
b := s[i]
if b < utf8.RuneSelf {
- if needsQuotingSet[b] {
+ // Quote anything except a backslash that would need quoting in a
+ // JSON string, as well as space and '='
+ if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) {
return true
}
i++
@@ -151,17 +160,3 @@
}
return false
}
-
-var needsQuotingSet = [utf8.RuneSelf]bool{
- '"': true,
- '=': true,
-}
-
-func init() {
- for i := 0; i < utf8.RuneSelf; i++ {
- r := rune(i)
- if unicode.IsSpace(r) || !unicode.IsPrint(r) {
- needsQuotingSet[i] = true
- }
- }
-}
diff --git a/slog/text_handler_test.go b/slog/text_handler_test.go
index 71f637a..6dbe402 100644
--- a/slog/text_handler_test.go
+++ b/slog/text_handler_test.go
@@ -183,7 +183,7 @@
in string
want bool
}{
- {"", false},
+ {"", true},
{"ab", false},
{"a=b", true},
{`"ab"`, true},
diff --git a/slog/value.go b/slog/value.go
index f331945..5719976 100644
--- a/slog/value.go
+++ b/slog/value.go
@@ -7,7 +7,9 @@
import (
"fmt"
"math"
+ "runtime"
"strconv"
+ "strings"
"time"
"golang.org/x/exp/slices"
@@ -346,8 +348,10 @@
return append(dst, v.duration().String()...)
case KindTime:
return append(dst, v.time().String()...)
- case KindAny, KindGroup, KindLogValuer:
- return append(dst, fmt.Sprint(v.any)...)
+ case KindGroup:
+ return fmt.Append(dst, v.group())
+ case KindAny, KindLogValuer:
+ return fmt.Append(dst, v.any)
default:
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
}
@@ -365,20 +369,19 @@
// Resolve repeatedly calls LogValue on v while it implements LogValuer,
// and returns the result.
-// If v resolves to a group, the group's attributes' values are also resolved.
+// If v resolves to a group, the group's attributes' values are not recursively
+// resolved.
// If the number of LogValue calls exceeds a threshold, a Value containing an
// error is returned.
// Resolve's return value is guaranteed not to be of Kind KindLogValuer.
-func (v Value) Resolve() Value {
- v = v.resolve()
- if v.Kind() == KindGroup {
- resolveAttrs(v.Group())
- }
- return v
-}
-
-func (v Value) resolve() Value {
+func (v Value) Resolve() (rv Value) {
orig := v
+ defer func() {
+ if r := recover(); r != nil {
+ rv = AnyValue(fmt.Errorf("LogValue panicked\n%s", stack(3, 5)))
+ }
+ }()
+
for i := 0; i < maxLogValues; i++ {
if v.Kind() != KindLogValuer {
return v
@@ -389,10 +392,26 @@
return AnyValue(err)
}
-// resolveAttrs replaces the values of the given Attrs with their
-// resolutions.
-func resolveAttrs(as []Attr) {
- for i, a := range as {
- as[i].Value = a.Value.Resolve()
+func stack(skip, nFrames int) string {
+ pcs := make([]uintptr, nFrames+1)
+ n := runtime.Callers(skip+1, pcs)
+ if n == 0 {
+ return "(no stack)"
}
+ frames := runtime.CallersFrames(pcs[:n])
+ var b strings.Builder
+ i := 0
+ for {
+ frame, more := frames.Next()
+ fmt.Fprintf(&b, "called from %s (%s:%d)\n", frame.Function, frame.File, frame.Line)
+ if !more {
+ break
+ }
+ i++
+ if i >= nFrames {
+ fmt.Fprintf(&b, "(rest of stack elided)\n")
+ break
+ }
+ }
+ return b.String()
}
diff --git a/slog/value_test.go b/slog/value_test.go
index e3c087c..1196e75 100644
--- a/slog/value_test.go
+++ b/slog/value_test.go
@@ -7,6 +7,7 @@
import (
"fmt"
"reflect"
+ "strings"
"testing"
"time"
"unsafe"
@@ -59,6 +60,7 @@
{StringValue("foo"), "foo"},
{TimeValue(testTime), "2000-01-02 03:04:05 +0000 UTC"},
{AnyValue(time.Duration(3 * time.Second)), "3s"},
+ {GroupValue(Int("a", 1), Bool("b", true)), "[a=1 b=true]"},
} {
if got := test.v.String(); got != test.want {
t.Errorf("%#v:\ngot %q\nwant %q", test.v, got, test.want)
@@ -173,18 +175,27 @@
t.Errorf("expected error, got %T", got)
}
- // Test Resolve group.
- r = &replace{GroupValue(
- Int("a", 1),
- Group("b", Any("c", &replace{StringValue("d")})),
- )}
- v = AnyValue(r)
+ // Groups are not recursively resolved.
+ c := Any("c", &replace{StringValue("d")})
+ v = AnyValue(&replace{GroupValue(Int("a", 1), Group("b", c))})
got2 := v.Resolve().Any().([]Attr)
- want2 := []Attr{Int("a", 1), Group("b", String("c", "d"))}
+ want2 := []Attr{Int("a", 1), Group("b", c)}
if !attrsEqual(got2, want2) {
t.Errorf("got %v, want %v", got2, want2)
}
+ // Verify that panics in Resolve are caught and turn into errors.
+ v = AnyValue(panickingLogValue{})
+ got = v.Resolve().Any()
+ gotErr, ok := got.(error)
+ if !ok {
+ t.Errorf("expected error, got %T", got)
+ }
+ // The error should provide some context information.
+ // We'll just check that this function name appears in it.
+ if got, want := gotErr.Error(), "TestLogValue"; !strings.Contains(got, want) {
+ t.Errorf("got %q, want substring %q", got, want)
+ }
}
func TestZeroTime(t *testing.T) {
@@ -201,6 +212,10 @@
func (r *replace) LogValue() Value { return r.v }
+type panickingLogValue struct{}
+
+func (panickingLogValue) LogValue() Value { panic("bad") }
+
// A Value with "unsafe" strings is significantly faster:
// safe: 1785 ns/op, 0 allocs
// unsafe: 690 ns/op, 0 allocs