diff --git a/logger.go b/logger.go index 8d0789e..7930982 100644 --- a/logger.go +++ b/logger.go @@ -31,12 +31,13 @@ type Logger struct { isDiscard uint32 - level int32 - prefix string - timeFunc TimeFunction - timeFormat string - callerOffset int - formatter Formatter + level int32 + prefix string + timeFunc TimeFunction + timeFormat string + callerOffset int + callerFormatter CallerFormatter + formatter Formatter reportCaller bool reportTimestamp bool @@ -71,8 +72,8 @@ func (l *Logger) log(level Level, msg interface{}, keyvals ...interface{}) { 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) + file, line, fn := l.fillLoc(l.callerOffset + 2) + caller := l.callerFormatter(file, line, fn) kvs = append(kvs, CallerKey, caller) } @@ -146,8 +147,8 @@ func location(skip int) (file string, line int, fn string) { return file, line, f.Name() } -// Cleanup a path by returning the last 2 segments of the path only. -func trimCallerPath(path string) string { +// Cleanup a path by returning the last n segments of the path only. +func trimCallerPath(path string, n int) 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, @@ -160,16 +161,23 @@ func trimCallerPath(path string) string { // // for discussion on the issue on Go side. + // Return the full path if n is 0. + if n <= 0 { + return path + } + // 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 + for i := 0; i < n-1; i++ { + // Find the penultimate separator. + idx = strings.LastIndexByte(path[:idx], '/') + if idx == -1 { + return path + } } return path[idx+1:] @@ -254,6 +262,13 @@ func (l *Logger) SetFormatter(f Formatter) { l.formatter = f } +// SetCallerFormatter sets the caller formatter. +func (l *Logger) SetCallerFormatter(f CallerFormatter) { + l.mu.Lock() + defer l.mu.Unlock() + l.callerFormatter = f +} + // With returns a new logger with the given keyvals added. func (l *Logger) With(keyvals ...interface{}) *Logger { sl := *l diff --git a/options.go b/options.go index 4828479..fc63ecc 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,7 @@ package log import ( + "fmt" "time" ) @@ -21,6 +22,20 @@ func NowUTC() time.Time { return time.Now().UTC() } +// CallerFormatter is the caller formatter. +type CallerFormatter func(string, int, string) string + +// ShortCallerFormatter is a caller formatter that returns the last 2 levels of the path +// and line number. +func ShortCallerFormatter(file string, line int, funcName string) string { + return fmt.Sprintf("%s:%d", trimCallerPath(file, 2), line) +} + +// LongCallerFormatter is a caller formatter that returns the full path and line number. +func LongCallerFormatter(file string, line int, funcName string) string { + return fmt.Sprintf("%s:%d", file, line) +} + // Options is the options for the logger. type Options struct { // TimeFunction is the time function for the logger. The default is time.Now. @@ -35,6 +50,8 @@ type Options struct { ReportTimestamp bool // ReportCaller is whether the logger should report the caller location. The default is false. ReportCaller bool + // CallerFormatter is the caller format for the logger. The default is CallerShort. + CallerFormatter CallerFormatter // 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. diff --git a/options_test.go b/options_test.go index aee9ba8..024a221 100644 --- a/options_test.go +++ b/options_test.go @@ -1,6 +1,8 @@ package log import ( + "bytes" + "fmt" "io/ioutil" "testing" @@ -22,3 +24,48 @@ func TestOptions(t *testing.T) { require.Equal(t, DefaultTimeFormat, logger.timeFormat) require.NotNil(t, logger.timeFunc) } + +func TestCallerFormatter(t *testing.T) { + var buf bytes.Buffer + l := NewWithOptions(&buf, Options{ReportCaller: true}) + file, line, fn := l.fillLoc(0) + hi := func() { l.Info("hi") } + cases := []struct { + name string + expected string + format CallerFormatter + }{ + { + name: "short caller formatter", + expected: fmt.Sprintf("INFO hi\n", line+1), + format: ShortCallerFormatter, + }, + { + name: "long caller formatter", + expected: fmt.Sprintf("INFO <%s:%d> hi\n", file, line+1), + format: LongCallerFormatter, + }, + { + name: "foo caller formatter", + expected: "INFO hi\n", + format: func(file string, line int, fn string) string { + return "foo" + }, + }, + { + name: "custom caller formatter", + expected: fmt.Sprintf("INFO <%s:%d:%s.func1> hi\n", file, line+1, fn), + format: func(file string, line int, fn string) string { + return fmt.Sprintf("%s:%d:%s", file, line, fn) + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + buf.Reset() + l.callerFormatter = c.format + hi() + require.Equal(t, c.expected, buf.String()) + }) + } +} diff --git a/pkg.go b/pkg.go index 6f80e24..9dd2d83 100644 --- a/pkg.go +++ b/pkg.go @@ -41,11 +41,16 @@ func NewWithOptions(w io.Writer, o Options) *Logger { timeFormat: o.TimeFormat, formatter: o.Formatter, fields: o.Fields, + callerFormatter: o.CallerFormatter, } l.SetOutput(w) l.SetLevel(Level(l.level)) + if l.callerFormatter == nil { + l.callerFormatter = ShortCallerFormatter + } + if l.timeFunc == nil { l.timeFunc = time.Now } @@ -97,6 +102,11 @@ func SetFormatter(f Formatter) { defaultLogger.SetFormatter(f) } +// SetCallerFormatter sets the caller formatter for the default logger. +func SetCallerFormatter(f CallerFormatter) { + defaultLogger.SetCallerFormatter(f) +} + // SetPrefix sets the prefix for the default logger. func SetPrefix(prefix string) { defaultLogger.SetPrefix(prefix)