From 010ab9058ca693fe9f835a318af6095461624b6d Mon Sep 17 00:00:00 2001
From: Ayman Bagabas <ayman.bagabas@gmail.com>
Date: Thu, 2 Mar 2023 17:13:04 -0500
Subject: [PATCH] ref(log): use log options to initialize new loggers

- remove interface
- remove functional options
- add options struct
---
 context.go                                  |   9 +-
 context_test.go                             |   8 +-
 examples/batch2/batch2.go                   |   6 +-
 examples/chocolate-chips/chocolate-chips.go |   6 +-
 examples/new/new.go                         |   8 +-
 examples/options/options.go                 |   8 +-
 examples/styles/styles.go                   |   3 +-
 json.go                                     |   2 +-
 json_test.go                                |  15 +-
 log.go                                      | 326 ------------------
 logfmt.go                                   |   2 +-
 logfmt_test.go                              |   3 +-
 logger.go                                   | 359 ++++++++++++++++----
 log_test.go => logger_test.go               |   5 +-
 options.go                                  | 106 +++---
 options_test.go                             |  24 ++
 pkg.go                                      |  51 ++-
 stdlog.go                                   |  13 +-
 stdlog_test.go                              |  20 +-
 text.go                                     |   4 +-
 text_test.go                                |  14 +-
 21 files changed, 469 insertions(+), 523 deletions(-)
 delete mode 100644 log.go
 rename log_test.go => logger_test.go (94%)
 create mode 100644 options_test.go

diff --git a/context.go b/context.go
index d0afe0e..1365f1c 100644
--- a/context.go
+++ b/context.go
@@ -3,18 +3,15 @@ package log
 import "context"
 
 // WithContext wraps the given logger in context.
