blob: 3931b3691cffdbea853fe91a7549edc45e60bb37 [file] [log] [blame]
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)
}