add NewColorLogger
This colors the whole record, based on given color function.
The colorizer code uses a new Hijack method of the underlying Logger.
Tamás Gulácsi
8 years ago
73 | 73 | s = err.Error() |
74 | 74 | return |
75 | 75 | } |
76 | ||
77 | func (l *jsonLogger) Hijack(f func(io.Writer) io.Writer) { | |
78 | l.Writer = f(l.Writer) | |
79 | } |
5 | 5 | |
6 | 6 | import ( |
7 | 7 | "errors" |
8 | "io" | |
8 | 9 | "sync/atomic" |
9 | 10 | ) |
10 | 11 | |
15 | 16 | // modifies any of its elements must make a copy first. |
16 | 17 | type Logger interface { |
17 | 18 | Log(keyvals ...interface{}) error |
19 | } | |
20 | ||
21 | // Hijacker allows accessing the Logger's underlying io.Writer with Hijack. | |
22 | type Hijacker interface { | |
23 | // Hijack gets the Logger's underlying Writer, and replaces it with the returned one. | |
24 | Hijack(func(io.Writer) io.Writer) | |
18 | 25 | } |
19 | 26 | |
20 | 27 | // ErrMissingValue is appended to keyvals slices with odd length to substitute |
31 | 31 | } |
32 | 32 | return nil |
33 | 33 | } |
34 | ||
35 | func (l *logfmtLogger) Hijack(f func(io.Writer) io.Writer) { | |
36 | l.w = f(l.w) | |
37 | } |
0 | package term | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "io" | |
5 | "sync" | |
6 | ||
7 | "github.com/go-kit/kit/log" | |
8 | ) | |
9 | ||
10 | // Color is the abstract color, the zero value is the Default. | |
11 | type Color uint8 | |
12 | ||
13 | const ( | |
14 | NoColor = Color(iota) | |
15 | Red | |
16 | Green | |
17 | Yellow | |
18 | Blue | |
19 | Magenta | |
20 | Cyan | |
21 | White | |
22 | Default | |
23 | ) | |
24 | ||
25 | type FgBgColor struct { | |
26 | Fg, Bg Color | |
27 | } | |
28 | ||
29 | func (c FgBgColor) IsZero() bool { | |
30 | return c.Fg == NoColor && c.Bg == NoColor | |
31 | } | |
32 | ||
33 | type ColorOption struct { | |
34 | Key FgBgColor | |
35 | Value func(interface{}) FgBgColor | |
36 | } | |
37 | ||
38 | var _ = log.Logger((*colorLogger)(nil)) | |
39 | ||
40 | // NewColorLogger returns a log.Logger which prouces nice colored logs. | |
41 | // It colors whole records based on the FgBgColor returned by the color function. | |
42 | // | |
43 | // For example for such a function, see LevelColor. | |
44 | // | |
45 | // Example for coloring errors with red: | |
46 | // | |
47 | // logger := term.NewColorLogger(log.NewLogfmtLogger(os.Stdout), | |
48 | // func(keyvals ...interface) term.FgBgColor { | |
49 | // for i := 1; i < len(keyvals); i += 2 { | |
50 | // if keyvals[i] != nil { | |
51 | // continue | |
52 | // } | |
53 | // if _, ok := keyvals[i].(error) { | |
54 | // return term.FgBgColor{Fg: term.White, Bg: term.Red} | |
55 | // } | |
56 | // } | |
57 | // return term.FgBgColor{} | |
58 | // }) | |
59 | // | |
60 | // logger.Log("c", "c is uncolored value", "err", nil) | |
61 | // logger.Log("c", "c is colored 'cause err colors it", "err", errors.New("coloring error")) | |
62 | func NewColorLogger(logger log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger { | |
63 | cl := &colorLogger{ | |
64 | logger: logger, | |
65 | } | |
66 | if hj, ok := logger.(log.Hijacker); ok { | |
67 | cl.color = color // otherwise, no coloring is possible! | |
68 | hj.Hijack(func(w io.Writer) io.Writer { | |
69 | cl.w = w | |
70 | return cl | |
71 | }) | |
72 | } | |
73 | return cl | |
74 | } | |
75 | ||
76 | type colorLogger struct { | |
77 | logger log.Logger | |
78 | color func(keyvals ...interface{}) FgBgColor | |
79 | w io.Writer | |
80 | ||
81 | actColor FgBgColor | |
82 | actColorMu sync.RWMutex // protects actColor | |
83 | } | |
84 | ||
85 | func (l *colorLogger) Log(keyvals ...interface{}) error { | |
86 | l.actColorMu.Lock() // Unlock is in Write! | |
87 | if l.color != nil { | |
88 | l.actColor = l.color(keyvals...) | |
89 | } | |
90 | l.actColorMu.Unlock() | |
91 | return l.logger.Log(keyvals...) | |
92 | } | |
93 | ||
94 | func (l *colorLogger) Write(p []byte) (int, error) { | |
95 | l.actColorMu.RLock() | |
96 | color := l.actColor | |
97 | l.actColorMu.RUnlock() | |
98 | if color.IsZero() { | |
99 | return l.w.Write(p) | |
100 | } | |
101 | var n int | |
102 | if color.Fg != NoColor { | |
103 | m, err := fmt.Fprintf(l.w, "\x1b[%dm", 30+color.Fg) | |
104 | if err != nil { | |
105 | return m, err | |
106 | } | |
107 | n += m | |
108 | } | |
109 | if color.Bg != NoColor { | |
110 | m, err := fmt.Fprintf(l.w, "\x1b[%dm", 40+color.Bg) | |
111 | if err != nil { | |
112 | return n + m, err | |
113 | } | |
114 | n += m | |
115 | } | |
116 | m, err := l.w.Write(p) | |
117 | if err != nil { | |
118 | return n + m, err | |
119 | } | |
120 | n += m | |
121 | m, err = l.w.Write([]byte("\x1b[0m")) | |
122 | return n + m, err | |
123 | } | |
124 | ||
125 | func (l *colorLogger) Hijack(f func(io.Writer) io.Writer) { | |
126 | l.w = f(l.w) | |
127 | } | |
128 | ||
129 | type ColoredValue struct { | |
130 | FgBgColor | |
131 | Value interface{} | |
132 | } | |
133 | ||
134 | func (cv ColoredValue) String() string { | |
135 | // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html | |
136 | if cv.Fg == 0 && cv.Bg == 0 { | |
137 | return fmt.Sprintf("%v", cv.Value) | |
138 | } | |
139 | if cv.Bg == 0 { | |
140 | return fmt.Sprintf("\x1b[%dm%v\x1b[0m", 30+cv.Fg, cv.Value) | |
141 | } | |
142 | if cv.Fg == 0 { | |
143 | return fmt.Sprintf("\x1b[%dm%v\x1b[0m", 40+cv.Bg, cv.Value) | |
144 | } | |
145 | return fmt.Sprintf("\x1b[%dm\x1b[%dm%v\x1b[0m", 30+cv.Fg, 40+cv.Bg, cv.Value) | |
146 | } | |
147 | ||
148 | func AsString(v interface{}) string { | |
149 | switch x := v.(type) { | |
150 | case string: | |
151 | return x | |
152 | case fmt.Stringer: | |
153 | return x.String() | |
154 | case fmt.Formatter: | |
155 | return fmt.Sprint(x) | |
156 | default: | |
157 | return fmt.Sprintf("%v", x) | |
158 | } | |
159 | } |
0 | package term_test | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "errors" | |
5 | "io" | |
6 | "io/ioutil" | |
7 | "strconv" | |
8 | "sync" | |
9 | "testing" | |
10 | ||
11 | "github.com/go-kit/kit/log" | |
12 | "github.com/go-kit/kit/log/term" | |
13 | "gopkg.in/logfmt.v0" | |
14 | ) | |
15 | ||
16 | type mymap map[int]int | |
17 | ||
18 | func (m mymap) String() string { return "special_behavior" } | |
19 | ||
20 | func TestColorLogger(t *testing.T) { | |
21 | var buf bytes.Buffer | |
22 | logger := newColorLogger(t, &buf) | |
23 | ||
24 | if err := logger.Log("hello", "world"); err != nil { | |
25 | t.Fatal(err) | |
26 | } | |
27 | if want, have := "hello=world\n", buf.String(); want != have { | |
28 | t.Errorf("want %#v, have %#v", want, have) | |
29 | } | |
30 | ||
31 | buf.Reset() | |
32 | if err := logger.Log("a", 1, "err", errors.New("error")); err != nil { | |
33 | t.Fatal(err) | |
34 | } | |
35 | if want, have := "\u001b[32m\u001b[48ma=1 err=error\n\u001b[0m", buf.String(); want != have { | |
36 | t.Errorf("want %#v, have %#v", want, have) | |
37 | } | |
38 | ||
39 | buf.Reset() | |
40 | if err := logger.Log("std_map", map[int]int{1: 2}, "my_map", mymap{0: 0}); err != nil { | |
41 | t.Fatal(err) | |
42 | } | |
43 | if want, have := "std_map=\""+logfmt.ErrUnsupportedValueType.Error()+"\" my_map=special_behavior\n", buf.String(); want != have { | |
44 | t.Errorf("want %#v, have %#v", want, have) | |
45 | } | |
46 | } | |
47 | ||
48 | func newColorLogger(t testing.TB, w io.Writer) log.Logger { | |
49 | return term.NewColorLogger(log.NewLogfmtLogger(w), | |
50 | func(keyvals ...interface{}) term.FgBgColor { | |
51 | for i := 0; i < len(keyvals); i += 2 { | |
52 | key := term.AsString(keyvals[i]) | |
53 | if key == "a" { | |
54 | return term.FgBgColor{Fg: term.Green, Bg: term.Default} | |
55 | } | |
56 | if key == "err" && keyvals[i+1] != nil { | |
57 | return term.FgBgColor{Fg: term.White, Bg: term.Red} | |
58 | } | |
59 | } | |
60 | return term.FgBgColor{} | |
61 | }) | |
62 | } | |
63 | ||
64 | func BenchmarkColorLoggerSimple(b *testing.B) { | |
65 | benchmarkRunner(b, newColorLogger(b, ioutil.Discard), baseMessage) | |
66 | } | |
67 | ||
68 | func BenchmarkColorLoggerContextual(b *testing.B) { | |
69 | benchmarkRunner(b, newColorLogger(b, ioutil.Discard), withMessage) | |
70 | } | |
71 | ||
72 | func TestColorLoggerConcurrency(t *testing.T) { | |
73 | testConcurrency(t, newColorLogger(t, ioutil.Discard)) | |
74 | } | |
75 | ||
76 | // copied from log/benchmark_test.go | |
77 | func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) { | |
78 | lc := log.NewContext(logger).With("common_key", "common_value") | |
79 | b.ReportAllocs() | |
80 | b.ResetTimer() | |
81 | for i := 0; i < b.N; i++ { | |
82 | f(lc) | |
83 | } | |
84 | } | |
85 | ||
86 | var ( | |
87 | baseMessage = func(logger log.Logger) { logger.Log("foo_key", "foo_value") } | |
88 | withMessage = func(logger log.Logger) { log.NewContext(logger).With("a", "b").Log("c", "d") } | |
89 | ) | |
90 | ||
91 | // copied from log/concurrency_test.go | |
92 | func testConcurrency(t *testing.T, logger log.Logger) { | |
93 | for _, n := range []int{10, 100, 500} { | |
94 | wg := sync.WaitGroup{} | |
95 | wg.Add(n) | |
96 | for i := 0; i < n; i++ { | |
97 | go func() { spam(logger); wg.Done() }() | |
98 | } | |
99 | wg.Wait() | |
100 | } | |
101 | } | |
102 | ||
103 | func spam(logger log.Logger) { | |
104 | for i := 0; i < 100; i++ { | |
105 | logger.Log("key", strconv.FormatInt(int64(i), 10)) | |
106 | } | |
107 | } |