Repository: hashicorp/go-hclog
Pull request #27 (2018-10-01 18:13:09)

Improve rendering of slice values
Rather than render slices via Sprintf, this renders them out exactly using a more JSON-like syntax. Nested slices are still rendered with Sprintf though.

Example:

```
logger.Info("this is test", "who", "programmer", "why", []interface{}{"testing", "dev", 1, uint64(5), []int{3, 4}})
```

would output
```
[INFO]  test: this is test: who=programmer why=[testing, dev, 1, 5, "[3 4]"]
```


Patch:
diff --git a/int.go b/int.go
index 6ff9737..2aaa1f8 100644
--- a/int.go
+++ b/int.go
@@ -2,11 +2,13 @@ package hclog
 
 import (
 	"bufio"
+	"bytes"
 	"encoding"
 	"encoding/json"
 	"fmt"
 	"log"
 	"os"
+	"reflect"
 	"runtime"
 	"sort"
 	"strconv"
@@ -148,7 +150,7 @@ func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{
 	if ok {
 		z.w.WriteString(s)
 	} else {
-		z.w.WriteString("[UNKN ]")
+		z.w.WriteString("[?????]")
 	}
 
 	if z.caller {
@@ -189,7 +191,10 @@ func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{
 
 	FOR:
 		for i := 0; i < len(args); i = i + 2 {
-			var val string
+			var (
+				val string
+				raw bool
+			)
 
 			switch st := args[i+1].(type) {
 			case string:
@@ -220,14 +225,20 @@ func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{
 			case Format:
 				val = fmt.Sprintf(st[0].(string), st[1:]...)
 			default:
-				val = fmt.Sprintf("%v", st)
+				v := reflect.ValueOf(st)
+				if v.Kind() == reflect.Slice {
+					val = z.renderSlice(v)
+					raw = true
+				} else {
+					val = fmt.Sprintf("%v", st)
+				}
 			}
 
 			z.w.WriteByte(' ')
 			z.w.WriteString(args[i].(string))
 			z.w.WriteByte('=')
 
-			if strings.ContainsAny(val, " \t\n\r") {
+			if !raw && strings.ContainsAny(val, " \t\n\r") {
 				z.w.WriteByte('"')
 				z.w.WriteString(val)
 				z.w.WriteByte('"')
@@ -244,6 +255,45 @@ func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{
 	}
 }
 
+func (z *intLogger) renderSlice(v reflect.Value) string {
+	var buf bytes.Buffer
+
+	buf.WriteRune('[')
+
+	for i := 0; i < v.Len(); i++ {
+		if i > 0 {
+			buf.WriteString(", ")
+		}
+
+		sv := v.Index(i)
+
+		var val string
+
+		switch sv.Kind() {
+		case reflect.String:
+			val = sv.String()
+		case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+			val = strconv.FormatInt(sv.Int(), 10)
+		case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+			val = strconv.FormatUint(sv.Uint(), 10)
+		default:
+			val = fmt.Sprintf("%v", sv.Interface())
+		}
+
+		if strings.ContainsAny(val, " \t\n\r") {
+			buf.WriteByte('"')
+			buf.WriteString(val)
+			buf.WriteByte('"')
+		} else {
+			buf.WriteString(val)
+		}
+	}
+
+	buf.WriteRune(']')
+
+	return buf.String()
+}
+
 // JSON logging function
 func (z *intLogger) logJson(t time.Time, level Level, msg string, args ...interface{}) {
 	vals := map[string]interface{}{


Test patch:
diff --git a/logger_test.go b/logger_test.go
index 365f413..bf7b08d 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -55,6 +55,46 @@ func TestLogger(t *testing.T) {
 		assert.Equal(t, "[INFO]  test: this is test: who=programmer why=\"testing is fun\"\n", rest)
 	})
 
+	t.Run("renders slice values specially", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:   "test",
+			Output: &buf,
+		})
+
+		logger.Info("this is test", "who", "programmer", "why", []interface{}{"testing", "dev", 1, uint64(5), []int{3, 4}})
+
+		str := buf.String()
+
+		dataIdx := strings.IndexByte(str, ' ')
+
+		// ts := str[:dataIdx]
+		rest := str[dataIdx+1:]
+
+		assert.Equal(t, "[INFO]  test: this is test: who=programmer why=[testing, dev, 1, 5, \"[3 4]\"]\n", rest)
+	})
+
+	t.Run("renders values in slices with quotes if needed", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:   "test",
+			Output: &buf,
+		})
+
+		logger.Info("this is test", "who", "programmer", "why", []string{"testing & qa", "dev"})
+
+		str := buf.String()
+
+		dataIdx := strings.IndexByte(str, ' ')
+
+		// ts := str[:dataIdx]
+		rest := str[dataIdx+1:]
+
+		assert.Equal(t, "[INFO]  test: this is test: who=programmer why=[\"testing & qa\", dev]\n", rest)
+	})
+
 	t.Run("outputs stack traces", func(t *testing.T) {
 		var buf bytes.Buffer
 
@@ -108,7 +148,7 @@ func TestLogger(t *testing.T) {
 		rest := str[dataIdx+1:]
 
 		// This test will break if you move this around, it's line dependent, just fyi
-		assert.Equal(t, "[INFO]  go-hclog/logger_test.go:101: test: this is test: who=programmer why=\"testing is fun\"\n", rest)
+		assert.Equal(t, "[INFO]  go-hclog/logger_test.go:141: test: this is test: who=programmer why=\"testing is fun\"\n", rest)
 	})
 
 	t.Run("prefixes the name", func(t *testing.T) {