Skip to content
36 changes: 24 additions & 12 deletions log/concurrency_test.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
package log_test

import (
"strconv"
"sync"
"math"
"testing"

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

// These test are designed to be run with the race detector.

func testConcurrency(t *testing.T, logger log.Logger) {
for _, n := range []int{10, 100, 500} {
wg := sync.WaitGroup{}
wg.Add(n)
for i := 0; i < n; i++ {
go func() { spam(logger); wg.Done() }()
func testConcurrency(t *testing.T, logger log.Logger, total int) {
n := int(math.Sqrt(float64(total)))
share := total / n

errC := make(chan error, n)

for i := 0; i < n; i++ {
go func() {
errC <- spam(logger, share)
}()
}

for i := 0; i < n; i++ {
err := <-errC
if err != nil {
t.Fatalf("concurrent logging error: %v", err)
}
wg.Wait()
}
}

func spam(logger log.Logger) {
for i := 0; i < 100; i++ {
logger.Log("key", strconv.FormatInt(int64(i), 10))
func spam(logger log.Logger, count int) error {
for i := 0; i < count; i++ {
err := logger.Log("key", i)
if err != nil {
return err
}
}
return nil
}
20 changes: 19 additions & 1 deletion log/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
4 changes: 3 additions & 1 deletion log/json_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func (textstringer) String() string {
}

func TestJSONLoggerStringValue(t *testing.T) {
t.Parallel()
tests := []struct {
v interface{}
expected string
Expand Down Expand Up @@ -152,5 +153,6 @@ func BenchmarkJSONLoggerContextual(b *testing.B) {
}

func TestJSONLoggerConcurrency(t *testing.T) {
testConcurrency(t, log.NewJSONLogger(ioutil.Discard))
t.Parallel()
testConcurrency(t, log.NewJSONLogger(ioutil.Discard), 10000)
}
41 changes: 7 additions & 34 deletions log/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})
}
47 changes: 1 addition & 46 deletions log/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func TestContextMissingValue(t *testing.T) {
// whether Context.Log is called via an interface typed variable or a concrete
// typed variable.
func TestContextStackDepth(t *testing.T) {
t.Parallel()
fn := fmt.Sprintf("%n", stack.Caller(0))

var output []interface{}
Expand Down Expand Up @@ -207,49 +208,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{})
}
4 changes: 3 additions & 1 deletion log/logfmt_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

func TestLogfmtLogger(t *testing.T) {
t.Parallel()
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)

Expand Down Expand Up @@ -47,7 +48,8 @@ func BenchmarkLogfmtLoggerContextual(b *testing.B) {
}

func TestLogfmtLoggerConcurrency(t *testing.T) {
testConcurrency(t, log.NewLogfmtLogger(ioutil.Discard))
t.Parallel()
testConcurrency(t, log.NewLogfmtLogger(ioutil.Discard), 10000)
}

type mymap map[int]int
Expand Down
1 change: 1 addition & 0 deletions log/nop_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

func TestNopLogger(t *testing.T) {
t.Parallel()
logger := log.NewNopLogger()
if err := logger.Log("abc", 123); err != nil {
t.Error(err)
Expand Down
Loading