├── .gitignore ├── LICENSE ├── README.md ├── babylogger.go ├── example └── main.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor/* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Magic Numbers 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 | Babylogger 2 | ========== 3 | 4 | [![GoDoc Badge](https://godoc.org/github.com/magicnumbers/babylogger?status.svg)](https://pkg.go.dev/github.com/meowgorithm/babylogger) 5 | 6 | A Go HTTP logger middleware, for babies. 7 | 8 | ![Example image of Babylogger doing its logging](https://i.imgur.com/VGg7Wl6.png "Babylogger doing its logging") 9 | 10 | We’ve used it with [Goji][goji] and the Go standard library, but it should work 11 | with any multiplexer worth its salt. And by that we mean any multiplexer 12 | compatible with the standard library. 13 | 14 | Note that ANSI escape sequences (read: colors) will be stripped from the output 15 | when the logger is not running in a terminal. For example, log files won't 16 | contain any sort of ANSI intended for color output. 17 | 18 | Also note that for accurate response time logging Babylogger should be the first middleware 19 | called. 20 | 21 | 22 | ## Examples 23 | 24 | ### Standard Library 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "net/http" 32 | "github.com/magicnumbers/babylogger" 33 | ) 34 | 35 | func main() { 36 | http.Handle("/", babylogger.Middleware(http.HandlerFunc(handler))) 37 | http.ListenAndServe(":8000", nil) 38 | } 39 | 40 | handler(w http.ResponseWriter, r *http.Request) { 41 | fmt.FPrintln(w, "Oh, hi, I didn’t see you there.") 42 | } 43 | ``` 44 | 45 | ### Goji 46 | 47 | ```go 48 | 49 | import ( 50 | "fmt" 51 | "net/http" 52 | "github.com/magicnumbers/babylogger" 53 | "goji.io" 54 | "goji.io/pat" 55 | ) 56 | 57 | func main() { 58 | mux := goji.NewMux() 59 | mux.Use(babylogger.Middleware) 60 | mux.HandleFunc(pat.Get("/"), handler) 61 | http.ListenAndServe(":8000", mux) 62 | } 63 | 64 | handler(w http.ResponseWriter, r *http.Request) { 65 | fmt.FPrintln(w, "Oh hi, I didn’t see you there.") 66 | } 67 | ``` 68 | 69 | 70 | ## License 71 | 72 | MIT 73 | 74 | [goji]: http://goji.io 75 | [mattn]: https://github.com/mattn 76 | -------------------------------------------------------------------------------- /babylogger.go: -------------------------------------------------------------------------------- 1 | // Package babylogger is a simple HTTP logging middleware. It works with any 2 | // multiplexer compatible with the Go standard library. 3 | // 4 | // When a terminal is present it will log using nice colors. When the output is 5 | // not in a terminal (for example in logs) ANSI escape sequences (read: colors) 6 | // will be stripped from the output. 7 | // 8 | // Also note that for accurate response time logging Babylogger should be the 9 | // first middleware called. 10 | // 11 | // Windows support is not currently implemented, however it would be trivial 12 | // enough with the help of a couple packages from Mattn: 13 | // http://github.com/mattn/go-isatty and https://github.com/mattn/go-colorable 14 | // 15 | // Example using the standard library: 16 | // 17 | // package main 18 | // 19 | // import ( 20 | // "fmt" 21 | // "net/http" 22 | // "github.com/meowgorithm/babylogger" 23 | // ) 24 | // 25 | // func main() { 26 | // http.Handle("/", babylogger.Middleware(http.HandlerFunc(handler))) 27 | // http.ListenAndServe(":8000", nil) 28 | // } 29 | // 30 | // handler(w http.ResponseWriter, r *http.Request) { 31 | // fmt.FPrintln(w, "Oh, hi, I didn’t see you there.") 32 | // } 33 | // 34 | // Example with Goji: 35 | // 36 | // import ( 37 | // "fmt" 38 | // "net/http" 39 | // "github.com/meowgorithm/babylogger" 40 | // "goji.io" 41 | // "goji.io/pat" 42 | // ) 43 | // 44 | // func main() { 45 | // mux := goji.NewMux() 46 | // mux.Use(babylogger.Middleware) 47 | // mux.HandleFunc(pat.Get("/"), handler) 48 | // http.ListenAndServe(":8000", mux) 49 | // } 50 | // 51 | // handler(w http.ResponseWriter, r *http.Request) { 52 | // fmt.FPrintln(w, "Oh hi, I didn’t see you there.") 53 | // } 54 | package babylogger 55 | 56 | import ( 57 | "bufio" 58 | "fmt" 59 | "log" 60 | "net" 61 | "net/http" 62 | "strings" 63 | "time" 64 | 65 | "github.com/charmbracelet/lipgloss" 66 | humanize "github.com/dustin/go-humanize" 67 | ) 68 | 69 | // Styles. 70 | var ( 71 | timeStyle = lipgloss.NewStyle(). 72 | Foreground(lipgloss.AdaptiveColor{Light: "240", Dark: "240"}) 73 | 74 | uriStyle = timeStyle.Copy() 75 | 76 | methodStyle = lipgloss.NewStyle(). 77 | Foreground(lipgloss.AdaptiveColor{Light: "62", Dark: "62"}) 78 | 79 | http200Style = lipgloss.NewStyle(). 80 | Foreground(lipgloss.AdaptiveColor{Light: "35", Dark: "48"}) 81 | 82 | http300Style = lipgloss.NewStyle(). 83 | Foreground(lipgloss.AdaptiveColor{Light: "208", Dark: "192"}) 84 | 85 | http400Style = lipgloss.NewStyle(). 86 | Foreground(lipgloss.AdaptiveColor{Light: "39", Dark: "86"}) 87 | 88 | http500Style = lipgloss.NewStyle(). 89 | Foreground(lipgloss.AdaptiveColor{Light: "203", Dark: "204"}) 90 | 91 | subtleStyle = lipgloss.NewStyle(). 92 | Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "250"}) 93 | 94 | addressStyle = subtleStyle.Copy() 95 | ) 96 | 97 | type logWriter struct { 98 | http.ResponseWriter 99 | code, bytes int 100 | } 101 | 102 | func (r *logWriter) Write(p []byte) (int, error) { 103 | written, err := r.ResponseWriter.Write(p) 104 | r.bytes += written 105 | return written, err 106 | } 107 | 108 | // Note this is generally only called when sending an HTTP error, so it's 109 | // important to set the `code` value to 200 as a default 110 | func (r *logWriter) WriteHeader(code int) { 111 | r.code = code 112 | r.ResponseWriter.WriteHeader(code) 113 | } 114 | 115 | // Hijack exposes the underlying ResponseWriter Hijacker implementation for 116 | // WebSocket compatibility 117 | func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 118 | hj, ok := r.ResponseWriter.(http.Hijacker) 119 | if !ok { 120 | return nil, nil, fmt.Errorf("WebServer does not support hijacking") 121 | } 122 | return hj.Hijack() 123 | } 124 | 125 | // Middleware is the logging middleware where we log incoming and outgoing 126 | // requests for a multiplexer. It should be the first middleware called so it 127 | // can log request times accurately. 128 | func Middleware(next http.Handler) http.Handler { 129 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 | 131 | addr := r.RemoteAddr 132 | if colon := strings.LastIndex(addr, ":"); colon != -1 { 133 | addr = addr[:colon] 134 | } 135 | 136 | arrow := subtleStyle.Render("<-") 137 | method := methodStyle.Render(r.Method) 138 | uri := uriStyle.Render(r.RequestURI) 139 | address := addressStyle.Render(addr) 140 | 141 | // Log request 142 | log.Printf("%s %s %s %s", arrow, method, uri, address) 143 | 144 | writer := &logWriter{ 145 | ResponseWriter: w, 146 | code: http.StatusOK, // default. so important! see above. 147 | } 148 | 149 | arrow = subtleStyle.Render("->") 150 | startTime := time.Now() 151 | 152 | // Not sure why the request could possibly be nil, but it has happened 153 | if r == nil { 154 | http.Error(w, http.StatusText(http.StatusInternalServerError), 155 | http.StatusInternalServerError) 156 | writer.code = http.StatusInternalServerError 157 | } else { 158 | next.ServeHTTP(writer, r) 159 | } 160 | 161 | elapsedTime := time.Now().Sub(startTime) 162 | 163 | var statusStyle lipgloss.Style 164 | 165 | if writer.code < 300 { // 200s 166 | statusStyle = http200Style 167 | } else if writer.code < 400 { // 300s 168 | statusStyle = http300Style 169 | } else if writer.code < 500 { // 400s 170 | statusStyle = http400Style 171 | } else { // 500s 172 | statusStyle = http500Style 173 | } 174 | 175 | status := statusStyle.Render(fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code))) 176 | 177 | // The excellent humanize package adds a space between the integer and 178 | // the unit as far as bytes are conerned (105 B). In our case that 179 | // makes it a little harder on the eyes when scanning the logs, so 180 | // we're stripping that space 181 | formattedBytes := strings.Replace( 182 | humanize.Bytes(uint64(writer.bytes)), 183 | " ", "", 1) 184 | 185 | bytes := subtleStyle.Render(formattedBytes) 186 | time := timeStyle.Render(fmt.Sprintf("%s", elapsedTime)) 187 | 188 | // Log response 189 | log.Printf("%s %s %s %v", arrow, status, bytes, time) 190 | }) 191 | } 192 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/meowgorithm/babylogger" 7 | ) 8 | 9 | func main() { 10 | 11 | // HTTP server with Babylogger middleware 12 | http.Handle("/", babylogger.Middleware(http.HandlerFunc(handler))) 13 | go func() { 14 | http.ListenAndServe(":1337", nil) 15 | }() 16 | 17 | // Perform some example HTTP requests, then exit 18 | h := "http://localhost:1337" 19 | c := &http.Client{} 20 | c.Get(h + "/") 21 | r, _ := http.NewRequest("POST", h+"/meow", nil) 22 | c.Do(r) 23 | r, _ = http.NewRequest("PUT", h+"/purr", nil) 24 | c.Do(r) 25 | c.Get(h + "/schnurr") 26 | } 27 | 28 | func handler(w http.ResponseWriter, r *http.Request) { 29 | switch r.RequestURI { 30 | case "/": 31 | w.WriteHeader(http.StatusOK) 32 | w.Write([]byte("oh hey")) 33 | case "/meow": 34 | w.WriteHeader(http.StatusTemporaryRedirect) 35 | w.Write([]byte("it's over there")) 36 | case "/purr": 37 | w.WriteHeader(http.StatusNotFound) 38 | w.Write([]byte("nope, not here")) 39 | default: 40 | w.WriteHeader(http.StatusInternalServerError) 41 | w.Write([]byte("ouch")) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/meowgorithm/babylogger 2 | 3 | require ( 4 | github.com/charmbracelet/lipgloss v0.7.1 5 | github.com/dustin/go-humanize v1.0.1 6 | golang.org/x/sys v0.8.0 // indirect 7 | ) 8 | 9 | go 1.13 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 | github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= 4 | github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 5 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 6 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 7 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 8 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 9 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 10 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 11 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 12 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 13 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 14 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 15 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 16 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= 17 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 18 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 19 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 20 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 21 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 24 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | --------------------------------------------------------------------------------