package log import ( "encoding/json" "fmt" "io" "runtime/debug" "strings" "github.com/mgutz/ansi" ) // colorScheme defines a color theme for HappyDevFormatter type colorScheme struct { Key string Message string Value string Misc string Source string Trace string Debug string Info string Warn string Error string } var indent = " " var maxCol = defaultMaxCol var theme *colorScheme func parseKVList(s, separator string) map[string]string { pairs := strings.Split(s, separator) if len(pairs) == 0 { return nil } m := map[string]string{} for _, pair := range pairs { if pair == "" { continue } parts := strings.Split(pair, "=") switch len(parts) { case 1: m[parts[0]] = "" case 2: m[parts[0]] = parts[1] } } return m } func parseTheme(theme string) *colorScheme { m := parseKVList(theme, ",") cs := &colorScheme{} var wildcard string var color = func(key string) string { if disableColors { return "" } style := m[key] c := ansi.ColorCode(style) if c == "" { c = wildcard } //fmt.Printf("plain=%b [%s] %s=%q\n", ansi.DefaultFG, key, style, c) return c } wildcard = color("*") if wildcard != ansi.Reset { cs.Key = wildcard cs.Value = wildcard cs.Misc = wildcard cs.Source = wildcard cs.Message = wildcard cs.Trace = wildcard cs.Debug = wildcard cs.Warn = wildcard cs.Info = wildcard cs.Error = wildcard } cs.Key = color("key") cs.Value = color("value") cs.Misc = color("misc") cs.Source = color("source") cs.Message = color("message") cs.Trace = color("TRC") cs.Debug = color("DBG") cs.Warn = color("WRN") cs.Info = color("INF") cs.Error = color("ERR") return cs } // HappyDevFormatter is the formatter used for terminals. It is // colorful, dev friendly and provides meaningful logs when // warnings and errors occur. // // HappyDevFormatter does not worry about performance. It's at least 3-4X // slower than JSONFormatter since it delegates to JSONFormatter to marshal // then unmarshal JSON. Then it does other stuff like read source files, sort // keys all to give a developer more information. // // SHOULD NOT be used in production for extended period of time. However, it // works fine in SSH terminals and binary deployments. type HappyDevFormatter struct { name string col int // always use the production formatter jsonFormatter *JSONFormatter } // NewHappyDevFormatter returns a new instance of HappyDevFormatter. func NewHappyDevFormatter(name string) *HappyDevFormatter { jf := NewJSONFormatter(name) return &HappyDevFormatter{ name: name, jsonFormatter: jf, } } func (hd *HappyDevFormatter) writeKey(buf bufferWriter, key string) { // assumes this is not the first key hd.writeString(buf, Separator) if key == "" { return } buf.WriteString(theme.Key) hd.writeString(buf, key) hd.writeString(buf, AssignmentChar) if !disableColors { buf.WriteString(ansi.Reset) } } func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{}, color string) { var str string if s, ok := value.(string); ok { str = s } else if s, ok := value.(fmt.Stringer); ok { str = s.String() } else { str = fmt.Sprintf("%v", value) } val := strings.Trim(str, "\n ") if (isPretty && key != "") || hd.col+len(key)+2+len(val) >= maxCol { buf.WriteString("\n") hd.col = 0 hd.writeString(buf, indent) } hd.writeKey(buf, key) if color != "" { buf.WriteString(color) } hd.writeString(buf, val) if color != "" && !disableColors { buf.WriteString(ansi.Reset) } } // 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) { buf.WriteString(s) hd.col += len(s) } func (hd *HappyDevFormatter) getContext(color string) string { if disableCallstack { return "" } frames := parseDebugStack(string(debug.Stack()), 5, true) if len(frames) == 0 { return "" } for _, frame := range frames { context := frame.String(color, theme.Source) if context != "" { return context } } return "" } func (hd *HappyDevFormatter) getLevelContext(level int, entry map[string]interface{}) (message string, context string, color string) { switch level { case LevelTrace: color = theme.Trace context = hd.getContext(color) context += "\n" case LevelDebug: color = theme.Debug case LevelInfo: color = theme.Info // case LevelWarn: // color = theme.Warn // context = hd.getContext(color) // context += "\n" case LevelWarn, LevelError, LevelFatal: // warnings return an error but if it does not have an error // then print line info only if level == LevelWarn { color = theme.Warn kv := entry[KeyMap.CallStack] if kv == nil { context = hd.getContext(color) context += "\n" break } } else { color = theme.Error } if disableCallstack || contextLines == -1 { context = trimDebugStack(string(debug.Stack())) break } frames := parseLogxiStack(entry, 4, true) if frames == nil { frames = parseDebugStack(string(debug.Stack()), 4, true) } if len(frames) == 0 { break } errbuf := pool.Get() defer pool.Put(errbuf) lines := 0 for _, frame := range frames { err := frame.readSource(contextLines) if err != nil { // by setting to empty, the original stack is used errbuf.Reset() break } ctx := frame.String(color, theme.Source) if ctx == "" { continue } errbuf.WriteString(ctx) errbuf.WriteRune('\n') lines++ } context = errbuf.String() default: panic("should never get here") } return message, context, color } // Format a log entry. func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, args []interface{}) { buf := pool.Get() defer pool.Put(buf) if len(args) == 1 { args = append(args, 0) copy(args[1:], args[0:]) args[0] = singleArgKey } // warn about reserved, bad and complex keys for i := 0; i < len(args); i += 2 { isReserved, err := isReservedKey(args[i]) if err != nil { InternalLog.Error("Key is not a string.", "err", fmt.Errorf("args[%d]=%v", i, args[i])) } else if isReserved { InternalLog.Fatal("Key conflicts with reserved key. Avoiding using single rune keys.", "key", args[i].(string)) } else { // Ensure keys are simple strings. The JSONFormatter doesn't escape // keys as a performance tradeoff. This panics if the JSON key // value has a different value than a simple quoted string. key := args[i].(string) b, err := json.Marshal(key) if err != nil { panic("Key is invalid. " + err.Error()) } if string(b) != `"`+key+`"` { panic("Key is complex. Use simpler key for: " + fmt.Sprintf("%q", key)) } } } // use the production JSON formatter to format the log first. This // ensures JSON will marshal/unmarshal correctly in production. entry := hd.jsonFormatter.LogEntry(level, msg, args) // reset the column tracker used for fancy formatting hd.col = 0 // timestamp buf.WriteString(theme.Misc) hd.writeString(buf, entry[KeyMap.Time].(string)) if !disableColors { buf.WriteString(ansi.Reset) } // emphasize warnings and errors message, context, color := hd.getLevelContext(level, entry) if message == "" { message = entry[KeyMap.Message].(string) } // DBG, INF ... hd.set(buf, "", entry[KeyMap.Level].(string), color) // logger name hd.set(buf, "", entry[KeyMap.Name], theme.Misc) // message from user hd.set(buf, "", message, theme.Message) // Preserve key order in the sequencethey were added by developer.This // makes it easier for developers to follow the log. order := []string{} lenArgs := len(args) for i := 0; i < len(args); i += 2 { if i+1 >= lenArgs { continue } if key, ok := args[i].(string); ok { order = append(order, key) } else { order = append(order, badKeyAtIndex(i)) } } for _, key := range order { // skip reserved keys which were already added to buffer above isReserved, err := isReservedKey(key) if err != nil { panic("key is invalid. Should never get here. " + err.Error()) } else if isReserved { continue } hd.set(buf, key, entry[key], theme.Value) } addLF := true hasCallStack := entry[KeyMap.CallStack] != nil // WRN,ERR file, line number context if context != "" { // warnings and traces are single line, space can be optimized if level == LevelTrace || (level == LevelWarn && !hasCallStack) { // gets rid of "in " idx := strings.IndexRune(context, 'n') hd.set(buf, "in", context[idx+2:], color) } else { buf.WriteRune('\n') if !disableColors { buf.WriteString(color) } addLF = context[len(context)-1:len(context)] != "\n" buf.WriteString(context) if !disableColors { buf.WriteString(ansi.Reset) } } } else if hasCallStack { hd.set(buf, "", entry[KeyMap.CallStack], color) } if addLF { buf.WriteRune('\n') } buf.WriteTo(writer) }