package zerolog import ( "bytes" "encoding/json" "fmt" "io" "os" "sort" "strconv" "strings" "sync" "time" ) const ( colorBold = iota + 1 colorFaint ) const ( colorBlack = iota + 30 colorRed colorGreen colorYellow colorBlue colorMagenta colorCyan colorWhite ) var ( consoleBufPool = sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, 0, 100)) }, } consoleDefaultTimeFormat = time.Kitchen consoleDefaultFormatter = func(i interface{}) string { return fmt.Sprintf("%s", i) } consoleDefaultPartsOrder = func() []string { return []string{ TimestampFieldName, LevelFieldName, CallerFieldName, MessageFieldName, } } consoleNoColor = false consoleTimeFormat = consoleDefaultTimeFormat ) // Formatter transforms the input into a formatted string. type Formatter func(interface{}) string // ConsoleWriter parses the JSON input and writes it in an // (optionally) colorized, human-friendly format to Out. type ConsoleWriter struct { // Out is the output destination. Out io.Writer // NoColor disables the colorized output. NoColor bool // TimeFormat specifies the format for timestamp in output. TimeFormat string // PartsOrder defines the order of parts in output. PartsOrder []string FormatTimestamp Formatter FormatLevel Formatter FormatCaller Formatter FormatMessage Formatter FormatFieldName Formatter FormatFieldValue Formatter FormatErrFieldName Formatter FormatErrFieldValue Formatter } // NewConsoleWriter creates and initializes a new ConsoleWriter. func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter { w := ConsoleWriter{ Out: os.Stdout, TimeFormat: consoleDefaultTimeFormat, PartsOrder: consoleDefaultPartsOrder(), } for _, opt := range options { opt(&w) } return w } // Write transforms the JSON input with formatters and appends to w.Out. func (w ConsoleWriter) Write(p []byte) (n int, err error) { if w.PartsOrder == nil { w.PartsOrder = consoleDefaultPartsOrder() } if w.TimeFormat == "" && consoleTimeFormat != consoleDefaultTimeFormat { consoleTimeFormat = consoleDefaultTimeFormat } if w.TimeFormat != "" && consoleTimeFormat != w.TimeFormat { consoleTimeFormat = w.TimeFormat } if w.NoColor == false && consoleNoColor != false { consoleNoColor = false } if w.NoColor == true && consoleNoColor != w.NoColor { consoleNoColor = w.NoColor } var buf = consoleBufPool.Get().(*bytes.Buffer) defer consoleBufPool.Put(buf) var evt map[string]interface{} p = decodeIfBinaryToBytes(p) d := json.NewDecoder(bytes.NewReader(p)) d.UseNumber() err = d.Decode(&evt) if err != nil { return n, fmt.Errorf("cannot decode event: %s", err) } for _, p := range w.PartsOrder { w.writePart(buf, evt, p) } w.writeFields(evt, buf) buf.WriteByte('\n') buf.WriteTo(w.Out) return len(p), nil } // writeFields appends formatted key-value pairs to buf. func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) { var fields = make([]string, 0, len(evt)) for field := range evt { switch field { case LevelFieldName, TimestampFieldName, MessageFieldName, CallerFieldName: continue } fields = append(fields, field) } sort.Strings(fields) if len(fields) > 0 { buf.WriteByte(' ') } // Move the "error" field to the front ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= ErrorFieldName }) if ei < len(fields) && fields[ei] == ErrorFieldName { fields[ei] = "" fields = append([]string{ErrorFieldName}, fields...) var xfields = make([]string, 0, len(fields)) for _, field := range fields { if field == "" { // Skip empty fields continue } xfields = append(xfields, field) } fields = xfields } for i, field := range fields { var fn Formatter var fv Formatter if field == ErrorFieldName { if w.FormatErrFieldName == nil { fn = consoleDefaultFormatErrFieldName } else { fn = w.FormatErrFieldName } if w.FormatErrFieldValue == nil { fv = consoleDefaultFormatErrFieldValue } else { fv = w.FormatErrFieldValue } } else { if w.FormatFieldName == nil { fn = consoleDefaultFormatFieldName } else { fn = w.FormatFieldName } if w.FormatFieldValue == nil { fv = consoleDefaultFormatFieldValue } else { fv = w.FormatFieldValue } } buf.WriteString(fn(field)) switch fValue := evt[field].(type) { case string: if needsQuote(fValue) { buf.WriteString(fv(strconv.Quote(fValue))) } else { buf.WriteString(fv(fValue)) } case json.Number: buf.WriteString(fv(fValue)) default: b, err := json.Marshal(fValue) if err != nil { fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err) } else { fmt.Fprint(buf, fv(b)) } } if i < len(fields)-1 { // Skip space for last field buf.WriteByte(' ') } } } // writePart appends a formatted part to buf. func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) { var f Formatter switch p { case LevelFieldName: if w.FormatLevel == nil { f = consoleDefaultFormatLevel } else { f = w.FormatLevel } case TimestampFieldName: if w.FormatTimestamp == nil { f = consoleDefaultFormatTimestamp } else { f = w.FormatTimestamp } case MessageFieldName: if w.FormatMessage == nil { f = consoleDefaultFormatMessage } else { f = w.FormatMessage } case CallerFieldName: if w.FormatCaller == nil { f = consoleDefaultFormatCaller } else { f = w.FormatCaller } default: if w.FormatFieldValue == nil { f = consoleDefaultFormatFieldValue } else { f = w.FormatFieldValue } } var s = f(evt[p]) if len(s) > 0 { buf.WriteString(s) if p != w.PartsOrder[len(w.PartsOrder)-1] { // Skip space for last part buf.WriteByte(' ') } } } // needsQuote returns true when the string s should be quoted in output. func needsQuote(s string) bool { for i := range s { if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' { return true } } return false } // colorize returns the string s wrapped in ANSI code c, unless disabled is true. func colorize(s interface{}, c int, disabled bool) string { if disabled { return fmt.Sprintf("%s", s) } return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s) } // ----- DEFAULT FORMATTERS --------------------------------------------------- var ( consoleDefaultFormatTimestamp = func(i interface{}) string { t := "" switch tt := i.(type) { case string: ts, err := time.Parse(time.RFC3339, tt) if err != nil { t = tt } else { t = ts.Format(consoleTimeFormat) } case json.Number: t = tt.String() } return colorize(t, colorFaint, consoleNoColor) } consoleDefaultFormatLevel = func(i interface{}) string { var l string if ll, ok := i.(string); ok { switch ll { case "debug": l = colorize("DBG", colorYellow, consoleNoColor) case "info": l = colorize("INF", colorGreen, consoleNoColor) case "warn": l = colorize("WRN", colorRed, consoleNoColor) case "error": l = colorize(colorize("ERR", colorRed, consoleNoColor), colorBold, consoleNoColor) case "fatal": l = colorize(colorize("FTL", colorRed, consoleNoColor), colorBold, consoleNoColor) case "panic": l = colorize(colorize("PNC", colorRed, consoleNoColor), colorBold, consoleNoColor) default: l = colorize("???", colorBold, consoleNoColor) } } else { l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3] } return l } consoleDefaultFormatCaller = func(i interface{}) string { var c string if cc, ok := i.(string); ok { c = cc } if len(c) > 0 { cwd, err := os.Getwd() if err == nil { c = strings.TrimPrefix(c, cwd) c = strings.TrimPrefix(c, "/") } c = colorize(c, colorBold, consoleNoColor) + colorize(" >", colorFaint, consoleNoColor) } return c } consoleDefaultFormatMessage = func(i interface{}) string { return fmt.Sprintf("%s", i) } consoleDefaultFormatFieldName = func(i interface{}) string { return colorize(fmt.Sprintf("%s=", i), colorFaint, consoleNoColor) } consoleDefaultFormatFieldValue = func(i interface{}) string { return fmt.Sprintf("%s", i) } consoleDefaultFormatErrFieldName = func(i interface{}) string { return colorize(fmt.Sprintf("%s=", i), colorRed, consoleNoColor) } consoleDefaultFormatErrFieldValue = func(i interface{}) string { return colorize(fmt.Sprintf("%s", i), colorRed, consoleNoColor) } )