-func WithContext(ctx context.Context, logger Logger, keyvals ...interface{}) context.Context {
-	if len(keyvals) > 0 {
-		logger = logger.With(keyvals...)
-	}
+func WithContext(ctx context.Context, logger *Logger) context.Context {
 	return context.WithValue(ctx, loggerContextKey, logger)
 }
 
 // FromContext returns the logger from the given context.
 // This will return the default package logger if no logger
 // found in context.
-func FromContext(ctx context.Context) Logger {
-	if logger, ok := ctx.Value(loggerContextKey).(Logger); ok {
+func FromContext(ctx context.Context) *Logger {
+	if logger, ok := ctx.Value(loggerContextKey).(*Logger); ok {
 		return logger
 	}
 	return defaultLogger
diff --git a/context_test.go b/context_test.go
index 91d80b2..6ab628b 100644
--- a/context_test.go
+++ b/context_test.go
@@ -3,6 +3,7 @@ package log
 import (
 	"bytes"
 	"context"
+	"io/ioutil"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -13,15 +14,16 @@ func TestLogContext_empty(t *testing.T) {
 }
 
 func TestLogContext_simple(t *testing.T) {
-	l := New()
+	l := New(ioutil.Discard)
 	ctx := WithContext(context.Background(), l)
 	require.Equal(t, l, FromContext(ctx))
 }
 
 func TestLogContext_fields(t *testing.T) {
 	var buf bytes.Buffer
-	l := New(WithOutput(&buf), WithLevel(DebugLevel))
-	ctx := WithContext(context.Background(), l, "foo", "bar")
+	l := New(&buf)
+	l.SetLevel(DebugLevel)
+	ctx := WithContext(context.Background(), l.With("foo", "bar"))
 	l = FromContext(ctx)
 	require.NotNil(t, l)
 	l.Debug("test")
diff --git a/examples/batch2/batch2.go b/examples/batch2/batch2.go
index 979e29b..3f68bcf 100644
--- a/examples/batch2/batch2.go
+++ b/examples/batch2/batch2.go
@@ -1,14 +1,12 @@
 package main
 
 import (
-	"time"
-
 	"github.com/charmbracelet/log"
 )
 
 func main() {
-	logger := log.New(log.WithTimestamp(), log.WithTimeFormat(time.Kitchen),
-		log.WithCaller(), log.WithPrefix("baking 🍪 ")).With("batch", 2, "chocolateChips", true)
+	logger := log.Default().With("batch", 2, "chocolateChips", true)
+	logger.SetPrefix("baking 🍪 ")
 	logger.SetReportTimestamp(false)
 	logger.SetReportCaller(false)
 	logger.SetLevel(log.DebugLevel)
diff --git a/examples/chocolate-chips/chocolate-chips.go b/examples/chocolate-chips/chocolate-chips.go
index 64f39e1..52c12d3 100644
--- a/examples/chocolate-chips/chocolate-chips.go
+++ b/examples/chocolate-chips/chocolate-chips.go
@@ -1,15 +1,13 @@
 package main
 
 import (
-	"time"
-
 	"github.com/charmbracelet/log"
 )
 
 func main() {
-	logger := log.New(log.WithTimestamp(), log.WithTimeFormat(time.Kitchen),
-		log.WithCaller(), log.WithPrefix("Baking 🍪 "))
+	logger := log.Default().With()
 
+	logger.SetPrefix("Baking 🍪 ")
 	logger.SetReportTimestamp(false)
 	logger.SetReportCaller(false)
 	logger.SetLevel(log.DebugLevel)
diff --git a/examples/new/new.go b/examples/new/new.go
index 685b240..06c69c8 100644
--- a/examples/new/new.go
+++ b/examples/new/new.go
@@ -1,8 +1,12 @@
 package main
 
-import "github.com/charmbracelet/log"
+import (
+	"os"
+
+	"github.com/charmbracelet/log"
+)
 
 func main() {
-	logger := log.New()
+	logger := log.New(os.Stderr)
 	logger.Warn("chewy!", "butter", true)
 }
diff --git a/examples/options/options.go b/examples/options/options.go
index c3711a6..83d5388 100644
--- a/examples/options/options.go
+++ b/examples/options/options.go
@@ -1,14 +1,18 @@
 package main
 
 import (
+	"os"
 	"time"
 
 	"github.com/charmbracelet/log"
 )
 
 func main() {
-	logger := log.New(log.WithTimestamp(), log.WithTimeFormat(time.Kitchen),
-		log.WithCaller(), log.WithPrefix("Baking 🍪 "))
+	logger := log.New(os.Stderr)
+	logger.SetPrefix("Baking 🍪 ")
+	logger.SetTimeFormat(time.Kitchen)
+	logger.SetReportTimestamp(true)
+	logger.SetReportCaller(true)
 	logger.Info("Starting oven!", "degree", 375)
 	time.Sleep(3 * time.Second)
 	logger.Info("Finished baking")
diff --git a/examples/styles/styles.go b/examples/styles/styles.go
index 87b8e0c..3f51dfe 100644
--- a/examples/styles/styles.go
+++ b/examples/styles/styles.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"os"
 	"time"
 
 	"github.com/charmbracelet/lipgloss"
@@ -18,7 +19,7 @@ func main() {
 		Foreground(lipgloss.Color("0"))
 	log.KeyStyles["err"] = lipgloss.NewStyle().Foreground(lipgloss.Color("204"))
 	log.ValueStyles["err"] = lipgloss.NewStyle().Bold(true)
-	logger := log.New()
+	logger := log.New(os.Stderr)
 	logger.Error("Whoops!", "err", "kitchen on fire")
 	time.Sleep(3 * time.Second)
 }
diff --git a/json.go b/json.go
index 2f2b910..5bdc097 100644
--- a/json.go
+++ b/json.go
@@ -6,7 +6,7 @@ import (
 	"time"
 )
 
-func (l *logger) jsonFormatter(keyvals ...interface{}) {
+func (l *Logger) jsonFormatter(keyvals ...interface{}) {
 	m := make(map[string]interface{}, len(keyvals)/2)
 	for i := 0; i < len(keyvals); i += 2 {
 		switch keyvals[i] {
diff --git a/json_test.go b/json_test.go
index 5187ab7..a5a52b3 100644
--- a/json_test.go
+++ b/json_test.go
@@ -13,7 +13,8 @@ import (
 
 func TestJson(t *testing.T) {
 	var buf bytes.Buffer
-	l := New(WithOutput(&buf), WithFormatter(JSONFormatter))
+	l := New(&buf)
+	l.SetFormatter(JSONFormatter)
 	cases := []struct {
 		name     string
 		expected string
@@ -124,8 +125,10 @@ func TestJson(t *testing.T) {
 
 func TestJsonCaller(t *testing.T) {
 	var buf bytes.Buffer
-	l := New(WithOutput(&buf), WithLevel(DebugLevel),
-		WithFormatter(JSONFormatter), WithCaller())
+	l := New(&buf)
+	l.SetFormatter(JSONFormatter)
+	l.SetReportCaller(true)
+	l.SetLevel(DebugLevel)
 	_, file, line, _ := runtime.Caller(0)
 	cases := []struct {
 		name     string
@@ -169,8 +172,10 @@ func TestJsonCustomKey(t *testing.T) {
 		TimestampKey = oldTsKey
 	}()
 	TimestampKey = "time"
-	logger := New(WithOutput(&buf), WithTimestamp(),
-		WithTimeFunction(_zeroTime), WithFormatter(JSONFormatter))
+	logger := New(&buf)
+	logger.SetTimeFunction(_zeroTime)
+	logger.SetFormatter(JSONFormatter)
+	logger.SetReportTimestamp(true)
 	logger.Info("info")
 	require.Equal(t, "{\"lvl\":\"info\",\"msg\":\"info\",\"time\":\"0001/01/01 00:00:00\"}\n", buf.String())
 }
diff --git a/log.go b/log.go
deleted file mode 100644
index c28f359..0000000
--- a/log.go
+++ /dev/null
@@ -1,326 +0,0 @@
-package log
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"runtime"
-	"strings"
-	"sync"
-	"sync/atomic"
-	"time"
-)
-
-var (
-	// ErrMissingValue is returned when a key is missing a value.
-	ErrMissingValue = fmt.Errorf("missing value")
-)
-
-// LoggerOption is an option for a logger.
-type LoggerOption = func(*logger)
-
-var _ Logger = &logger{}
-
-// logger is a logger that implements Logger.
-type logger struct {
-	w  io.Writer
-	b  bytes.Buffer
-	mu *sync.RWMutex
-
-	isDiscard uint32
-
-	level        int32
-	prefix       string
-	timeFunc     TimeFunction
-	timeFormat   string
-	callerOffset int
-	formatter    Formatter
-
-	caller    bool
-	noStyles  bool
-	timestamp bool
-
-	keyvals []interface{}
-
-	helpers sync.Map
-}
-
-// New returns a new logger. It uses os.Stderr as the default output.
-func New(opts ...LoggerOption) Logger {
-	l := &logger{
-		b:     bytes.Buffer{},
-		mu:    &sync.RWMutex{},
-		level: int32(InfoLevel),
-	}
-
-	for _, opt := range opts {
-		opt(l)
-	}
-
-	if l.w == nil {
-		l.w = os.Stderr
-	}
-
-	l.SetOutput(l.w)
-	l.SetLevel(Level(l.level))
-
-	if l.timeFunc == nil {
-		l.timeFunc = time.Now
-	}
-
-	if l.timeFormat == "" {
-		l.timeFormat = DefaultTimeFormat
-	}
-
-	return l
-}
-
-func (l *logger) log(level Level, msg interface{}, keyvals ...interface{}) {
-	if atomic.LoadUint32(&l.isDiscard) != 0 {
-		return
-	}
-
-	// check if the level is allowed
-	if atomic.LoadInt32(&l.level) > int32(level) {
-		return
-	}
-
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	defer l.b.Reset()
-
-	var kvs []interface{}
-	if l.timestamp {
-		kvs = append(kvs, TimestampKey, l.timeFunc())
-	}
-
-	if level != noLevel {
-		kvs = append(kvs, LevelKey, level)
-	}
-
-	if l.caller {
-		// Call stack is log.Error -> log.log (2)
-		file, line, _ := l.fillLoc(l.callerOffset + 2)
-		caller := fmt.Sprintf("%s:%d", trimCallerPath(file), line)
-		kvs = append(kvs, CallerKey, caller)
-	}
-
-	if l.prefix != "" {
-		kvs = append(kvs, PrefixKey, l.prefix+":")
-	}
-
-	if msg != nil {
-		m := fmt.Sprint(msg)
-		kvs = append(kvs, MessageKey, m)
-	}
-
-	// append logger fields
-	kvs = append(kvs, l.keyvals...)
-	if len(l.keyvals)%2 != 0 {
-		kvs = append(kvs, ErrMissingValue)
-	}
-	// append the rest
-	kvs = append(kvs, keyvals...)
-	if len(keyvals)%2 != 0 {
-		kvs = append(kvs, ErrMissingValue)
-	}
-
-	switch l.formatter {
-	case LogfmtFormatter:
-		l.logfmtFormatter(kvs...)
-	case JSONFormatter:
-		l.jsonFormatter(kvs...)
-	default:
-		l.textFormatter(kvs...)
-	}
-
-	_, _ = l.w.Write(l.b.Bytes())
-}
-
-// Helper marks the calling function as a helper
-// and skips it for source location information.
-// It's the equivalent of testing.TB.Helper().
-func (l *logger) Helper() {
-	l.helper(1)
-}
-
-func (l *logger) helper(skip int) {
-	_, _, fn := location(skip + 1)
-	l.helpers.LoadOrStore(fn, struct{}{})
-}
-
-func (l *logger) fillLoc(skip int) (file string, line int, fn string) {
-	// Copied from testing.T
-	const maxStackLen = 50
-	var pc [maxStackLen]uintptr
-
-	// Skip two extra frames to account for this function
-	// and runtime.Callers itself.
-	n := runtime.Callers(skip+2, pc[:])
-	frames := runtime.CallersFrames(pc[:n])
-	for {
-		frame, more := frames.Next()
-		_, helper := l.helpers.Load(frame.Function)
-		if !helper || !more {
-			// Found a frame that wasn't a helper function.
-			// Or we ran out of frames to check.
-			return frame.File, frame.Line, frame.Function
-		}
-	}
-}
-
-func location(skip int) (file string, line int, fn string) {
-	pc, file, line, _ := runtime.Caller(skip + 1)
-	f := runtime.FuncForPC(pc)
-	return file, line, f.Name()
-}
-
-// Cleanup a path by returning the last 2 segments of the path only.
-func trimCallerPath(path string) string {
-	// lovely borrowed from zap
-	// nb. To make sure we trim the path correctly on Windows too, we
-	// counter-intuitively need to use '/' and *not* os.PathSeparator here,
-	// because the path given originates from Go stdlib, specifically
-	// runtime.Caller() which (as of Mar/17) returns forward slashes even on
-	// Windows.
-	//
-	// See https://github.com/golang/go/issues/3335
-	// and https://github.com/golang/go/issues/18151
-	//
-	// for discussion on the issue on Go side.
-
-	// Find the last separator.
-	idx := strings.LastIndexByte(path, '/')
-	if idx == -1 {
-		return path
-	}
-
-	// Find the penultimate separator.
-	idx = strings.LastIndexByte(path[:idx], '/')
-	if idx == -1 {
-		return path
-	}
-
-	return path[idx+1:]
-}
-
-// SetReportTimestamp sets whether the timestamp should be reported.
-func (l *logger) SetReportTimestamp(report bool) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.timestamp = report
-}
-
-// SetReportCaller sets whether the caller location should be reported.
-func (l *logger) SetReportCaller(report bool) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.caller = report
-}
-
-// GetLevel returns the current level.
-func (l *logger) GetLevel() Level {
-	l.mu.RLock()
-	defer l.mu.RUnlock()
-	return Level(l.level)
-}
-
-// SetLevel sets the current level.
-func (l *logger) SetLevel(level Level) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	atomic.StoreInt32(&l.level, int32(level))
-}
-
-// GetPrefix returns the current prefix.
-func (l *logger) GetPrefix() string {
-	l.mu.RLock()
-	defer l.mu.RUnlock()
-	return l.prefix
-}
-
-// SetPrefix sets the current prefix.
-func (l *logger) SetPrefix(prefix string) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.prefix = prefix
-}
-
-// SetTimeFormat sets the time format.
-func (l *logger) SetTimeFormat(format string) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.timeFormat = format
-}
-
-// SetTimeFunction sets the time function.
-func (l *logger) SetTimeFunction(f TimeFunction) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.timeFunc = f
-}
-
-// SetOutput sets the output destination.
-func (l *logger) SetOutput(w io.Writer) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.w = w
-	var isDiscard uint32 = 0
-	if w == ioutil.Discard {
-		isDiscard = 1
-	}
-	atomic.StoreUint32(&l.isDiscard, isDiscard)
-	if !isTerminal(w) {
-		// This only affects the TextFormatter
-		l.noStyles = true
-	}
-}
-
-// SetFormatter sets the formatter.
-func (l *logger) SetFormatter(f Formatter) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	l.formatter = f
-}
-
-// With returns a new logger with the given keyvals added.
-func (l *logger) With(keyvals ...interface{}) Logger {
-	sl := *l
-	sl.b = bytes.Buffer{}
-	sl.mu = &sync.RWMutex{}
-	sl.keyvals = append(l.keyvals, keyvals...)
-	return &sl
-}
-
-// Debug prints a debug message.
-func (l *logger) Debug(msg interface{}, keyvals ...interface{}) {
-	l.log(DebugLevel, msg, keyvals...)
-}
-
-// Info prints an info message.
-func (l *logger) Info(msg interface{}, keyvals ...interface{}) {
-	l.log(InfoLevel, msg, keyvals...)
-}
-
-// Warn prints a warning message.
-func (l *logger) Warn(msg interface{}, keyvals ...interface{}) {
-	l.log(WarnLevel, msg, keyvals...)
-}
-
-// Error prints an error message.
-func (l *logger) Error(msg interface{}, keyvals ...interface{}) {
-	l.log(ErrorLevel, msg, keyvals...)
-}
-
-// Fatal prints a fatal message and exits.
-func (l *logger) Fatal(msg interface{}, keyvals ...interface{}) {
-	l.log(FatalLevel, msg, keyvals...)
-	os.Exit(1)
-}
-
-// Print prints a message with no level.
-func (l *logger) Print(msg interface{}, keyvals ...interface{}) {
-	l.log(noLevel, msg, keyvals...)
-}
diff --git a/logfmt.go b/logfmt.go
index 07382cf..2878b97 100644
--- a/logfmt.go
+++ b/logfmt.go
@@ -8,7 +8,7 @@ import (
 	"github.com/go-logfmt/logfmt"
 )
 
-func (l *logger) logfmtFormatter(keyvals ...interface{}) {
+func (l *Logger) logfmtFormatter(keyvals ...interface{}) {
 	e := logfmt.NewEncoder(&l.b)
 
 	for i := 0; i < len(keyvals); i += 2 {
diff --git a/logfmt_test.go b/logfmt_test.go
index 8075034..aa49626 100644
--- a/logfmt_test.go
+++ b/logfmt_test.go
@@ -10,7 +10,8 @@ import (
 
 func TestLogfmt(t *testing.T) {
 	var buf bytes.Buffer
-	l := New(WithOutput(&buf), WithFormatter(LogfmtFormatter))
+	l := New(&buf)
+	l.SetFormatter(LogfmtFormatter)
 	cases := []struct {
 		name     string
 		expected string
diff --git a/logger.go b/logger.go
index 7878c31..cefa882 100644
--- a/logger.go
+++ b/logger.go
@@ -1,78 +1,297 @@
 package log
 
 import (
+	"bytes"
+	"fmt"
 	"io"
-	"log"
-	"time"
+	"io/ioutil"
+	"os"
+	"runtime"
+	"strings"
+	"sync"
+	"sync/atomic"
 )
 
-// DefaultTimeFormat is the default time format.
-const DefaultTimeFormat = "2006/01/02 15:04:05"
-
-// TimeFunction is a function that returns a time.Time.
-type TimeFunction = func() time.Time
-
-// NowUTC is a convenient function that returns the
-// current time in UTC timezone.
-//
-// This is to be used as a time function.
-// For example:
-//
-//	log.SetTimeFunction(log.NowUTC)
-func NowUTC() time.Time {
-	return time.Now().UTC()
-}
-
-// Logger is an interface for logging.
-type Logger interface {
-	// SetLevel sets the allowed level.
-	SetLevel(level Level)
-	// GetLevel returns the allowed level.
-	GetLevel() Level
-
-	// SetPrefix sets the logger prefix. The default is no prefix.
-	SetPrefix(prefix string)
-	// GetPrefix returns the logger prefix.
-	GetPrefix() string
-
-	// SetReportTimestamp sets whether the logger should report the timestamp.
-	SetReportTimestamp(bool)
-	// SetReportCaller sets whether the logger should report the caller location.
-	SetReportCaller(bool)
-	// SetTimeFunction sets the time function used to get the time.
-	// The default is time.Now.
+var (
+	// ErrMissingValue is returned when a key is missing a value.
+	ErrMissingValue = fmt.Errorf("missing value")
+)
+
+// LoggerOption is an option for a logger.
+type LoggerOption = func(*Logger)
+
+// Logger is a Logger that implements Logger.
+type Logger struct {
+	w  io.Writer
+	b  bytes.Buffer
+	mu *sync.RWMutex
+
+	isDiscard uint32
+
+	level        int32
+	prefix       string
+	timeFunc     TimeFunction
+	timeFormat   string
+	callerOffset int
+	formatter    Formatter
+
+	noStyles        bool
+	reportCaller    bool
+	reportTimestamp bool
+
+	fields []interface{}
+
+	helpers *sync.Map
+}
+
+func (l *Logger) log(level Level, msg interface{}, keyvals ...interface{}) {
+	if atomic.LoadUint32(&l.isDiscard) != 0 {
+		return
+	}
+
+	// check if the level is allowed
+	if atomic.LoadInt32(&l.level) > int32(level) {
+		return
+	}
+
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	defer l.b.Reset()
+
+	var kvs []interface{}
+	if l.reportTimestamp {
+		kvs = append(kvs, TimestampKey, l.timeFunc())
+	}
+
+	if level != noLevel {
+		kvs = append(kvs, LevelKey, level)
+	}
+
+	if l.reportCaller {
+		// Call stack is log.Error -> log.log (2)
+		file, line, _ := l.fillLoc(l.callerOffset + 2)
+		caller := fmt.Sprintf("%s:%d", trimCallerPath(file), line)
+		kvs = append(kvs, CallerKey, caller)
+	}
+
+	if l.prefix != "" {
+		kvs = append(kvs, PrefixKey, l.prefix+":")
+	}
+
+	if msg != nil {
+		m := fmt.Sprint(msg)
+		kvs = append(kvs, MessageKey, m)
+	}
+
+	// append logger fields
+	kvs = append(kvs, l.fields...)
+	if len(l.fields)%2 != 0 {
+		kvs = append(kvs, ErrMissingValue)
+	}
+	// append the rest
+	kvs = append(kvs, keyvals...)
+	if len(keyvals)%2 != 0 {
+		kvs = append(kvs, ErrMissingValue)
+	}
+
+	switch l.formatter {
+	case LogfmtFormatter:
+		l.logfmtFormatter(kvs...)
+	case JSONFormatter:
+		l.jsonFormatter(kvs...)
+	default:
+		l.textFormatter(kvs...)
+	}
+
+	_, _ = l.w.Write(l.b.Bytes())
+}
+
+// Helper marks the calling function as a helper
+// and skips it for source location information.
+// It's the equivalent of testing.TB.Helper().
+func (l *Logger) Helper() {
+	l.helper(1)
+}
+
+func (l *Logger) helper(skip int) {
+	_, _, fn := location(skip + 1)
+	l.helpers.LoadOrStore(fn, struct{}{})
+}
+
+func (l *Logger) fillLoc(skip int) (file string, line int, fn string) {
+	// Copied from testing.T
+	const maxStackLen = 50
+	var pc [maxStackLen]uintptr
+
+	// Skip two extra frames to account for this function
+	// and runtime.Callers itself.
+	n := runtime.Callers(skip+2, pc[:])
+	frames := runtime.CallersFrames(pc[:n])
+	for {
+		frame, more := frames.Next()
+		_, helper := l.helpers.Load(frame.Function)
+		if !helper || !more {
+			// Found a frame that wasn't a helper function.
+			// Or we ran out of frames to check.
+			return frame.File, frame.Line, frame.Function
+		}
+	}
+}
+
+func location(skip int) (file string, line int, fn string) {
+	pc, file, line, _ := runtime.Caller(skip + 1)
+	f := runtime.FuncForPC(pc)
+	return file, line, f.Name()
+}
+
+// Cleanup a path by returning the last 2 segments of the path only.
+func trimCallerPath(path string) string {
+	// lovely borrowed from zap
+	// nb. To make sure we trim the path correctly on Windows too, we
+	// counter-intuitively need to use '/' and *not* os.PathSeparator here,
+	// because the path given originates from Go stdlib, specifically
+	// runtime.Caller() which (as of Mar/17) returns forward slashes even on
+	// Windows.
 	//
-	// To use UTC time instead of local time set the time
-	// function to `NowUTC`.
-	SetTimeFunction(f TimeFunction)
-	// SetTimeFormat sets the time format. The default is "2006/01/02 15:04:05".
-	SetTimeFormat(format string)
-	// SetOutput sets the output destination. The default is os.Stderr.
-	SetOutput(w io.Writer)
-	// SetFormatter sets the formatter. The default is TextFormatter.
-	SetFormatter(f Formatter)
-
-	// Helper marks the calling function as a helper
-	// and skips it for source location information.
-	// It's the equivalent of testing.TB.Helper().
-	Helper()
-
-	// With returns a new sub logger with the given key value pairs.
-	With(keyval ...interface{}) Logger
-
-	// Debug logs a debug message.
-	Debug(msg interface{}, keyval ...interface{})
-	// Info logs an info message.
-	Info(msg interface{}, keyval ...interface{})
-	// Warn logs a warning message.
-	Warn(msg interface{}, keyval ...interface{})
-	// Error logs an error message.
-	Error(msg interface{}, keyval ...interface{})
-	// Fatal logs a fatal message.
-	Fatal(msg interface{}, keyval ...interface{})
-	// Print logs a message with no level.
-	Print(msg interface{}, keyval ...interface{})
-
-	// StandardLog returns a standard logger from this logger.
-	StandardLog(...StandardLogOption) *log.Logger
+	// See https://github.com/golang/go/issues/3335
+	// and https://github.com/golang/go/issues/18151
+	//
+	// for discussion on the issue on Go side.
+
+	// Find the last separator.
+	idx := strings.LastIndexByte(path, '/')
+	if idx == -1 {
+		return path
+	}
+
+	// Find the penultimate separator.
+	idx = strings.LastIndexByte(path[:idx], '/')
+	if idx == -1 {
+		return path
+	}
+
+	return path[idx+1:]
+}
+
+// SetReportTimestamp sets whether the timestamp should be reported.
+func (l *Logger) SetReportTimestamp(report bool) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.reportTimestamp = report
+}
+
+// SetReportCaller sets whether the caller location should be reported.
+func (l *Logger) SetReportCaller(report bool) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.reportCaller = report
+}
+
+// GetLevel returns the current level.
+func (l *Logger) GetLevel() Level {
+	l.mu.RLock()
+	defer l.mu.RUnlock()
+	return Level(l.level)
+}
+
+// SetLevel sets the current level.
+func (l *Logger) SetLevel(level Level) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	atomic.StoreInt32(&l.level, int32(level))
+}
+
+// GetPrefix returns the current prefix.
+func (l *Logger) GetPrefix() string {
+	l.mu.RLock()
+	defer l.mu.RUnlock()
+	return l.prefix
+}
+
+// SetPrefix sets the current prefix.
+func (l *Logger) SetPrefix(prefix string) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.prefix = prefix
+}
+
+// SetTimeFormat sets the time format.
+func (l *Logger) SetTimeFormat(format string) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.timeFormat = format
+}
+
+// SetTimeFunction sets the time function.
+func (l *Logger) SetTimeFunction(f TimeFunction) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.timeFunc = f
+}
+
+// SetOutput sets the output destination.
+func (l *Logger) SetOutput(w io.Writer) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	if w == nil {
+		w = os.Stderr
+	}
+	l.w = w
+	var isDiscard uint32
+	if w == ioutil.Discard {
+		isDiscard = 1
+	}
+	atomic.StoreUint32(&l.isDiscard, isDiscard)
+	if !isTerminal(w) {
+		// This only affects the TextFormatter
+		l.noStyles = true
+	}
+}
+
+// SetFormatter sets the formatter.
+func (l *Logger) SetFormatter(f Formatter) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.formatter = f
+}
+
+// With returns a new logger with the given keyvals added.
+func (l *Logger) With(keyvals ...interface{}) *Logger {
+	sl := *l
+	sl.b = bytes.Buffer{}
+	sl.mu = &sync.RWMutex{}
+	sl.helpers = &sync.Map{}
+	sl.fields = append(l.fields, keyvals...)
+	return &sl
+}
+
+// Debug prints a debug message.
+func (l *Logger) Debug(msg interface{}, keyvals ...interface{}) {
+	l.log(DebugLevel, msg, keyvals...)
+}
+
+// Info prints an info message.
+func (l *Logger) Info(msg interface{}, keyvals ...interface{}) {
+	l.log(InfoLevel, msg, keyvals...)
+}
+
+// Warn prints a warning message.
+func (l *Logger) Warn(msg interface{}, keyvals ...interface{}) {
+	l.log(WarnLevel, msg, keyvals...)
+}
+
+// Error prints an error message.
+func (l *Logger) Error(msg interface{}, keyvals ...interface{}) {
+	l.log(ErrorLevel, msg, keyvals...)
+}
+
+// Fatal prints a fatal message and exits.
+func (l *Logger) Fatal(msg interface{}, keyvals ...interface{}) {
+	l.log(FatalLevel, msg, keyvals...)
+	os.Exit(1)
+}
+
+// Print prints a message with no level.
+func (l *Logger) Print(msg interface{}, keyvals ...interface{}) {
+	l.log(noLevel, msg, keyvals...)
 }
diff --git a/log_test.go b/logger_test.go
similarity index 94%
rename from log_test.go
rename to logger_test.go
index 4c85f86..3044244 100644
--- a/log_test.go
+++ b/logger_test.go
@@ -9,7 +9,7 @@ import (
 
 func TestSubLogger(t *testing.T) {
 	var buf bytes.Buffer
-	l := New(WithOutput(&buf))
+	l := New(&buf)
 	cases := []struct {
 		name     string
 		expected string
@@ -69,7 +69,8 @@ func TestWrongLevel(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
 			buf.Reset()
-			l := New(WithOutput(&buf), WithLevel(c.level))
+			l := New(&buf)
+			l.SetLevel(c.level)
 			l.Info("info")
 			assert.Equal(t, c.expected, buf.String())
 		})
diff --git a/options.go b/options.go
index a1581df..4828479 100644
--- a/options.go
+++ b/options.go
@@ -1,70 +1,42 @@
 package log
 
-import "io"
-
-// WithOutput returns a LoggerOption that sets the output for the logger. The
-// default is os.Stderr.
-func WithOutput(w io.Writer) LoggerOption {
-	return func(l *logger) {
-		l.w = w
-	}
-}
-
-// WithTimeFunction returns a LoggerOption that sets the time function for the
-// logger. The default is time.Now.
-func WithTimeFunction(f TimeFunction) LoggerOption {
-	return func(l *logger) {
-		l.timeFunc = f
-	}
-}
-
-// WithTimeFormat returns a LoggerOption that sets the time format for the
-// logger. The default is "2006/01/02 15:04:05".
-func WithTimeFormat(format string) LoggerOption {
-	return func(l *logger) {
-		l.timeFormat = format
-	}
-}
-
-// WithLevel returns a LoggerOption that sets the level for the logger. The
-// default is InfoLevel.
-func WithLevel(level Level) LoggerOption {
-	return func(l *logger) {
-		l.level = int32(level)
-	}
-}
-
-// WithPrefix returns a LoggerOption that sets the prefix for the logger.
-func WithPrefix(prefix string) LoggerOption {
-	return func(l *logger) {
-		l.prefix = prefix
-	}
-}
-
-// WithTimestamp returns a LoggerOption that enables timestamps for the logger.
-func WithTimestamp() LoggerOption {
-	return func(l *logger) {
-		l.timestamp = true
-	}
-}
-
-// WithCaller returns a LoggerOption that enables caller for the logger.
-func WithCaller() LoggerOption {
-	return func(l *logger) {
-		l.caller = true
-	}
-}
-
-// WithFields returns a LoggerOption that sets the fields for the logger.
-func WithFields(keyvals ...interface{}) LoggerOption {
-	return func(l *logger) {
-		l.keyvals = keyvals
-	}
-}
-
-// WithFormatter returns a LoggerOption that sets the formatter for the logger.
-func WithFormatter(f Formatter) LoggerOption {
-	return func(l *logger) {
-		l.formatter = f
-	}
+import (
+	"time"
+)
+
+// DefaultTimeFormat is the default time format.
+const DefaultTimeFormat = "2006/01/02 15:04:05"
+
+// TimeFunction is a function that returns a time.Time.
+type TimeFunction = func() time.Time
+
+// NowUTC is a convenient function that returns the
+// current time in UTC timezone.
+//
+// This is to be used as a time function.
+// For example:
+//
+//	log.SetTimeFunction(log.NowUTC)
+func NowUTC() time.Time {
+	return time.Now().UTC()
+}
+
+// Options is the options for the logger.
+type Options struct {
+	// TimeFunction is the time function for the logger. The default is time.Now.
+	TimeFunction TimeFunction
+	// TimeFormat is the time format for the logger. The default is "2006/01/02 15:04:05".
+	TimeFormat string
+	// Level is the level for the logger. The default is InfoLevel.
+	Level Level
+	// Prefix is the prefix for the logger. The default is no prefix.
+	Prefix string
+	// ReportTimestamp is whether the logger should report the timestamp. The default is false.
+	ReportTimestamp bool
+	// ReportCaller is whether the logger should report the caller location. The default is false.
+	ReportCaller bool
+	// Fields is the fields for the logger. The default is no fields.
+	Fields []interface{}
+	// Formatter is the formatter for the logger. The default is TextFormatter.
+	Formatter Formatter
 }
diff --git a/options_test.go b/options_test.go
new file mode 100644
index 0000000..aee9ba8
--- /dev/null
+++ b/options_test.go
@@ -0,0 +1,24 @@
+package log
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestOptions(t *testing.T) {
+	opts := Options{
+		Level:        ErrorLevel,
+		ReportCaller: true,
+		Fields:       []interface{}{"foo", "bar"},
+	}
+	logger := NewWithOptions(ioutil.Discard, opts)
+	require.Equal(t, ErrorLevel, logger.GetLevel())
+	require.True(t, logger.reportCaller)
+	require.False(t, logger.reportTimestamp)
+	require.Equal(t, []interface{}{"foo", "bar"}, logger.fields)
+	require.Equal(t, TextFormatter, logger.formatter)
+	require.Equal(t, DefaultTimeFormat, logger.timeFormat)
+	require.NotNil(t, logger.timeFunc)
+}
diff --git a/pkg.go b/pkg.go
index 3edd771..d52c534 100644
--- a/pkg.go
+++ b/pkg.go
@@ -1,18 +1,61 @@
 package log
 
 import (
+	"bytes"
 	"io"
 	"log"
 	"os"
+	"sync"
+	"time"
 )
 
-var defaultLogger = New(WithTimestamp()).(*logger)
+var defaultLogger = NewWithOptions(os.Stderr, Options{ReportTimestamp: true})
 
 // Default returns the default logger. The default logger comes with timestamp enabled.
-func Default() Logger {
+func Default() *Logger {
 	return defaultLogger
 }
 
+// SetDefault sets the default global logger.
+func SetDefault(logger *Logger) {
+	defaultLogger = logger
+}
+
+// New returns a new logger with the default options.
+func New(w io.Writer) *Logger {
+	return NewWithOptions(w, Options{})
+}
+
+// NewWithOptions returns a new logger using the provided options.
+func NewWithOptions(w io.Writer, o Options) *Logger {
+	l := &Logger{
+		b:               bytes.Buffer{},
+		mu:              &sync.RWMutex{},
+		helpers:         &sync.Map{},
+		level:           int32(o.Level),
+		reportTimestamp: o.ReportTimestamp,
+		reportCaller:    o.ReportCaller,
+		prefix:          o.Prefix,
+		timeFunc:        o.TimeFunction,
+		timeFormat:      o.TimeFormat,
+		formatter:       o.Formatter,
+		fields:          o.Fields,
+	}
+
+	l.SetOutput(w)
+	l.SetLevel(Level(l.level))
+
+	if l.timeFunc == nil {
+		l.timeFunc = time.Now
+	}
+
+	if l.timeFormat == "" {
+		l.timeFormat = DefaultTimeFormat
+	}
+
+	return l
+}
+
 // SetReportTimestamp sets whether to report timestamp for the default logger.
 func SetReportTimestamp(report bool) {
 	defaultLogger.SetReportTimestamp(report)
@@ -64,7 +107,7 @@ func GetPrefix() string {
 }
 
 // With returns a new logger with the given keyvals.
-func With(keyvals ...interface{}) Logger {
+func With(keyvals ...interface{}) *Logger {
 	return defaultLogger.With(keyvals...)
 }
 
@@ -108,6 +151,6 @@ func Print(msg interface{}, keyvals ...interface{}) {
 }
 
 // StandardLog returns a standard logger from the default logger.
-func StandardLog(opts ...StandardLogOption) *log.Logger {
+func StandardLog(opts ...StandardLogOptions) *log.Logger {
 	return defaultLogger.StandardLog(opts...)
 }
diff --git a/stdlog.go b/stdlog.go
index 8b148b6..5911c61 100644
--- a/stdlog.go
+++ b/stdlog.go
@@ -3,11 +3,12 @@ package log
 import (
 	"log"
 	"strings"
+	"sync"
 )
 
 type stdLogWriter struct {
-	l   *logger
-	opt *StandardLogOption
+	l   *Logger
+	opt *StandardLogOptions
 }
 
 func (l *stdLogWriter) Write(p []byte) (n int, err error) {
@@ -44,16 +45,18 @@ func (l *stdLogWriter) Write(p []byte) (n int, err error) {
 	return len(p), nil
 }
 
-// StandardLogOption can be used to configure the standard log adapter.
-type StandardLogOption struct {
+// StandardLogOptions can be used to configure the standard log adapter.
+type StandardLogOptions struct {
 	ForceLevel Level
 }
 
 // StandardLog returns a standard logger from Logger. The returned logger
 // can infer log levels from message prefix. Expected prefixes are DEBUG, INFO,
 // WARN, ERROR, and ERR.
-func (l *logger) StandardLog(opts ...StandardLogOption) *log.Logger {
+func (l *Logger) StandardLog(opts ...StandardLogOptions) *log.Logger {
 	nl := *l
+	nl.mu = &sync.RWMutex{}
+	nl.helpers = &sync.Map{}
 	// The caller stack is
 	// log.Printf() -> l.Output() -> l.out.Write(stdLogger.Write)
 	nl.callerOffset += 3
diff --git a/stdlog_test.go b/stdlog_test.go
index c8df095..b6b8d5c 100644
--- a/stdlog_test.go
+++ b/stdlog_test.go
@@ -14,8 +14,8 @@ import (
 
 func TestStdLog(t *testing.T) {
 	var buf bytes.Buffer
+	l := New(&buf)
 	cases := []struct {
-		logger   Logger
 		f        func(l *log.Logger)
 		name     string
 		expected string
@@ -23,28 +23,25 @@ func TestStdLog(t *testing.T) {
 		{
 			name:     "simple",
 			expected: "INFO info\n",
-			logger:   New(),
 			f:        func(l *log.Logger) { l.Print("info") },
 		},
 		{
 			name:     "without level",
 			expected: "INFO coffee\n",
-			logger:   New(),
 			f:        func(l *log.Logger) { l.Print("coffee") },
 		},
 		{
 			name:     "error level",
 			expected: "ERROR coffee\n",
-			logger:   New(),
 			f:        func(l *log.Logger) { l.Print("ERROR coffee") },
 		},
 	}
 	for _, c := range cases {
 		buf.Reset()
 		t.Run(c.name, func(t *testing.T) {
-			c.logger.SetOutput(&buf)
-			c.logger.SetTimeFunction(_zeroTime)
-			c.f(c.logger.StandardLog())
+			l.SetOutput(&buf)
+			l.SetTimeFunction(_zeroTime)
+			c.f(l.StandardLog())
 			assert.Equal(t, c.expected, buf.String())
 		})
 	}
@@ -52,7 +49,7 @@ func TestStdLog(t *testing.T) {
 
 func TestStdLog_forceLevel(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf))
+	logger := New(&buf)
 	cases := []struct {
 		name     string
 		expected string
@@ -77,7 +74,7 @@ func TestStdLog_forceLevel(t *testing.T) {
 	for _, c := range cases {
 		buf.Reset()
 		t.Run(c.name, func(t *testing.T) {
-			l := logger.StandardLog(StandardLogOption{ForceLevel: c.level})
+			l := logger.StandardLog(StandardLogOptions{ForceLevel: c.level})
 			l.Print("coffee")
 			assert.Equal(t, c.expected, buf.String())
 		})
@@ -86,7 +83,8 @@ func TestStdLog_forceLevel(t *testing.T) {
 
 func TestStdLog_writer(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf), WithCaller())
+	logger := New(&buf)
+	logger.SetReportCaller(true)
 	_, file, line, ok := runtime.Caller(0)
 	require.True(t, ok)
 	cases := []struct {
@@ -113,7 +111,7 @@ func TestStdLog_writer(t *testing.T) {
 	for _, c := range cases {
 		buf.Reset()
 		t.Run(c.name, func(t *testing.T) {
-			l := log.New(logger.StandardLog(StandardLogOption{ForceLevel: c.level}).Writer(), "", 0)
+			l := log.New(logger.StandardLog(StandardLogOptions{ForceLevel: c.level}).Writer(), "", 0)
 			l.Print("coffee")
 			assert.Equal(t, c.expected, buf.String())
 		})
diff --git a/text.go b/text.go
index ba384bb..943d0f7 100644
--- a/text.go
+++ b/text.go
@@ -15,7 +15,7 @@ const (
 	indentSeparator = "  │ "
 )
 
-func (l *logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) {
+func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) {
 	// kindly borrowed from hclog
 	for {
 		nl := strings.IndexByte(str, '\n')
@@ -146,7 +146,7 @@ func needsQuoting(str string) bool {
 	return false
 }
 
-func (l *logger) textFormatter(keyvals ...interface{}) {
+func (l *Logger) textFormatter(keyvals ...interface{}) {
 	for i := 0; i < len(keyvals); i += 2 {
 		switch keyvals[i] {
 		case TimestampKey:
diff --git a/text_test.go b/text_test.go
index 4f87059..f51e14a 100644
--- a/text_test.go
+++ b/text_test.go
@@ -22,7 +22,8 @@ func _zeroTime() time.Time {
 
 func TestTextCaller(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf), WithCaller())
+	logger := New(&buf)
+	logger.SetReportCaller(true)
 	// We calculate the caller offset based on the caller line number.
 	_, file, line, _ := runtime.Caller(0)
 	cases := []struct {
@@ -90,7 +91,7 @@ func TestTextCaller(t *testing.T) {
 
 func TestTextLogger(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf))
+	logger := New(&buf)
 	cases := []struct {
 		name     string
 		expected string
@@ -201,8 +202,8 @@ func TestTextLogger(t *testing.T) {
 
 func TestTextHelper(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf), WithCaller())
-
+	logger := New(&buf)
+	logger.SetReportCaller(true)
 	helper := func() {
 		logger.Helper()
 		logger.Info("helper func")
@@ -216,7 +217,8 @@ func TestTextHelper(t *testing.T) {
 
 func TestTextFatal(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf), WithCaller())
+	logger := New(&buf)
+	logger.SetReportCaller(true)
 	if os.Getenv("FATAL") == "1" {
 		logger.Fatal("i'm dead")
 		return
@@ -232,7 +234,7 @@ func TestTextFatal(t *testing.T) {
 
 func TestTextValueStyles(t *testing.T) {
 	var buf bytes.Buffer
-	logger := New(WithOutput(&buf)).(*logger)
+	logger := New(&buf)
 	logger.noStyles = false
 	oldValueStyle := ValueStyle
 	defer func() { ValueStyle = oldValueStyle }()