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 }()