374 lines
8.8 KiB
Go
374 lines
8.8 KiB
Go
|
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)
|
||
|
}
|