Repository: hashicorp/go-hclog
Pull request #20 (2018-04-02 20:00:56)

Add `NullLogger` and `.gitignore`
In switching Vault over to using hclog instead of logxi, I found many tests using a null logger, so I wrote a null logger fulfilling the hclog interface. Thought it might be useful to others going forward, so voila!

I also added a .gitignore for convenience, but I'm happy to pull it out if desired.


Patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..42cc410
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,1 @@
+.idea*
\ No newline at end of file
diff --git a/README.md b/README.md
index 1153e28..614342b 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@ interface for use in development and production environments.
 It provides logging levels that provide decreased output based upon the
 desired amount of output, unlike the standard library `log` package.
 
-It provides `Printf` style logging of values via `hclog.Fmt()`.
+It does not provide `Printf` style logging, only key/value logging that is
+exposed as arguments to the logging functions for simplicity.
 
 It provides a human readable output mode for use in development as well as
 JSON output mode for production.
@@ -99,17 +100,6 @@ requestLogger.Info("we are transporting a request")
 This allows sub Loggers to be context specific without having to thread that
 into all the callers.
 
-### Using `hclog.Fmt()`
-
-```go
-var int totalBandwidth = 200
-appLogger.Info("total bandwidth exceeded", "bandwidth", hclog.Fmt("%d GB/s", totalBandwidth))
-```
-
-```text
-... [INFO ] my-app: total bandwidth exceeded: bandwidth="200 GB/s"
-```
-
 ### Use this with code that uses the standard library logger
 
 If you want to use the standard library's `log.Logger` interface you can wrap
diff --git a/int.go b/int.go
index 0166c3d..340cfdf 100644
--- a/int.go
+++ b/int.go
@@ -214,8 +214,6 @@ func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{
 			case CapturedStacktrace:
 				stacktrace = st
 				continue FOR
-			case Format:
-				val = fmt.Sprintf(st[0].(string), st[1:]...)
 			default:
 				val = fmt.Sprintf("%v", st)
 			}
@@ -296,20 +294,16 @@ func (z *intLogger) logJson(t time.Time, level Level, msg string, args ...interf
 				continue
 			}
 			val := args[i+1]
-			switch sv := val.(type) {
-			case error:
-				// 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
-				switch sv.(type) {
+			// 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 = sv.Error()
+					val = err.Error()
 				}
-			case Format:
-				val = fmt.Sprintf(sv[0].(string), sv[1:]...)
 			}
-
 			vals[args[i].(string)] = val
 		}
 	}
diff --git a/log.go b/log.go
index 3629248..6be9839 100644
--- a/log.go
+++ b/log.go
@@ -37,18 +37,6 @@ const (
 	Error Level = 5
 )
 
-// When processing a value of this type, the logger automatically treats the first
-// argument as a Printf formatting string and passes the rest as the values to be
-// formatted. For example: L.Info(Fmt{"%d beans/day", beans}). This is a simple
-// convience type for when formatting is required.
-type Format []interface{}
-
-// Fmt returns a Format type. This is a convience function for creating a Format
-// type.
-func Fmt(str string, args ...interface{}) Format {
-	return append(Format{str}, args...)
-}
-
 // LevelFromString returns a Level type for the named log level, or "NoLevel" if
 // the level string is invalid. This facilitates setting the log level via
 // config or environment variable by name in a predictable way.
diff --git a/nulllogger.go b/nulllogger.go
new file mode 100644
index 0000000..c10ce6e
--- /dev/null
+++ b/nulllogger.go
@@ -0,0 +1,45 @@
+package hclog
+
+import (
+	"log"
+	"io/ioutil"
+)
+
+// NewNullLogger instantiates a Logger for which all calls
+// will succeed without doing anything.
+// Useful for testing purposes.
+func NewNullLogger() Logger {
+	return &nullLogger{}
+}
+
+type nullLogger struct{}
+
+func (l *nullLogger) Trace(msg string, args ...interface{}) {}
+
+func (l *nullLogger) Debug(msg string, args ...interface{}) {}
+
+func (l *nullLogger) Info(msg string, args ...interface{}) {}
+
+func (l *nullLogger) Warn(msg string, args ...interface{}) {}
+
+func (l *nullLogger) Error(msg string, args ...interface{}) {}
+
+func (l *nullLogger) IsTrace() bool { return false }
+
+func (l *nullLogger) IsDebug() bool { return false }
+
+func (l *nullLogger) IsInfo() bool { return false }
+
+func (l *nullLogger) IsWarn() bool { return false }
+
+func (l *nullLogger) IsError() bool { return false }
+
+func (l *nullLogger) With(args ...interface{}) Logger { return l }
+
+func (l *nullLogger) Named(name string) Logger { return l }
+
+func (l *nullLogger) ResetNamed(name string) Logger { return l }
+
+func (l *nullLogger) StandardLogger(opts *StandardLoggerOptions) *log.Logger {
+	return log.New(ioutil.Discard, "", log.LstdFlags)
+}
\ No newline at end of file


