Repository: go-logr/logr (go)

Issue:
funcr doesn't handle recursive pointers well
```
type T struct {    
    P *T    
}    

func example(log logr.Logger) {    
    t := T{}    
    t.P = &t    
    log.Info("recurse", "t", t)     
}
```

yields a (predictable) stack overflow.

We probably should add a max-depth option or track a stack of pointers across `pretty` calls and not print pointers already in the stack.  Or both.

Pull request:
funcr: Prevent stack overflow on recursive structs
Add a new Option `MaxLogDepth` which  tells funcr how many levels of nested fields (e.g. a struct that contains 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.

Fixes #123


Patch:
diff --git a/funcr/funcr.go b/funcr/funcr.go
index 4727d27..b23ab96 100644
--- a/funcr/funcr.go
+++ b/funcr/funcr.go
@@ -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.
@@ -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,
@@ -321,7 +334,7 @@ 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 (
@@ -329,7 +342,11 @@ const (
 )
 
 // 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.
@@ -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('}')
@@ -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 == "" {
@@ -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('}')
@@ -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()
@@ -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.
@@ -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('}')
@@ -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())
 }


Test patch:
diff --git a/funcr/example_test.go b/funcr/example_test.go
index 9197a9a..53373a2 100644
--- a/funcr/example_test.go
+++ b/funcr/example_test.go
@@ -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>"}}}}}}
+}