Repository: hashicorp/go-hclog (go)

Issue:
JSON marshal does not work on error type
JSON doesn't properly marshal error types, causing the logger to essentially print out an empty JSON object whenever an `error` is part of the arg value. Potentially related GH issue: https://github.com/golang/go/issues/10748

Example:
```go
package main

import (
	"errors"
	"os"

	hclog "github.com/hashicorp/go-hclog"
)

func main() {
	err := errors.New("this is an error")
	logger := hclog.New(&hclog.LoggerOptions{
		Level:      hclog.Info,
		Output:     os.Stdout,
		JSONFormat: true,
	})

	logger.Info("hello from logger", "err", err)
}
```

Output:
```
{"@level":"info","@message":"hello from logger","@timestamp":"2017-08-26T22:02:31.816599-04:00","err":{}}
```

Pull request:
Handle JSON marshal error type into err.Error()
Fixes #8 

Patch:
diff --git a/int.go b/int.go
index 9f90c28..c8609c3 100644
--- a/int.go
+++ b/int.go
@@ -2,6 +2,7 @@ package hclog
 
 import (
 	"bufio"
+	"encoding"
 	"encoding/json"
 	"fmt"
 	"log"
@@ -279,7 +280,18 @@ func (z *intLogger) logJson(t time.Time, level Level, msg string, args ...interf
 				// without injecting into logs...
 				continue
 			}
-			vals[args[i].(string)] = args[i+1]
+			val := args[i+1]
+			// Check if val is of type error. If error type doesn't
+			// implement json.Marshaler or encoding.TextMarshaler
+			// then set val to err.Error() so that it gets marshaled
+			if err, ok := val.(error); ok {
+				switch err.(type) {
+				case json.Marshaler, encoding.TextMarshaler:
+				default:
+					val = err.Error()
+				}
+			}
+			vals[args[i].(string)] = val
 		}
 	}
 
diff --git a/log.go b/log.go
index 6bb16ba..1b36f81 100644
--- a/log.go
+++ b/log.go
@@ -133,6 +133,6 @@ type LoggerOptions struct {
 	// Control if the output should be in JSON.
 	JSONFormat bool
 
-	// Intclude file and line information in each log line
+	// Include file and line information in each log line
 	IncludeLocation bool
 }


Test patch:
diff --git a/logger_test.go b/logger_test.go
index 38e345c..0effcd6 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -2,6 +2,10 @@ package hclog
 
 import (
 	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -103,10 +107,147 @@ 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:96: test: this is test: who=programmer why=\"testing is fun\"\n", rest)
+		assert.Equal(t, "[INFO ] go-hclog/logger_test.go:100: test: this is test: who=programmer why=\"testing is fun\"\n", rest)
 	})
 }
 
+func TestLogger_JSON(t *testing.T) {
+	t.Run("json formatting", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:       "test",
+			Output:     &buf,
+			JSONFormat: true,
+		})
+
+		logger.Info("this is test", "who", "programmer", "why", "testing is fun")
+
+		b := buf.Bytes()
+
+		var raw map[string]interface{}
+		if err := json.Unmarshal(b, &raw); err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, "this is test", raw["@message"])
+		assert.Equal(t, "programmer", raw["who"])
+		assert.Equal(t, "testing is fun", raw["why"])
+	})
+	t.Run("json formatting error type", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:       "test",
+			Output:     &buf,
+			JSONFormat: true,
+		})
+
+		errMsg := errors.New("this is an error")
+		logger.Info("this is test", "who", "programmer", "err", errMsg)
+
+		b := buf.Bytes()
+
+		var raw map[string]interface{}
+		if err := json.Unmarshal(b, &raw); err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, "this is test", raw["@message"])
+		assert.Equal(t, "programmer", raw["who"])
+		assert.Equal(t, errMsg.Error(), raw["err"])
+	})
+	t.Run("json formatting custom error type json marshaler", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:       "test",
+			Output:     &buf,
+			JSONFormat: true,
+		})
+
+		errMsg := &customErrJSON{"this is an error"}
+		rawMsg, err := errMsg.MarshalJSON()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedMsg, err := strconv.Unquote(string(rawMsg))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		logger.Info("this is test", "who", "programmer", "err", errMsg)
+
+		b := buf.Bytes()
+
+		var raw map[string]interface{}
+		if err := json.Unmarshal(b, &raw); err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, "this is test", raw["@message"])
+		assert.Equal(t, "programmer", raw["who"])
+		assert.Equal(t, expectedMsg, raw["err"])
+	})
+	t.Run("json formatting custom error type text marshaler", func(t *testing.T) {
+		var buf bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Name:       "test",
+			Output:     &buf,
+			JSONFormat: true,
+		})
+
+		errMsg := &customErrText{"this is an error"}
+		rawMsg, err := errMsg.MarshalText()
+		if err != nil {
+			t.Fatal(err)
+		}
+		expectedMsg := string(rawMsg)
+
+		logger.Info("this is test", "who", "programmer", "err", errMsg)
+
+		b := buf.Bytes()
+
+		var raw map[string]interface{}
+		if err := json.Unmarshal(b, &raw); err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, "this is test", raw["@message"])
+		assert.Equal(t, "programmer", raw["who"])
+		assert.Equal(t, expectedMsg, raw["err"])
+	})
+}
+
+type customErrJSON struct {
+	Message string
+}
+
+// error impl.
+func (c *customErrJSON) Error() string {
+	return c.Message
+}
+
+// json.Marshaler impl.
+func (c customErrJSON) MarshalJSON() ([]byte, error) {
+	return []byte(strconv.Quote(fmt.Sprintf("json-marshaler: %s", c.Message))), nil
+}
+
+type customErrText struct {
+	Message string
+}
+
+// error impl.
+func (c *customErrText) Error() string {
+	return c.Message
+}
+
+// text.Marshaler impl.
+func (c customErrText) MarshalText() ([]byte, error) {
+	return []byte(fmt.Sprintf("text-marshaler: %s", c.Message)), nil
+}
+
 func BenchmarkLogger(b *testing.B) {
 	b.Run("info with 10 pairs", func(b *testing.B) {
 		var buf bytes.Buffer