Add some polish.
- Improved names.
- Better docs.
- More colors.
- Fix Default color.
- Support colored output on Windows.
Chris Hines
8 years ago
8 | 8 | "github.com/go-kit/kit/log" |
9 | 9 | ) |
10 | 10 | |
11 | // Color is the abstract color, the zero value is the Default. | |
11 | // Color represents an ANSI color. The zero value is Default. | |
12 | 12 | type Color uint8 |
13 | 13 | |
14 | // ANSI colors. | |
14 | 15 | const ( |
15 | NoColor = Color(iota) | |
16 | Default = Color(iota) | |
17 | Black | |
18 | DarkRed | |
19 | DarkGreen | |
20 | Brown | |
21 | DarkBlue | |
22 | DarkMagenta | |
23 | DarkCyan | |
24 | Gray | |
25 | ||
26 | DarkGray | |
16 | 27 | Red |
17 | 28 | Green |
18 | 29 | Yellow |
20 | 31 | Magenta |
21 | 32 | Cyan |
22 | 33 | White |
23 | Default | |
24 | 34 | |
25 | maxColor | |
35 | numColors | |
26 | 36 | ) |
27 | 37 | |
28 | var resetColorBytes = []byte("\x1b[0m") | |
38 | var resetColorBytes = []byte("\x1b[39;49m") | |
29 | 39 | var fgColorBytes [][]byte |
30 | 40 | var bgColorBytes [][]byte |
31 | 41 | |
32 | 42 | func init() { |
33 | for color := NoColor; color < maxColor; color++ { | |
34 | fgColorBytes = append(fgColorBytes, []byte(fmt.Sprintf("\x1b[%dm", 30+color))) | |
35 | bgColorBytes = append(bgColorBytes, []byte(fmt.Sprintf("\x1b[%dm", 40+color))) | |
43 | // Default | |
44 | fgColorBytes = append(fgColorBytes, []byte("\x1b[39m")) | |
45 | bgColorBytes = append(bgColorBytes, []byte("\x1b[49m")) | |
46 | ||
47 | // dark colors | |
48 | for color := Black; color < DarkGray; color++ { | |
49 | fgColorBytes = append(fgColorBytes, []byte(fmt.Sprintf("\x1b[%dm", 30+color-Black))) | |
50 | bgColorBytes = append(bgColorBytes, []byte(fmt.Sprintf("\x1b[%dm", 40+color-Black))) | |
51 | } | |
52 | ||
53 | // bright colors | |
54 | for color := DarkGray; color < numColors; color++ { | |
55 | fgColorBytes = append(fgColorBytes, []byte(fmt.Sprintf("\x1b[%d;1m", 30+color-DarkGray))) | |
56 | bgColorBytes = append(bgColorBytes, []byte(fmt.Sprintf("\x1b[%d;1m", 40+color-DarkGray))) | |
36 | 57 | } |
37 | 58 | } |
38 | 59 | |
60 | // FgBgColor represents a foreground and background color. | |
39 | 61 | type FgBgColor struct { |
40 | 62 | Fg, Bg Color |
41 | 63 | } |
42 | 64 | |
43 | func (c FgBgColor) IsZero() bool { | |
44 | return c.Fg == NoColor && c.Bg == NoColor | |
65 | func (c FgBgColor) isZero() bool { | |
66 | return c.Fg == Default && c.Bg == Default | |
45 | 67 | } |
46 | 68 | |
47 | // NewColorLogger returns a log.Logger which writes colored logs to w. It | |
48 | // colors whole records based on the FgBgColor returned by the color function. | |
49 | // Log events are formatted by the Logger returned by newLogger. | |
69 | // NewColorLogger returns a Logger which writes colored logs to w. ANSI color | |
70 | // codes for the colors returned by color are added to the formatted output | |
71 | // from the Logger returned by newLogger and the combined result written to w. | |
50 | 72 | func NewColorLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger { |
51 | 73 | if color == nil { |
52 | 74 | panic("color func nil") |
70 | 92 | |
71 | 93 | func (l *colorLogger) Log(keyvals ...interface{}) error { |
72 | 94 | color := l.color(keyvals...) |
73 | if color.IsZero() { | |
95 | if color.isZero() { | |
74 | 96 | return l.noColorLogger.Log(keyvals...) |
75 | 97 | } |
76 | 98 | |
77 | 99 | lb := l.getLoggerBuf() |
78 | 100 | defer l.putLoggerBuf(lb) |
79 | if color.Fg != NoColor { | |
101 | if color.Fg != Default { | |
80 | 102 | lb.buf.Write(fgColorBytes[color.Fg]) |
81 | 103 | } |
82 | if color.Bg != NoColor { | |
104 | if color.Bg != Default { | |
83 | 105 | lb.buf.Write(bgColorBytes[color.Bg]) |
84 | 106 | } |
85 | 107 | err := lb.logger.Log(keyvals...) |
86 | 108 | if err != nil { |
87 | 109 | return err |
88 | 110 | } |
89 | if color.Fg != NoColor || color.Bg != NoColor { | |
111 | if color.Fg != Default || color.Bg != Default { | |
90 | 112 | lb.buf.Write(resetColorBytes) |
91 | 113 | } |
92 | 114 | _, err = io.Copy(l.w, lb.buf) |
131 | 153 | } |
132 | 154 | switch asString(keyvals[i+1]) { |
133 | 155 | case "debug": |
134 | return FgBgColor{Fg: Green} | |
156 | return FgBgColor{Fg: DarkGray} | |
135 | 157 | case "info": |
136 | return FgBgColor{Fg: White} | |
158 | return FgBgColor{Fg: Gray} | |
137 | 159 | case "warn": |
138 | 160 | return FgBgColor{Fg: Yellow} |
139 | 161 | case "error": |
140 | 162 | return FgBgColor{Fg: Red} |
141 | 163 | case "crit": |
142 | return FgBgColor{Fg: Default, Bg: Red} | |
164 | return FgBgColor{Fg: Gray, Bg: DarkRed} | |
143 | 165 | default: |
144 | 166 | return FgBgColor{} |
145 | 167 | } |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "bytes" |
4 | "errors" | |
5 | 4 | "io" |
6 | 5 | "io/ioutil" |
7 | "os" | |
8 | 6 | "strconv" |
9 | 7 | "sync" |
10 | 8 | "testing" |
25 | 23 | t.Fatal(err) |
26 | 24 | } |
27 | 25 | if want, have := "hello=world\n", buf.String(); want != have { |
28 | t.Errorf("want %#v, have %#v", want, have) | |
26 | t.Errorf("\nwant %#v\nhave %#v", want, have) | |
29 | 27 | } |
30 | 28 | |
31 | 29 | buf.Reset() |
32 | if err := logger.Log("a", 1, "err", errors.New("error")); err != nil { | |
30 | if err := logger.Log("a", 1); err != nil { | |
33 | 31 | t.Fatal(err) |
34 | 32 | } |
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) | |
33 | if want, have := "\x1b[32;1m\x1b[47;1ma=1\n\x1b[39;49m", buf.String(); want != have { | |
34 | t.Errorf("\nwant %#v\nhave %#v", want, have) | |
37 | 35 | } |
38 | 36 | } |
39 | 37 | |
40 | 38 | func newColorLogger(w io.Writer) log.Logger { |
41 | 39 | return term.NewColorLogger(w, log.NewLogfmtLogger, |
42 | 40 | func(keyvals ...interface{}) term.FgBgColor { |
43 | for i := 0; i < len(keyvals); i += 2 { | |
44 | key := keyvals[i] | |
45 | if key == "a" { | |
46 | return term.FgBgColor{Fg: term.Green, Bg: term.Default} | |
47 | } | |
48 | if key == "err" && keyvals[i+1] != nil { | |
49 | return term.FgBgColor{Fg: term.White, Bg: term.Red} | |
50 | } | |
41 | if keyvals[0] == "a" { | |
42 | return term.FgBgColor{Fg: term.Green, Bg: term.White} | |
51 | 43 | } |
52 | 44 | return term.FgBgColor{} |
53 | 45 | }) |
97 | 89 | logger.Log("a", strconv.FormatInt(int64(i), 10)) |
98 | 90 | } |
99 | 91 | } |
100 | ||
101 | func ExampleNewColorLogger() { | |
102 | // Color errors red | |
103 | logger := term.NewColorLogger(os.Stdout, log.NewLogfmtLogger, | |
104 | func(keyvals ...interface{}) term.FgBgColor { | |
105 | for i := 1; i < len(keyvals); i += 2 { | |
106 | if _, ok := keyvals[i].(error); ok { | |
107 | return term.FgBgColor{Fg: term.White, Bg: term.Red} | |
108 | } | |
109 | } | |
110 | return term.FgBgColor{} | |
111 | }) | |
112 | ||
113 | logger.Log("c", "c is uncolored value", "err", nil) | |
114 | logger.Log("c", "c is colored 'cause err colors it", "err", errors.New("coloring error")) | |
115 | } |
0 | // +build !windows | |
1 | ||
2 | package term | |
3 | ||
4 | import "io" | |
5 | ||
6 | // NewColorWriter returns an io.Writer that writes to w and provides cross | |
7 | // platform support for ANSI color codes. If w is not a terminal it is | |
8 | // returned unmodified. | |
9 | func NewColorWriter(w io.Writer) io.Writer { | |
10 | return w | |
11 | } |
0 | // The code in this file is adapted from github.com/mattn/go-colorable. | |
1 | ||
2 | // +build windows | |
3 | ||
4 | package term | |
5 | ||
6 | import ( | |
7 | "bytes" | |
8 | "fmt" | |
9 | "io" | |
10 | "strconv" | |
11 | "strings" | |
12 | "syscall" | |
13 | "unsafe" | |
14 | ) | |
15 | ||
16 | const ( | |
17 | foregroundBlue = 0x1 | |
18 | foregroundGreen = 0x2 | |
19 | foregroundRed = 0x4 | |
20 | foregroundIntensity = 0x8 | |
21 | foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) | |
22 | backgroundBlue = 0x10 | |
23 | backgroundGreen = 0x20 | |
24 | backgroundRed = 0x40 | |
25 | backgroundIntensity = 0x80 | |
26 | backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) | |
27 | ) | |
28 | ||
29 | type ( | |
30 | wchar uint16 | |
31 | short int16 | |
32 | dword uint32 | |
33 | word uint16 | |
34 | ) | |
35 | ||
36 | type coord struct { | |
37 | x short | |
38 | y short | |
39 | } | |
40 | ||
41 | type smallRect struct { | |
42 | left short | |
43 | top short | |
44 | right short | |
45 | bottom short | |
46 | } | |
47 | ||
48 | type consoleScreenBufferInfo struct { | |
49 | size coord | |
50 | cursorPosition coord | |
51 | attributes word | |
52 | window smallRect | |
53 | maximumWindowSize coord | |
54 | } | |
55 | ||
56 | var ( | |
57 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") | |
58 | procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") | |
59 | ) | |
60 | ||
61 | type colorWriter struct { | |
62 | out io.Writer | |
63 | handle syscall.Handle | |
64 | lastbuf bytes.Buffer | |
65 | oldattr word | |
66 | } | |
67 | ||
68 | // NewColorWriter returns an io.Writer that writes to w and provides cross | |
69 | // platform support for ANSI color codes. If w is not a terminal it is | |
70 | // returned unmodified. | |
71 | func NewColorWriter(w FdWriter) io.Writer { | |
72 | if !IsTerminal(w.Fd()) { | |
73 | return w | |
74 | } | |
75 | var csbi consoleScreenBufferInfo | |
76 | handle := syscall.Handle(w.Fd()) | |
77 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) | |
78 | return &colorWriter{out: w, handle: handle, oldattr: csbi.attributes} | |
79 | } | |
80 | ||
81 | func (w *colorWriter) Write(data []byte) (n int, err error) { | |
82 | var csbi consoleScreenBufferInfo | |
83 | procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) | |
84 | ||
85 | er := bytes.NewBuffer(data) | |
86 | loop: | |
87 | for { | |
88 | r1, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) | |
89 | if r1 == 0 { | |
90 | break loop | |
91 | } | |
92 | ||
93 | c1, _, err := er.ReadRune() | |
94 | if err != nil { | |
95 | break loop | |
96 | } | |
97 | if c1 != 0x1b { | |
98 | fmt.Fprint(w.out, string(c1)) | |
99 | continue | |
100 | } | |
101 | c2, _, err := er.ReadRune() | |
102 | if err != nil { | |
103 | w.lastbuf.WriteRune(c1) | |
104 | break loop | |
105 | } | |
106 | if c2 != 0x5b { | |
107 | w.lastbuf.WriteRune(c1) | |
108 | w.lastbuf.WriteRune(c2) | |
109 | continue | |
110 | } | |
111 | ||
112 | var buf bytes.Buffer | |
113 | var m rune | |
114 | for { | |
115 | c, _, err := er.ReadRune() | |
116 | if err != nil { | |
117 | w.lastbuf.WriteRune(c1) | |
118 | w.lastbuf.WriteRune(c2) | |
119 | w.lastbuf.Write(buf.Bytes()) | |
120 | break loop | |
121 | } | |
122 | if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { | |
123 | m = c | |
124 | break | |
125 | } | |
126 | buf.Write([]byte(string(c))) | |
127 | } | |
128 | ||
129 | switch m { | |
130 | case 'm': | |
131 | attr := csbi.attributes | |
132 | cs := buf.String() | |
133 | if cs == "" { | |
134 | procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.oldattr)) | |
135 | continue | |
136 | } | |
137 | token := strings.Split(cs, ";") | |
138 | intensityMode := word(0) | |
139 | for _, ns := range token { | |
140 | if n, err = strconv.Atoi(ns); err == nil { | |
141 | switch { | |
142 | case n == 0: | |
143 | attr = w.oldattr | |
144 | case n == 1: | |
145 | attr |= intensityMode | |
146 | case 30 <= n && n <= 37: | |
147 | attr = (attr & backgroundMask) | |
148 | if (n-30)&1 != 0 { | |
149 | attr |= foregroundRed | |
150 | } | |
151 | if (n-30)&2 != 0 { | |
152 | attr |= foregroundGreen | |
153 | } | |
154 | if (n-30)&4 != 0 { | |
155 | attr |= foregroundBlue | |
156 | } | |
157 | intensityMode = foregroundIntensity | |
158 | case n == 39: // reset foreground color | |
159 | attr &= backgroundMask | |
160 | attr |= w.oldattr & foregroundMask | |
161 | case 40 <= n && n <= 47: | |
162 | attr = (attr & foregroundMask) | |
163 | if (n-40)&1 != 0 { | |
164 | attr |= backgroundRed | |
165 | } | |
166 | if (n-40)&2 != 0 { | |
167 | attr |= backgroundGreen | |
168 | } | |
169 | if (n-40)&4 != 0 { | |
170 | attr |= backgroundBlue | |
171 | } | |
172 | intensityMode = backgroundIntensity | |
173 | case n == 49: // reset background color | |
174 | attr &= foregroundMask | |
175 | attr |= w.oldattr & backgroundMask | |
176 | } | |
177 | procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr)) | |
178 | } | |
179 | } | |
180 | } | |
181 | } | |
182 | return len(data) - w.lastbuf.Len(), nil | |
183 | } |
0 | package term_test | |
1 | ||
2 | import ( | |
3 | "errors" | |
4 | "os" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | "github.com/go-kit/kit/log/term" | |
8 | ) | |
9 | ||
10 | func ExampleNewLogger() { | |
11 | // Color errors red | |
12 | colorFn := func(keyvals ...interface{}) term.FgBgColor { | |
13 | for i := 1; i < len(keyvals); i += 2 { | |
14 | if _, ok := keyvals[i].(error); ok { | |
15 | return term.FgBgColor{Fg: term.White, Bg: term.Red} | |
16 | } | |
17 | } | |
18 | return term.FgBgColor{} | |
19 | } | |
20 | ||
21 | logger := term.NewLogger(os.Stdout, log.NewLogfmtLogger, colorFn) | |
22 | ||
23 | logger.Log("msg", "default color", "err", nil) | |
24 | logger.Log("msg", "colored because of error", "err", errors.New("coloring error")) | |
25 | } |
0 | // Package term provides tools for logging to a terminal. | |
1 | package term | |
2 | ||
3 | import ( | |
4 | "io" | |
5 | ||
6 | "github.com/go-kit/kit/log" | |
7 | ) | |
8 | ||
9 | // NewLogger returns a Logger that takes advantage of terminal features if | |
10 | // possible. Log events are formatted by the Logger returned by newLogger. If | |
11 | // w is a terminal each log event is colored according to the color function. | |
12 | func NewLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger { | |
13 | fw, ok := w.(FdWriter) | |
14 | if !ok || !IsTerminal(fw.Fd()) { | |
15 | return newLogger(w) | |
16 | } | |
17 | return NewColorLogger(NewColorWriter(fw), newLogger, color) | |
18 | } | |
19 | ||
20 | // An FdWriter is a Writer that has a file descriptor. | |
21 | type FdWriter interface { | |
22 | io.Writer | |
23 | Fd() uintptr | |
24 | } |
6 | 6 | |
7 | 7 | package term |
8 | 8 | |
9 | // IsTty always returns false on AppEngine. | |
10 | func IsTty(fd uintptr) bool { | |
9 | // IsTerminal always returns false on AppEngine. | |
10 | func IsTerminal(fd uintptr) bool { | |
11 | 11 | return false |
12 | 12 | } |
7 | 7 | import "syscall" |
8 | 8 | |
9 | 9 | const ioctlReadTermios = syscall.TIOCGETA |
10 | ||
11 | type Termios syscall.Termios |
4 | 4 | ) |
5 | 5 | |
6 | 6 | const ioctlReadTermios = syscall.TIOCGETA |
7 | ||
8 | // Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin. | |
9 | type Termios struct { | |
10 | Iflag uint32 | |
11 | Oflag uint32 | |
12 | Cflag uint32 | |
13 | Lflag uint32 | |
14 | Cc [20]uint8 | |
15 | Ispeed uint32 | |
16 | Ospeed uint32 | |
17 | } |
9 | 9 | import "syscall" |
10 | 10 | |
11 | 11 | const ioctlReadTermios = syscall.TCGETS |
12 | ||
13 | type Termios syscall.Termios |
11 | 11 | "unsafe" |
12 | 12 | ) |
13 | 13 | |
14 | // IsTty returns true if the given file descriptor is a terminal. | |
15 | func IsTty(fd uintptr) bool { | |
16 | var termios Termios | |
14 | // IsTerminal returns true if the given file descriptor is a terminal. | |
15 | func IsTerminal(fd uintptr) bool { | |
16 | var termios syscall.Termios | |
17 | 17 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) |
18 | 18 | return err == 0 |
19 | 19 | } |
2 | 2 | import "syscall" |
3 | 3 | |
4 | 4 | const ioctlReadTermios = syscall.TIOCGETA |
5 | ||
6 | type Termios syscall.Termios |
17 | 17 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") |
18 | 18 | ) |
19 | 19 | |
20 | // IsTty returns true if the given file descriptor is a terminal. | |
21 | func IsTty(fd uintptr) bool { | |
20 | // IsTerminal returns true if the given file descriptor is a terminal. | |
21 | func IsTerminal(fd uintptr) bool { | |
22 | 22 | var st uint32 |
23 | 23 | r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) |
24 | 24 | return r != 0 && e == 0 |