Skip to content

Commit

Permalink
Add SyncWriter and SyncLogger.
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisHines committed Jul 20, 2016
1 parent b9125e3 commit c35457e
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 81 deletions.
20 changes: 19 additions & 1 deletion example_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
package log_test

import (
"net/url"
"os"

"github.com/go-kit/kit/log"
)

func Example_stdout() {
w := log.NewSyncWriter(os.Stdout)
logger := log.NewLogfmtLogger(w)

reqUrl := &url.URL{
Scheme: "https",
Host: "github.com",
Path: "/go-kit/kit",
}

logger.Log("method", "GET", "url", reqUrl)

// Output:
// method=GET url=https://github.com/go-kit/kit
}

func ExampleContext() {
logger := log.NewLogfmtLogger(os.Stdout)
w := log.NewSyncWriter(os.Stdout)
logger := log.NewLogfmtLogger(w)
logger.Log("foo", 123)
ctx := log.NewContext(logger).With("level", "info")
ctx.Log()
Expand Down
41 changes: 7 additions & 34 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
//
// The fundamental interface is Logger. Loggers create log events from
// key/value data.
//
// Concurrent Safety
//
// Applications with multiple goroutines want each log event written to the
// same logger to remain separate from other log events. Package log provides
// multiple solutions for concurrent safe logging.
package log

import (
"errors"
"sync/atomic"
)
import "errors"

// Logger is the fundamental interface for all log operations. Log creates a
// log event from keyvals, a variadic sequence of alternating keys and values.
Expand Down Expand Up @@ -149,33 +152,3 @@ type LoggerFunc func(...interface{}) error
func (f LoggerFunc) Log(keyvals ...interface{}) error {
return f(keyvals...)
}

// SwapLogger wraps another logger that may be safely replaced while other
// goroutines use the SwapLogger concurrently. The zero value for a SwapLogger
// will discard all log events without error.
//
// SwapLogger serves well as a package global logger that can be changed by
// importers.
type SwapLogger struct {
logger atomic.Value
}

type loggerStruct struct {
Logger
}

// Log implements the Logger interface by forwarding keyvals to the currently
// wrapped logger. It does not log anything if the wrapped logger is nil.
func (l *SwapLogger) Log(keyvals ...interface{}) error {
s, ok := l.logger.Load().(loggerStruct)
if !ok || s.Logger == nil {
return nil
}
return s.Log(keyvals...)
}

// Swap replaces the currently wrapped logger with logger. Swap may be called
// concurrently with calls to Log from other goroutines.
func (l *SwapLogger) Swap(logger Logger) {
l.logger.Store(loggerStruct{logger})
}
46 changes: 0 additions & 46 deletions log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,49 +207,3 @@ func BenchmarkTenWith(b *testing.B) {
lc.Log("k", "v")
}
}

func TestSwapLogger(t *testing.T) {
var logger log.SwapLogger

// Zero value does not panic or error.
err := logger.Log("k", "v")
if got, want := err, error(nil); got != want {
t.Errorf("got %v, want %v", got, want)
}

buf := &bytes.Buffer{}
json := log.NewJSONLogger(buf)
logger.Swap(json)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), `{"k":"v"}`+"\n"; got != want {
t.Errorf("got %v, want %v", got, want)
}

buf.Reset()
prefix := log.NewLogfmtLogger(buf)
logger.Swap(prefix)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), "k=v\n"; got != want {
t.Errorf("got %v, want %v", got, want)
}

buf.Reset()
logger.Swap(nil)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), ""; got != want {
t.Errorf("got %v, want %v", got, want)
}
}

func TestSwapLoggerConcurrency(t *testing.T) {
testConcurrency(t, &log.SwapLogger{})
}
81 changes: 81 additions & 0 deletions sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package log

import (
"io"
"sync"
"sync/atomic"
)