Test patch:
diff --git a/logger_test.go b/logger_test.go
index d8c143f..bb233e9 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -211,26 +211,6 @@ func TestLogger(t *testing.T) {
 		dataIdx = strings.IndexByte(output, ' ')
 		assert.Equal(t, "[INFO ] with_test: derived_test: a=1 b=2 c=3 cat=30\n", output[dataIdx+1:])
 	})
-
-	t.Run("supports Printf style expansions when requested", func(t *testing.T) {
-		var buf bytes.Buffer
-
-		logger := New(&LoggerOptions{
-			Name:   "test",
-			Output: &buf,
-		})
-
-		logger.Info("this is test", "production", Fmt("%d beans/day", 12))
-
-		str := buf.String()
-
-		dataIdx := strings.IndexByte(str, ' ')
-
-		// ts := str[:dataIdx]
-		rest := str[dataIdx+1:]
-
-		assert.Equal(t, "[INFO ] test: this is test: production=\"12 beans/day\"\n", rest)
-	})
 }
 
 func TestLogger_JSON(t *testing.T) {
@@ -304,7 +284,6 @@ func TestLogger_JSON(t *testing.T) {
 		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
 
@@ -337,7 +316,6 @@ func TestLogger_JSON(t *testing.T) {
 		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
 
@@ -367,28 +345,6 @@ func TestLogger_JSON(t *testing.T) {
 		assert.Equal(t, "programmer", raw["who"])
 		assert.Equal(t, expectedMsg, raw["err"])
 	})
-
-	t.Run("supports Printf style expansions when requested", func(t *testing.T) {
-		var buf bytes.Buffer
-
-		logger := New(&LoggerOptions{
-			Name:       "test",
-			Output:     &buf,
-			JSONFormat: true,
-		})
-
-		logger.Info("this is test", "production", Fmt("%d beans/day", 12))
-
-		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, "12 beans/day", raw["production"])
-	})
 }
 
 type customErrJSON struct {
diff --git a/nulllogger_test.go b/nulllogger_test.go
new file mode 100644
index 0000000..f9433cc
--- /dev/null
+++ b/nulllogger_test.go
@@ -0,0 +1,44 @@
+package hclog
+
+import (
+	"testing"
+	"github.com/stretchr/testify/assert"
+)
+
+var logger = NewNullLogger()
+
+func TestNullLoggerIsEfficient(t *testing.T) {
+	// Since statements like "IsWarn()", "IsError()", etc. are used to gate
+	// actually writing warning and error statements, the null logger will
+	// be faster and more efficient if it always returns false for these calls.
+	assert.False(t, logger.IsTrace())
+	assert.False(t, logger.IsDebug())
+	assert.False(t, logger.IsInfo())
+	assert.False(t, logger.IsWarn())
+	assert.False(t, logger.IsError())
+}
+
+func TestNullLoggerReturnsNullLoggers(t *testing.T) {
+
+	// Sometimes the logger is asked to return subloggers.
+	// These should also be a nullLogger.
+
+	subLogger := logger.With()
+	_, ok := subLogger.(*nullLogger)
+	assert.True(t, ok)
+
+	subLogger = logger.Named("")
+	_, ok = subLogger.(*nullLogger)
+	assert.True(t, ok)
+
+	subLogger = logger.ResetNamed("")
+	_, ok = subLogger.(*nullLogger)
+	assert.True(t, ok)
+}
+
+func TestStandardLoggerIsntNil(t *testing.T) {
+	// Don't return a nil pointer for the standard logger,
+	// lest it cause a panic.
+	stdLogger := logger.StandardLogger(nil)
+	assert.NotEqual(t, nil, stdLogger)
+}