├── .gitignore ├── LICENSE ├── README.md ├── color.go ├── color_test.go ├── colors.png ├── example.png ├── go.mod ├── go.sum └── goid ├── doc.go └── gotrack.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 George Xie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Colored Goroutines 2 | 3 | 4 | `import "github.com/xiegeo/coloredgoroutine"` 5 | 6 | Just replace 7 | ``` 8 | var logger = log.New(os.Stdout, "logger: ", log.Lshortfile) 9 | ``` 10 | with 11 | ``` 12 | var logger = log.New(coloredgoroutine.Colors(os.Stdout), "logger: ", log.Lshortfile) 13 | ``` 14 | 15 | So you can easily remove it when you are done debugging and no one will know. 16 | 17 | Sample output: 18 | 19 | ![screenshot](example.png) 20 | 21 | Color template: 22 | 23 | ![screenshot](colors.png) 24 | 25 | Bigger numbered go routines will repeat the colors. 26 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package coloredgoroutine 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | 8 | "github.com/fatih/color" 9 | "github.com/xiegeo/coloredgoroutine/goid" 10 | ) 11 | 12 | type writer struct { 13 | w io.Writer 14 | } 15 | 16 | func (w *writer) Write(p []byte) (int, error) { 17 | if len(p) == 0 { 18 | return 0, nil 19 | } 20 | c := getColorForUID(goid.ID()) 21 | if p[len(p)-1] == '\n' { 22 | p = p[:len(p)-1] 23 | return fmt.Fprintln(w.w, c.Sprintf("%s", p)) 24 | } 25 | return c.Fprintf(w.w, "%s", p) 26 | } 27 | 28 | //Colors add color to the writer based on the current go routine id 29 | func Colors(w io.Writer) io.Writer { 30 | return &writer{ 31 | w: w, 32 | } 33 | } 34 | 35 | type colorDetail struct { 36 | color.Color 37 | fg color.Attribute 38 | bg color.Attribute 39 | } 40 | 41 | func newColorDetail(fg, bg color.Attribute) colorDetail { 42 | return colorDetail{ 43 | Color: *color.New(fg, bg), 44 | fg: fg, 45 | bg: bg, 46 | } 47 | } 48 | 49 | var colors []colorDetail 50 | var bannedColors []colorDetail 51 | 52 | func banned(f, b color.Attribute) bool { 53 | same := color.BgBlack - color.FgBlack 54 | if b-f == same { 55 | return true 56 | } 57 | b = b - same 58 | if b > f { 59 | b, f = f, b 60 | } 61 | switch b { 62 | case color.FgGreen: 63 | return f == color.FgYellow || f == color.FgHiBlue || f == color.FgHiMagenta || f == color.FgHiBlack 64 | case color.FgYellow: 65 | return f == color.FgWhite || f == color.FgHiGreen || f == color.FgHiMagenta || f == color.FgHiCyan 66 | case color.FgBlue: 67 | return f == color.FgCyan 68 | case color.FgMagenta: 69 | return f == color.FgHiBlack || f == color.FgHiRed || f == color.FgCyan 70 | case color.FgCyan: 71 | return f == color.FgHiBlack || f == color.FgHiBlue 72 | case color.FgWhite: 73 | return f == color.FgHiGreen || f == color.FgHiYellow || f == color.FgHiCyan 74 | case color.FgHiBlack: 75 | return f == color.FgHiBlue || f == color.FgHiMagenta || f == color.FgHiRed 76 | case color.FgHiGreen: 77 | return f == color.FgHiYellow || f == color.FgHiCyan 78 | case color.FgHiYellow: 79 | return f == color.FgHiCyan 80 | case color.FgHiBlue: 81 | return f == color.FgHiRed || f == color.FgHiMagenta 82 | 83 | } 84 | return false 85 | } 86 | 87 | func init() { 88 | add := func(f, b color.Attribute) { 89 | if banned(f, b) { 90 | bannedColors = append(bannedColors, newColorDetail(f, b)) 91 | } else { 92 | colors = append(colors, newColorDetail(f, b)) 93 | } 94 | } 95 | hi := color.FgHiBlack - color.FgBlack 96 | for f := color.FgBlack; f <= color.FgWhite; f++ { 97 | for b := color.BgBlack; b <= color.BgWhite; b++ { 98 | add(f, b) 99 | add(f+hi, b) 100 | add(f, b+hi) 101 | add(f+hi, b+hi) 102 | } 103 | } 104 | shuffle(colors) 105 | order(bannedColors) 106 | } 107 | 108 | func shuffle(c []colorDetail) { 109 | shuffleKey := 11 110 | for i := 0; i < len(c); i++ { 111 | t := (i * shuffleKey) % len(c) 112 | c[i], c[t] = c[t], c[i] 113 | } 114 | } 115 | 116 | func order(c []colorDetail) { 117 | same := color.BgBlack - color.FgBlack 118 | sort.Slice(c, func(i, j int) bool { 119 | ai, bi := c[i].fg, c[i].bg-same 120 | if ai > bi { 121 | ai, bi = bi, ai 122 | } 123 | aj, bj := c[j].fg, c[j].bg-same 124 | if aj > bj { 125 | aj, bj = bj, aj 126 | } 127 | if ai < aj { 128 | return true 129 | } 130 | if ai > aj { 131 | return false 132 | } 133 | return bi < bj 134 | }) 135 | } 136 | 137 | func getColorForUID(id uint64) *color.Color { 138 | return getColorForID(int(id)) 139 | } 140 | 141 | func getColorForID(id int) *color.Color { 142 | id = id % len(colors) 143 | if id < 0 { 144 | id += len(colors) 145 | } 146 | return &colors[id].Color 147 | } 148 | -------------------------------------------------------------------------------- /color_test.go: -------------------------------------------------------------------------------- 1 | package coloredgoroutine 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/xiegeo/coloredgoroutine/goid" 11 | ) 12 | 13 | func TestGetColorForID(t *testing.T) { 14 | t.Log(getColorForID(math.MinInt32).Sprintf("%d", math.MinInt32)) 15 | t.Log(getColorForUID(math.MaxUint64).Sprintf("%d", uint64(math.MaxUint64))) 16 | 17 | for i := 0; i < len(colors); { 18 | s := []interface{}{} 19 | for { 20 | s = append(s, getColorForID(i).Sprintf("%3d", i)) 21 | //c := &colors[i] 22 | //s = append(s, c.Sprintf("%3d%3d", c.fg, c.bg-10)) 23 | i++ 24 | if i%24 == 0 || i == len(colors) { 25 | break 26 | } 27 | } 28 | t.Log(s...) 29 | } 30 | } 31 | func TestPrintBannedColors(t *testing.T) { 32 | for i := 0; i < len(bannedColors); { 33 | s := []interface{}{} 34 | for i < len(bannedColors) { 35 | c := &bannedColors[i] 36 | s = append(s, c.Sprintf("%3d%3d", c.fg, c.bg-10)) 37 | i++ 38 | if i%13 == 0 { 39 | break 40 | } 41 | } 42 | t.Log(s...) 43 | } 44 | } 45 | 46 | func TestColorsInGoRoutines(t *testing.T) { 47 | 48 | c := Colors(os.Stdout) 49 | 50 | fmt.Fprintln(c, "Hi, I am go routine", goid.ID(), "from test routine") 51 | 52 | if testing.Short() { 53 | t.Skip("skip the longer version") 54 | } 55 | 56 | count := 100 57 | 58 | var wg sync.WaitGroup 59 | wg.Add(count) 60 | 61 | for i := 0; i < count; i++ { 62 | i := i 63 | go func() { 64 | fmt.Fprintln(c, "Hi, I am go routine", goid.ID(), "from loop i =", i) 65 | wg.Done() 66 | }() 67 | } 68 | wg.Wait() 69 | } 70 | -------------------------------------------------------------------------------- /colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiegeo/coloredgoroutine/8e8801d947a9668855ef319566780c941deea326/colors.png -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiegeo/coloredgoroutine/8e8801d947a9668855ef319566780c941deea326/example.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiegeo/coloredgoroutine 2 | 3 | require ( 4 | github.com/fatih/color v1.7.0 5 | github.com/mattn/go-colorable v0.0.9 // indirect 6 | github.com/mattn/go-isatty v0.0.4 // indirect 7 | golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 2 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 3 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 4 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 5 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 6 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 7 | golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789 h1:T8D7l6WB3tLu+VpKvw06ieD/OhBi1XpJmG1U/FtttZg= 8 | golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 9 | -------------------------------------------------------------------------------- /goid/doc.go: -------------------------------------------------------------------------------- 1 | //Package goid exports a private function sourced from github.com/tylerb/gls 2 | package goid 3 | 4 | //ID returns the ID the the current Go routine 5 | func ID() uint64 { 6 | return curGoroutineID() 7 | } 8 | -------------------------------------------------------------------------------- /goid/gotrack.go: -------------------------------------------------------------------------------- 1 | package goid 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "runtime" 8 | "strconv" 9 | "sync" 10 | ) 11 | 12 | // Sourced https://github.com/bradfitz/http2/blob/dc0c5c000ec33e263612939744d51a3b68b9cece/gotrack.go 13 | var goroutineSpace = []byte("goroutine ") 14 | var littleBuf = sync.Pool{ 15 | New: func() interface{} { 16 | buf := make([]byte, 64) 17 | return &buf 18 | }, 19 | } 20 | 21 | func curGoroutineID() uint64 { 22 | bp := littleBuf.Get().(*[]byte) 23 | defer littleBuf.Put(bp) 24 | b := *bp 25 | b = b[:runtime.Stack(b, false)] 26 | // Parse the 4707 out of "goroutine 4707 [" 27 | b = bytes.TrimPrefix(b, goroutineSpace) 28 | i := bytes.IndexByte(b, ' ') 29 | if i < 0 { 30 | panic(fmt.Sprintf("No space found in %q", b)) 31 | } 32 | b = b[:i] 33 | n, err := parseUintBytes(b, 10, 64) 34 | if err != nil { 35 | panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err)) 36 | } 37 | return n 38 | } 39 | 40 | // parseUintBytes is like strconv.ParseUint, but using a []byte. 41 | func parseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) { 42 | var cutoff, maxVal uint64 43 | 44 | if bitSize == 0 { 45 | bitSize = int(strconv.IntSize) 46 | } 47 | 48 | s0 := s 49 | switch { 50 | case len(s) < 1: 51 | err = strconv.ErrSyntax 52 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 53 | 54 | case 2 <= base && base <= 36: 55 | // valid base; nothing to do 56 | 57 | case base == 0: 58 | // Look for octal, hex prefix. 59 | switch { 60 | case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): 61 | base = 16 62 | s = s[2:] 63 | if len(s) < 1 { 64 | err = strconv.ErrSyntax 65 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 66 | } 67 | case s[0] == '0': 68 | base = 8 69 | default: 70 | base = 10 71 | } 72 | 73 | default: 74 | err = errors.New("invalid base " + strconv.Itoa(base)) 75 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 76 | } 77 | 78 | n = 0 79 | cutoff = cutoff64(base) 80 | maxVal = 1<= base { 98 | n = 0 99 | err = strconv.ErrSyntax 100 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 101 | } 102 | 103 | if n >= cutoff { 104 | // n*base overflows 105 | n = 1<<64 - 1 106 | err = strconv.ErrRange 107 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 108 | } 109 | n *= uint64(base) 110 | 111 | n1 := n + uint64(v) 112 | if n1 < n || n1 > maxVal { 113 | // n+v overflows 114 | n = 1<<64 - 1 115 | err = strconv.ErrRange 116 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} 117 | } 118 | n = n1 119 | } 120 | 121 | return n, nil 122 | } 123 | 124 | // Return the first number n such that n*base >= 1<<64. 125 | func cutoff64(base int) uint64 { 126 | if base < 2 { 127 | return 0 128 | } 129 | return (1<<64-1)/uint64(base) + 1 130 | } 131 | --------------------------------------------------------------------------------