diff --git a/README.md b/README.md index c998168..0093174 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -![demo](https://github.com/mgutz/logxi/raw/master/images/demo.gif) +![demo](https://github.com/serussell/logxi/raw/master/images/demo.gif) # logxi @@ -22,12 +22,12 @@ logger built for speed and happy development. ### Installation - go get -u github.com/mgutz/logxi/v1 + go get -u github.com/serussell/logxi/v1 ### Getting Started ```go -import "github.com/mgutz/logxi/v1" +import "github.com/serussell/logxi/v1" // create package variable for Logger interface var logger log.Logger diff --git a/v1/bench/bench_test.go b/v1/bench/bench_test.go index 924650f..11079c0 100644 --- a/v1/bench/bench_test.go +++ b/v1/bench/bench_test.go @@ -7,8 +7,7 @@ import ( "testing" "github.com/Sirupsen/logrus" - "github.com/mgutz/logxi/v1" - "gopkg.in/inconshreveable/log15.v2" + "github.com/serussell/logxi/v1" ) type M map[string]interface{} diff --git a/v1/callstack.go b/v1/callstack.go index 208eb40..2b22b5c 100644 --- a/v1/callstack.go +++ b/v1/callstack.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strconv" "strings" @@ -20,6 +21,7 @@ type frameInfo struct { filename string lineno int method string + pc uintptr context []*sourceLine contextLines int } @@ -97,7 +99,7 @@ func (ci *frameInfo) String(color string, sourceColor string) string { buf.WriteString(strconv.Itoa(ci.lineno)) buf.WriteString(")") - if ci.contextLines == -1 { + if contextLines == -1 { return buf.String() } buf.WriteString("\n") @@ -118,17 +120,26 @@ func (ci *frameInfo) String(color string, sourceColor string) string { } for _, li := range ci.context { + if !disableColors { + // need to reset here. If source is set to default color, then the message + // color will bleed over into the source context lines + buf.WriteString(ansi.Reset) + } var format string format = fmt.Sprintf("%%s%%%dd: %%s\n", linenoWidth) if li.lineno == ci.lineno { - buf.WriteString(color) - if ci.contextLines > 2 { + if !disableColors { + buf.WriteString(color) + } + if contextLines > 0 { format = fmt.Sprintf("%%s=> %%%dd: %%s\n", linenoWidth) } } else { - buf.WriteString(sourceColor) - if ci.contextLines > 2 { + if !disableColors { + buf.WriteString(sourceColor) + } + if contextLines > 0 { // account for "=> " format = fmt.Sprintf("%%s%%%dd: %%s\n", linenoWidth+3) } @@ -145,117 +156,53 @@ func (ci *frameInfo) String(color string, sourceColor string) string { return buf.String() } -// parseDebugStack parases a stack created by debug.Stack() -// -// This is what the string looks like -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70) -// (*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack())) -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3) -// (*JSONFormatter).appendValue: jf.writeError(buf, err) -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca) -// (*JSONFormatter).set: jf.appendValue(buf, val) -// ... -// /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1) -// goexit: -func parseDebugStack(stack string, skip int, ignoreRuntime bool) []*frameInfo { +// Generates a stack from runtime.Callers() +func stackFrames(skip int, ignoreRuntime bool) []*frameInfo { frames := []*frameInfo{} - // BUG temporarily disable since there is a bug with embedded newlines - if true { - return frames - } - - lines := strings.Split(stack, "\n") - - for i := skip * 2; i < len(lines); i += 2 { - ci := &frameInfo{} - sourceLine := lines[i] - if sourceLine == "" { + size := 20 + pcs := make([]uintptr, size) + // always skip the first frame, since it's runtime.Callers itself + pcs = pcs[:runtime.Callers(1+skip, pcs)] + + for _, pc := range pcs { + fn := runtime.FuncForPC(pc) + name := fn.Name() + file, line := fn.FileLine(pc - 1) + if ignoreRuntime && strings.Contains(file, filepath.Join("src", "runtime")) { break } - if ignoreRuntime && strings.Contains(sourceLine, filepath.Join("src", "runtime")) { - break - } - - colon := strings.Index(sourceLine, ":") - slash := strings.Index(sourceLine, "/") - if colon < slash { - // must be on Windows where paths look like c:/foo/bar.go:lineno - colon = strings.Index(sourceLine[slash:], ":") + slash - } - space := strings.Index(sourceLine, " ") - ci.filename = sourceLine[0:colon] - // BUG with callstack where the error message has embedded newlines - // if colon > space { - // fmt.Println("lines", lines) - // } - // fmt.Println("SOURCELINE", sourceLine, "len", len(sourceLine), "COLON", colon, "SPACE", space) - numstr := sourceLine[colon+1 : space] - lineno, err := strconv.Atoi(numstr) - if err != nil { - InternalLog.Warn("Could not parse line number", "sourceLine", sourceLine, "numstr", numstr) - continue + ci := &frameInfo{ + filename: file, + lineno: line, + method: name, + pc: pc, } - ci.lineno = lineno - methodLine := lines[i+1] - colon = strings.Index(methodLine, ":") - ci.method = strings.Trim(methodLine[0:colon], "\t ") frames = append(frames, ci) } return frames } -// parseDebugStack parases a stack created by debug.Stack() -// -// This is what the string looks like -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70) -// (*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack())) -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3) -// (*JSONFormatter).appendValue: jf.writeError(buf, err) -// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca) -// (*JSONFormatter).set: jf.appendValue(buf, val) -// ... -// /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1) -// goexit: -func trimDebugStack(stack string) string { +// Returns debug stack excluding logxi frames +func trimmedStackTrace() string { buf := pool.Get() defer pool.Put(buf) - lines := strings.Split(stack, "\n") - for i := 0; i < len(lines); i += 2 { - sourceLine := lines[i] - if sourceLine == "" { - break - } - - colon := strings.Index(sourceLine, ":") - slash := strings.Index(sourceLine, "/") - if colon < slash { - // must be on Windows where paths look like c:/foo/bar.go:lineno - colon = strings.Index(sourceLine[slash:], ":") + slash - } - filename := sourceLine[0:colon] + frames := stackFrames(0, false) + for _, frame := range frames { // skip anything in the logxi package - if isLogxiCode(filename) { + if isLogxiCode(frame.filename) { continue } - buf.WriteString(sourceLine) - buf.WriteRune('\n') - buf.WriteString(lines[i+1]) - buf.WriteRune('\n') - } - return buf.String() -} -func parseLogxiStack(entry map[string]interface{}, skip int, ignoreRuntime bool) []*frameInfo { - kv := entry[KeyMap.CallStack] - if kv == nil { - return nil - } + fmt.Fprintf(buf, "%s:%d (0x%x)\n", frame.filename, frame.lineno, frame.pc) + + err := frame.readSource(0) + if err != nil || len(frame.context) < 1 { + continue + } - var frames []*frameInfo - if stack, ok := kv.(string); ok { - frames = parseDebugStack(stack, skip, ignoreRuntime) + fmt.Fprintf(buf, "\t%s: %s\n", frame.method, frame.context[0].line) } - return frames + return buf.String() } diff --git a/v1/cmd/demo/main.go b/v1/cmd/demo/main.go index 82ab062..4d3a4e1 100644 --- a/v1/cmd/demo/main.go +++ b/v1/cmd/demo/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/mgutz/logxi/v1" + "github.com/serussell/logxi/v1" ) var errConfig = fmt.Errorf("file not found") diff --git a/v1/cmd/filter/main.go b/v1/cmd/filter/main.go index 33492ed..ca7e464 100644 --- a/v1/cmd/filter/main.go +++ b/v1/cmd/filter/main.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/mgutz/logxi/v1" + "github.com/serussell/logxi/v1" ) func sendExternal(obj map[string]interface{}) { diff --git a/v1/cmd/reldir/foo.go b/v1/cmd/reldir/foo.go index 7729f2f..3a19553 100644 --- a/v1/cmd/reldir/foo.go +++ b/v1/cmd/reldir/foo.go @@ -1,6 +1,6 @@ package reldir -import "github.com/mgutz/logxi/v1" +import "github.com/serussell/logxi/v1" // Foo returns error func Foo() { diff --git a/v1/defaultLogger.go b/v1/defaultLogger.go index 40fb513..38bd8cb 100644 --- a/v1/defaultLogger.go +++ b/v1/defaultLogger.go @@ -52,7 +52,7 @@ func NewLogger3(writer io.Writer, name string, formatter Formatter) Logger { // New creates a colorable default logger. func New(name string) Logger { - return NewLogger(colorableStdout, name) + return NewLogger(colorableStderr, name) } // Trace logs a debug entry. diff --git a/v1/formatter.go b/v1/formatter.go index 9357394..4863429 100644 --- a/v1/formatter.go +++ b/v1/formatter.go @@ -45,6 +45,8 @@ func formatFactory(name string, kind string) (Formatter, error) { formatter = NewTextFormatter(name) case FormatJSON: formatter = NewJSONFormatter(name) + case FormatRFC5424: + formatter = NewRFC5424Formatter(name) } return formatter, err } diff --git a/v1/happyDevFormatter.go b/v1/happyDevFormatter.go index 3931b36..8bb3160 100644 --- a/v1/happyDevFormatter.go +++ b/v1/happyDevFormatter.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "runtime/debug" "strings" "github.com/mgutz/ansi" @@ -111,7 +110,6 @@ func parseTheme(theme string) *colorScheme { // works fine in SSH terminals and binary deployments. type HappyDevFormatter struct { name string - col int // always use the production formatter jsonFormatter *JSONFormatter } @@ -125,21 +123,21 @@ func NewHappyDevFormatter(name string) *HappyDevFormatter { } } -func (hd *HappyDevFormatter) writeKey(buf bufferWriter, key string) { +func (hd *HappyDevFormatter) writeKey(buf bufferWriter, key string, col *int) { // assumes this is not the first key - hd.writeString(buf, Separator) + hd.writeString(buf, Separator, col) if key == "" { return } buf.WriteString(theme.Key) - hd.writeString(buf, key) - hd.writeString(buf, AssignmentChar) + hd.writeString(buf, key, col) + hd.writeString(buf, AssignmentChar, col) if !disableColors { buf.WriteString(ansi.Reset) } } -func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{}, color string) { +func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{}, color string, col *int) { var str string if s, ok := value.(string); ok { str = s @@ -149,16 +147,16 @@ func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{} str = fmt.Sprintf("%v", value) } val := strings.Trim(str, "\n ") - if (isPretty && key != "") || hd.col+len(key)+2+len(val) >= maxCol { + if (isPretty && key != "") || *col+len(key)+2+len(val) >= maxCol { buf.WriteString("\n") - hd.col = 0 - hd.writeString(buf, indent) + *col = 0 + hd.writeString(buf, indent, col) } - hd.writeKey(buf, key) + hd.writeKey(buf, key, col) if color != "" { buf.WriteString(color) } - hd.writeString(buf, val) + hd.writeString(buf, val, col) if color != "" && !disableColors { buf.WriteString(ansi.Reset) } @@ -166,16 +164,16 @@ func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{} // Write a string and tracks the position of the string so we can break lines // cleanly. Do not send ANSI escape sequences, just raw strings -func (hd *HappyDevFormatter) writeString(buf bufferWriter, s string) { +func (hd *HappyDevFormatter) writeString(buf bufferWriter, s string, col *int) { buf.WriteString(s) - hd.col += len(s) + *col += len(s) } func (hd *HappyDevFormatter) getContext(color string) string { if disableCallstack { return "" } - frames := parseDebugStack(string(debug.Stack()), 5, true) + frames := stackFrames(7, true) if len(frames) == 0 { return "" } @@ -220,17 +218,15 @@ func (hd *HappyDevFormatter) getLevelContext(level int, entry map[string]interfa } if disableCallstack || contextLines == -1 { - context = trimDebugStack(string(debug.Stack())) + context = trimmedStackTrace() break } - frames := parseLogxiStack(entry, 4, true) - if frames == nil { - frames = parseDebugStack(string(debug.Stack()), 4, true) - } + frames := stackFrames(6, true) if len(frames) == 0 { break } + errbuf := pool.Get() defer pool.Put(errbuf) lines := 0 @@ -294,11 +290,11 @@ func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, arg entry := hd.jsonFormatter.LogEntry(level, msg, args) // reset the column tracker used for fancy formatting - hd.col = 0 + col := 0 // timestamp buf.WriteString(theme.Misc) - hd.writeString(buf, entry[KeyMap.Time].(string)) + hd.writeString(buf, entry[KeyMap.Time].(string), &col) if !disableColors { buf.WriteString(ansi.Reset) } @@ -310,11 +306,11 @@ func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, arg } // DBG, INF ... - hd.set(buf, "", entry[KeyMap.Level].(string), color) + hd.set(buf, "", entry[KeyMap.Level].(string), color, &col) // logger name - hd.set(buf, "", entry[KeyMap.Name], theme.Misc) + hd.set(buf, "", entry[KeyMap.Name], theme.Misc, &col) // message from user - hd.set(buf, "", message, theme.Message) + hd.set(buf, "", message, theme.Message, &col) // Preserve key order in the sequencethey were added by developer.This // makes it easier for developers to follow the log. @@ -339,7 +335,7 @@ func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, arg } else if isReserved { continue } - hd.set(buf, key, entry[key], theme.Value) + hd.set(buf, key, entry[key], theme.Value, &col) } addLF := true @@ -351,7 +347,7 @@ func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, arg if level == LevelTrace || (level == LevelWarn && !hasCallStack) { // gets rid of "in " idx := strings.IndexRune(context, 'n') - hd.set(buf, "in", context[idx+2:], color) + hd.set(buf, "in", context[idx+2:], theme.Value, &col) } else { buf.WriteRune('\n') if !disableColors { @@ -364,7 +360,7 @@ func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, arg } } } else if hasCallStack { - hd.set(buf, "", entry[KeyMap.CallStack], color) + hd.set(buf, "", entry[KeyMap.CallStack], color, &col) } if addLF { buf.WriteRune('\n') diff --git a/v1/init.go b/v1/init.go index 57c9140..fea7c37 100644 --- a/v1/init.go +++ b/v1/init.go @@ -68,7 +68,7 @@ var logxiNameLevelMap map[string]int // logxiFormat is the formatter kind to create var logxiFormat string -var colorableStdout io.Writer +var colorableStderr io.Writer var defaultContextLines = 2 var defaultFormat string var defaultLevel int @@ -144,7 +144,7 @@ func setDefaults(isTerminal bool) { if os.Getenv("ConEmuANSI") == "ON" { defaultLogxiColorsEnv = "key=cyan+h,value,misc=blue+h,source=yellow,TRC,DBG,WRN=yellow+h,INF=green+h,ERR=red+h" } else { - colorableStdout = NewConcurrentWriter(colorable.NewColorableStdout()) + colorableStderr = NewConcurrentWriter(colorable.NewColorableStderr()) defaultLogxiColorsEnv = "ERR=red,misc=cyan,key=cyan" } // DefaultScheme is a color scheme optimized for dark background @@ -176,15 +176,15 @@ func isReservedKey(k interface{}) (bool, error) { } func init() { - colorableStdout = NewConcurrentWriter(os.Stdout) + colorableStderr = NewConcurrentWriter(os.Stderr) - isTerminal = isatty.IsTerminal(os.Stdout.Fd()) + isTerminal = isatty.IsTerminal(os.Stderr.Fd()) // the internal logger to report errors if isTerminal { - InternalLog = NewLogger3(NewConcurrentWriter(os.Stdout), "__logxi", NewTextFormatter("__logxi")) + InternalLog = NewLogger3(NewConcurrentWriter(os.Stderr), "__logxi", NewTextFormatter("__logxi")) } else { - InternalLog = NewLogger3(NewConcurrentWriter(os.Stdout), "__logxi", NewJSONFormatter("__logxi")) + InternalLog = NewLogger3(NewConcurrentWriter(os.Stderr), "__logxi", NewJSONFormatter("__logxi")) } InternalLog.SetLevel(LevelError) @@ -193,6 +193,7 @@ func init() { RegisterFormatFactory(FormatHappy, formatFactory) RegisterFormatFactory(FormatText, formatFactory) RegisterFormatFactory(FormatJSON, formatFactory) + RegisterFormatFactory(FormatRFC5424, formatFactory) ProcessEnv(readFromEnviron()) // package logger for users diff --git a/v1/logger.go b/v1/logger.go index 113a38a..98ac0a2 100644 --- a/v1/logger.go +++ b/v1/logger.go @@ -100,6 +100,8 @@ const FormatText = "text" // FormatJSON uses JSONFormatter const FormatJSON = "JSON" +const FormatRFC5424 = "RFC5424" + // FormatEnv selects formatter based on LOGXI_FORMAT environment variable const FormatEnv = "" diff --git a/v1/rfc5424Formatter.go b/v1/rfc5424Formatter.go new file mode 100644 index 0000000..136832a --- /dev/null +++ b/v1/rfc5424Formatter.go @@ -0,0 +1,96 @@ +package log + +import ( + "fmt" + "io" + "os" + "runtime/debug" + "strconv" + "strings" + "time" +) + +// RFC5424Formatter is the default recorder used if one is unspecified when +// creating a new Logger. +type RFC5424Formatter struct { + name string +} + +// NewRFC5424Formatter returns a new instance of RFC5424Formatter. SetName +// must be called befored using it. +func NewRFC5424Formatter(name string) *RFC5424Formatter { + return &RFC5424Formatter{name: name} +} + +func (tf *RFC5424Formatter) set(buf bufferWriter, key string, val interface{}) { + buf.WriteString(" ") + buf.WriteString(key) + buf.WriteString("=") + if err, ok := val.(error); ok { + buf.WriteString(err.Error()) + buf.WriteString(strings.Replace(string(debug.Stack()), "\n", " | ", -1)) + return + } + buf.WriteString(fmt.Sprintf("\"%v\"", val)) +} + +// Format records a log entry. +func (tf *RFC5424Formatter) Format(writer io.Writer, level int, msg string, args []interface{}) { + buf := pool.Get() + defer pool.Put(buf) + + // VERSION TIMESTAMP HOSTNAME APP-NAME PROC-ID MSGID MSG" + + // Prival = Facility + Priority + // Facility is "user", so "1", x 8. Therefore, 8 + // Priority is the same as level, except Emergency is 0 not -1, and there is + // no Trace or All so these are converted to DEBUG + priority := level + if level < 0 { + priority = 0 + } else if level > 7 { + priority = 7 + } + prival := 8 + priority + + buf.WriteString("<" + strconv.Itoa(prival) + ">1 ") + buf.WriteString(time.Now().Format(time.RFC3339)) + buf.WriteString(" ") + hostname, _ := os.Hostname() + buf.WriteString(hostname) + buf.WriteString(" ") + buf.WriteString(tf.name) + buf.WriteString(" ") + buf.WriteString(pidStr) + buf.WriteString(" ") + buf.WriteString(LevelMap[level]) + buf.WriteString(" ") + + var lenArgs = len(args) + if lenArgs > 0 { + buf.WriteString("[SDID@1") + if lenArgs == 1 { + tf.set(buf, singleArgKey, args[0]) + } else if lenArgs%2 == 0 { + for i := 0; i < lenArgs; i += 2 { + if key, ok := args[i].(string); ok { + if key == "" { + // show key is invalid + tf.set(buf, badKeyAtIndex(i), args[i+1]) + } else { + tf.set(buf, key, args[i+1]) + } + } else { + // show key is invalid + tf.set(buf, badKeyAtIndex(i), args[i+1]) + } + } + } else { + tf.set(buf, warnImbalancedKey, args) + } + buf.WriteString("] ") + } + buf.WriteString(msg) + buf.WriteRune('\n') + buf.WriteTo(writer) +} diff --git a/v1/util.go b/v1/util.go index 22f3130..3db1786 100644 --- a/v1/util.go +++ b/v1/util.go @@ -44,7 +44,7 @@ func indexOfNonSpace(s string) int { return -1 } -var inLogxiPath = filepath.Join("mgutz", "logxi", "v"+strings.Split(Version, ".")[0]) +var inLogxiPath = filepath.Join("serussell", "logxi", "v"+strings.Split(Version, ".")[0]) func isLogxiCode(filename string) bool { // need to see errors in tests