// SwapLogger wraps another logger that may be safely replaced while other
// goroutines use the SwapLogger concurrently. The zero value for a SwapLogger
// will discard all log events without error.
//
// SwapLogger serves well as a package global logger that can be changed by
// importers.
type SwapLogger struct {
logger atomic.Value
}

type loggerStruct struct {
Logger
}

// Log implements the Logger interface by forwarding keyvals to the currently
// wrapped logger. It does not log anything if the wrapped logger is nil.
func (l *SwapLogger) Log(keyvals ...interface{}) error {
s, ok := l.logger.Load().(loggerStruct)
if !ok || s.Logger == nil {
return nil
}
return s.Log(keyvals...)
}

// Swap replaces the currently wrapped logger with logger. Swap may be called
// concurrently with calls to Log from other goroutines.
func (l *SwapLogger) Swap(logger Logger) {
l.logger.Store(loggerStruct{logger})
}

// SyncWriter synchronizes concurrent writes to an io.Writer.
type SyncWriter struct {
mu sync.Mutex
w io.Writer
}

// NewSyncWriter returns a new SyncWriter. The returned writer is safe for
// concurrent use by multiple goroutines.
func NewSyncWriter(w io.Writer) *SyncWriter {
return &SyncWriter{w: w}
}

// Write writes p to the underlying io.Writer. If another write is already in
// progress, the calling goroutine blocks until the SyncWriter is available.
func (w *SyncWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
n, err = w.w.Write(p)
w.mu.Unlock()
return n, err
}

// syncLogger provides concurrent safe logging for another Logger.
type syncLogger struct {
mu sync.Mutex
logger Logger
}

// NewSyncLogger returns a logger that synchronizes concurrent use of the
// wrapped logger. When multiple goroutines use the SyncLogger concurrently
// only one goroutine will be allowed to log to the wrapped logger at a time.
// The other goroutines will block until the logger is available.
func NewSyncLogger(logger Logger) Logger {
return &syncLogger{logger: logger}
}

// Log logs keyvals to the underlying Logger. If another log is already in
// progress, the calling goroutine blocks until the syncLogger is available.
func (l *syncLogger) Log(keyvals ...interface{}) error {
l.mu.Lock()
err := l.logger.Log(keyvals...)
l.mu.Unlock()
return err
}
70 changes: 70 additions & 0 deletions sync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package log_test

import (
"bytes"
"io"
"testing"

"github.com/go-kit/kit/log"
)

func TestSwapLogger(t *testing.T) {
var logger log.SwapLogger

// Zero value does not panic or error.
err := logger.Log("k", "v")
if got, want := err, error(nil); got != want {
t.Errorf("got %v, want %v", got, want)
}

buf := &bytes.Buffer{}
json := log.NewJSONLogger(buf)
logger.Swap(json)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), `{"k":"v"}`+"\n"; got != want {
t.Errorf("got %v, want %v", got, want)
}

buf.Reset()
prefix := log.NewLogfmtLogger(buf)
logger.Swap(prefix)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), "k=v\n"; got != want {
t.Errorf("got %v, want %v", got, want)
}

buf.Reset()
logger.Swap(nil)

if err := logger.Log("k", "v"); err != nil {
t.Error(err)
}
if got, want := buf.String(), ""; got != want {
t.Errorf("got %v, want %v", got, want)
}
}

func TestSwapLoggerConcurrency(t *testing.T) {
testConcurrency(t, &log.SwapLogger{})
}

func TestSyncLoggerConcurrency(t *testing.T) {
var w io.Writer
w = &bytes.Buffer{}
logger := log.NewLogfmtLogger(w)
logger = log.NewSyncLogger(logger)
testConcurrency(t, logger)
}

func TestSyncWriterConcurrency(t *testing.T) {
var w io.Writer
w = &bytes.Buffer{}
w = log.NewSyncWriter(w)
testConcurrency(t, log.NewLogfmtLogger(w))
}

0 comments on commit c35457e

Please sign in to comment.