Skip to content

Commit

Permalink
Merge pull request #124 from thockin/master
Browse files Browse the repository at this point in the history
funcr: Prevent stack overflow on recursive structs
  • Loading branch information
pohly committed Dec 5, 2021
2 parents dd8f76f + 2ccfbf6 commit 99e02a9
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 11 deletions.
14 changes: 14 additions & 0 deletions funcr/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,17 @@ func ExamplePseudoStruct() {
log.Info("the message", "key", funcr.PseudoStruct(kv))
// Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}}
}

func ExampleOptions_maxLogDepth() {
type List struct {
Next *List
}
l := List{}
l.Next = &l // recursive

var log logr.Logger = funcr.NewJSON(
func(obj string) { fmt.Println(obj) },
funcr.Options{MaxLogDepth: 4})
log.Info("recursive", "list", l)
// Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":"<max-log-depth-exceeded>"}}}}}}
}
39 changes: 28 additions & 11 deletions funcr/funcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ type Options struct {
// called for key-value pairs passed directly to Info and Error. See
// RenderBuiltinsHook for more details.
RenderArgsHook func(kvList []interface{}) []interface{}

// MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct
// that contains a struct, etc.) it may log. Every time it finds a struct,
// slice, array, or map the depth is increased by one. When the maximum is
// reached, the value will be converted to a string indicating that the max
// depth has been exceeded. If this field is not specified, a default
// value will be used.
MaxLogDepth int
}

// MessageClass indicates which category or categories of messages to consider.
Expand Down Expand Up @@ -193,11 +201,16 @@ func NewFormatterJSON(opts Options) Formatter {
return newFormatter(opts, outputJSON)
}

const defaultTimestampFmt = "2006-01-02 15:04:05.000000"
// Defaults for Options.
const defaultTimestampFormat = "2006-01-02 15:04:05.000000"
const defaultMaxLogDepth = 16

func newFormatter(opts Options, outfmt outputFormat) Formatter {
if opts.TimestampFormat == "" {
opts.TimestampFormat = defaultTimestampFmt
opts.TimestampFormat = defaultTimestampFormat
}
if opts.MaxLogDepth == 0 {
opts.MaxLogDepth = defaultMaxLogDepth
}
f := Formatter{
outputFormat: outfmt,
Expand Down Expand Up @@ -321,15 +334,19 @@ func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing b
}

func (f Formatter) pretty(value interface{}) string {
return f.prettyWithFlags(value, 0)
return f.prettyWithFlags(value, 0, 0)
}

const (
flagRawStruct = 0x1 // do not print braces on structs
)

// TODO: This is not fast. Most of the overhead goes here.
func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
func (f Formatter) prettyWithFlags(value interface{}, flags uint32, depth int) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}

// Handle types that take full control of logging.
if v, ok := value.(logr.Marshaler); ok {
// Replace the value with what the type wants to get logged.
Expand Down Expand Up @@ -394,7 +411,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
// arbitrary keys might need escaping
buf.WriteString(prettyString(v[i].(string)))
buf.WriteByte(':')
buf.WriteString(f.pretty(v[i+1]))
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
Expand Down Expand Up @@ -464,7 +481,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
buf.WriteByte(',')
}
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
continue
}
if name == "" {
Expand All @@ -475,7 +492,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
buf.WriteString(name)
buf.WriteByte('"')
buf.WriteByte(':')
buf.WriteString(f.pretty(v.Field(i).Interface()))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
Expand All @@ -488,7 +505,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
buf.WriteByte(',')
}
e := v.Index(i)
buf.WriteString(f.pretty(e.Interface()))
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
}
buf.WriteByte(']')
return buf.String()
Expand All @@ -513,7 +530,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
keystr = f.prettyWithFlags(it.Key().Interface(), 0)
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
Expand All @@ -522,7 +539,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
}
buf.WriteString(keystr)
buf.WriteByte(':')
buf.WriteString(f.pretty(it.Value().Interface()))
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
i++
}
buf.WriteByte('}')
Expand All @@ -531,7 +548,7 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32) string {
if v.IsNil() {
return "null"
}
return f.pretty(v.Elem().Interface())
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
Expand Down

0 comments on commit 99e02a9

Please sign in to comment.