-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
prettyconsole.go
131 lines (113 loc) · 3.28 KB
/
prettyconsole.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package logger
import (
"fmt"
"math"
"net/url"
"os"
"sort"
"strings"
"time"
"github.com/fatih/color"
"github.com/tidwall/gjson"
"go.uber.org/zap"
)
var levelColors = map[string]func(...interface{}) string{
"default": color.New(color.FgWhite).SprintFunc(),
"debug": color.New(color.FgGreen).SprintFunc(),
"info": color.New(color.FgWhite).SprintFunc(),
"warn": color.New(color.FgYellow).SprintFunc(),
"error": color.New(color.FgRed).SprintFunc(),
"panic": color.New(color.FgHiRed).SprintFunc(),
"crit": color.New(color.FgHiRed).SprintFunc(),
"fatal": color.New(color.FgHiRed).SprintFunc(),
}
var blue = color.New(color.FgBlue).SprintFunc()
var green = color.New(color.FgGreen).SprintFunc()
// PrettyConsole wraps a Sink (Writer, Syncer, Closer), usually stdout, and
// formats the incoming json bytes with colors and white space for readability
// before passing on to the underlying Writer in Sink.
type PrettyConsole struct {
zap.Sink
}
// Write reformats the incoming json bytes with colors, newlines and whitespace
// for better readability in console.
func (pc PrettyConsole) Write(b []byte) (int, error) {
if !gjson.ValidBytes(b) {
return 0, fmt.Errorf("unable to parse json for pretty console: %s", string(b))
}
js := gjson.ParseBytes(b)
headline := generateHeadline(js)
details := generateDetails(js)
return pc.Sink.Write([]byte(fmt.Sprintln(headline, details)))
}
// Close is overridden to prevent accidental closure of stderr/stdout
func (pc PrettyConsole) Close() error {
switch pc.Sink {
case os.Stderr, os.Stdout:
// Never close Stderr/Stdout because this will break any future go runtime logging from panics etc
return nil
default:
return pc.Sink.Close()
}
}
func generateHeadline(js gjson.Result) string {
ts := js.Get("ts")
var tsStr string
if f := ts.Float(); f > 1 {
sec, dec := math.Modf(f)
tsStr = iso8601UTC(time.Unix(int64(sec), int64(dec*(1e9))))
} else {
// assume already formatted
tsStr = ts.Str
}
headline := []interface{}{
tsStr,
" ",
coloredLevel(js.Get("level")),
fmt.Sprintf("%-50s", js.Get("msg")),
" ",
fmt.Sprintf("%-32s", blue(js.Get("caller"))),
}
return fmt.Sprint(headline...)
}
// detailsBlacklist of keys to show in details. This does not
// exclude it from being present in other logger sinks, like .jsonl files.
var detailsBlacklist = map[string]bool{
"level": true,
"ts": true,
"msg": true,
"caller": true,
"hash": true,
}
func generateDetails(js gjson.Result) string {
data := js.Map()
keys := []string{}
for k := range data {
if detailsBlacklist[k] || len(data[k].String()) == 0 {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var details strings.Builder
for _, v := range keys {
details.WriteString(fmt.Sprintf("%s=%v ", green(v), data[v]))
}
return details.String()
}
func coloredLevel(level gjson.Result) string {
color, ok := levelColors[level.String()]
if !ok {
color = levelColors["default"]
}
return color(fmt.Sprintf("%-8s", fmt.Sprint("[", strings.ToUpper(level.String()), "]")))
}
// iso8601UTC formats given time to ISO8601.
func iso8601UTC(t time.Time) string {
return t.UTC().Format(time.RFC3339)
}
func prettyConsoleSink(s zap.Sink) func(*url.URL) (zap.Sink, error) {
return func(*url.URL) (zap.Sink, error) {
return PrettyConsole{s}, nil
}
}