Repository: hashicorp/go-hclog
Pull request #56 (2020-01-23 18:31:03)

Adds ResetOutput and ResetOutputWithFlush
This can be used if you want to change the destination of your output
without application logic having to implement a second mutex to do so.


Patch:
diff --git a/intlogger.go b/intlogger.go
index 0786c92..5d76ee3 100644
--- a/intlogger.go
+++ b/intlogger.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -561,6 +562,41 @@ func (l *intLogger) ResetNamed(name string) Logger {
 	return &sl
 }
 
+func (l *intLogger) ResetOutput(opts *LoggerOptions) error {
+	if opts.Output == nil {
+		return errors.New("given output is nil")
+	}
+
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+
+	return l.resetOutput(opts)
+}
+
+func (l *intLogger) ResetOutputWithFlush(opts *LoggerOptions, flushable Flushable) error {
+	if opts.Output == nil {
+		return errors.New("given output is nil")
+	}
+	if flushable == nil {
+		return errors.New("flushable is nil")
+	}
+
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+
+	if err := flushable.Flush(); err != nil {
+		return err
+	}
+
+	return l.resetOutput(opts)
+}
+
+func (l *intLogger) resetOutput(opts *LoggerOptions) error {
+	l.writer = newWriter(opts.Output, opts.Color)
+	l.setColorization(opts)
+	return nil
+}
+
 // Update the logging level on-the-fly. This will affect all subloggers as
 // well.
 func (l *intLogger) SetLevel(level Level) {
diff --git a/logger.go b/logger.go
index 95b0318..147bd2d 100644
--- a/logger.go
+++ b/logger.go
@@ -241,3 +241,22 @@ type InterceptLogger interface {
 type SinkAdapter interface {
 	Accept(name string, level Level, msg string, args ...interface{})
 }
+
+// Flushable represents a method for flushing an output buffer. It can be used
+// if Resetting the log to use a new output, in order to flush the writes to
+// the existing output beforehand.
+type Flushable interface {
+	Flush() error
+}
+
+// OutputResettable provides ways to swap the output in use at runtime
+type OutputResettable interface {
+	// ResetOutput swaps the current output writer with the one given in the
+	// opts. Color options given in opts will be used for the new output.
+	ResetOutput(opts *LoggerOptions) error
+
+	// ResetOutputWithFlush swaps the current output writer with the one given
+	// in the opts, first calling Flush on the given Flushable. Color options
+	// given in opts will be used for the new output.
+	ResetOutputWithFlush(opts *LoggerOptions, flushable Flushable) error
+}


Test patch:
diff --git a/logger_test.go b/logger_test.go
index 4d34268..f79e82d 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -16,6 +16,24 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+type bufferingBuffer struct {
+	held    bytes.Buffer
+	flushed bytes.Buffer
+}
+
+func (b *bufferingBuffer) Write(p []byte) (int, error) {
+	return b.held.Write(p)
+}
+
+func (b *bufferingBuffer) String() string {
+	return b.flushed.String()
+}
+
+func (b *bufferingBuffer) Flush() error {
+	_, err := b.flushed.WriteString(b.held.String())
+	return err
+}
+
 func TestLogger(t *testing.T) {
 	t.Run("uses default output if none is given", func(t *testing.T) {
 		var buf bytes.Buffer
@@ -316,6 +334,69 @@ func TestLogger(t *testing.T) {
 
 		assert.Equal(t, "[INFO]  test: this is test: production=\"12 beans/day\"\n", rest)
 	})
+
+	t.Run("supports resetting the output", func(t *testing.T) {
+		var first, second bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Output: &first,
+		})
+
+		logger.Info("this is test", "production", Fmt("%d beans/day", 12))
+
+		str := first.String()
+		dataIdx := strings.IndexByte(str, ' ')
+		rest := str[dataIdx+1:]
+
+		assert.Equal(t, "[INFO]  this is test: production=\"12 beans/day\"\n", rest)
+
+		logger.(OutputResettable).ResetOutput(&LoggerOptions{
+			Output: &second,
+		})
+
+		logger.Info("this is another test", "production", Fmt("%d beans/day", 13))
+
+		str = first.String()
+		dataIdx = strings.IndexByte(str, ' ')
+		rest = str[dataIdx+1:]
+		assert.Equal(t, "[INFO]  this is test: production=\"12 beans/day\"\n", rest)
+
+		str = second.String()
+		dataIdx = strings.IndexByte(str, ' ')
+		rest = str[dataIdx+1:]
+		assert.Equal(t, "[INFO]  this is another test: production=\"13 beans/day\"\n", rest)
+	})
+
+	t.Run("supports resetting the output with flushing", func(t *testing.T) {
+		var first bufferingBuffer
+		var second bytes.Buffer
+
+		logger := New(&LoggerOptions{
+			Output: &first,
+		})
+
+		logger.Info("this is test", "production", Fmt("%d beans/day", 12))
+
+		str := first.String()
+		assert.Empty(t, str)
+
+		logger.(OutputResettable).ResetOutputWithFlush(&LoggerOptions{
+			Output: &second,
+		}, &first)
+
+		logger.Info("this is another test", "production", Fmt("%d beans/day", 13))
+
+		str = first.String()
+		dataIdx := strings.IndexByte(str, ' ')
+		rest := str[dataIdx+1:]
+		assert.Equal(t, "[INFO]  this is test: production=\"12 beans/day\"\n", rest)
+
+		str = second.String()
+		dataIdx = strings.IndexByte(str, ' ')
+		rest = str[dataIdx+1:]
+		assert.Equal(t, "[INFO]  this is another test: production=\"13 beans/day\"\n", rest)
+	})
+
 }
 
 func TestLogger_leveledWriter(t *testing.T) {