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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
// Package babylogger is a simple HTTP logging middleware. It works with any
// multiplexer compatible with the Go standard library.
//
// When a terminal is present it will log using nice colors. When the output is
// not in a terminal (for example in logs) ANSI escape sequences (read: colors)
// will be stripped from the output.
//
// Also note that for accurate response time logging Babylogger should be the
// first middleware called.
//
// Windows support is not currently implemented, however it would be trivial
// enough with the help of a couple packages from Mattn:
// http://github.com/mattn/go-isatty and https://github.com/mattn/go-colorable
//
// Example using the standard library:
//
// package main
//
// import (
// "fmt"
// "net/http"
// "github.com/meowgorithm/babylogger"
// )
//
// func main() {
// http.Handle("/", babylogger.Middleware(http.HandlerFunc(handler)))
// http.ListenAndServe(":8000", nil)
// }
//
// handler(w http.ResponseWriter, r *http.Request) {
// fmt.FPrintln(w, "Oh, hi, I didn’t see you there.")
// }
//
// Example with Goji:
//
// import (
// "fmt"
// "net/http"
// "github.com/meowgorithm/babylogger"
// "goji.io"
// "goji.io/pat"
// )
//
// func main() {
// mux := goji.NewMux()
// mux.Use(babylogger.Middleware)
// mux.HandleFunc(pat.Get("/"), handler)
// http.ListenAndServe(":8000", mux)
// }
//
// handler(w http.ResponseWriter, r *http.Request) {
// fmt.FPrintln(w, "Oh hi, I didn’t see you there.")
// }
package babylogger
import (
"bufio"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
humanize "github.com/dustin/go-humanize"
)
// Styles.
var (
timeStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "240", Dark: "240"})
uriStyle = timeStyle.Copy()
methodStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "62", Dark: "62"})
http200Style = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "35", Dark: "48"})
http300Style = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "208", Dark: "192"})
http400Style = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "39", Dark: "86"})
http500Style = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "203", Dark: "204"})
subtleStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "250"})
addressStyle = subtleStyle.Copy()
)
type logWriter struct {
http.ResponseWriter
code, bytes int
}
func (r *logWriter) Write(p []byte) (int, error) {
written, err := r.ResponseWriter.Write(p)
r.bytes += written
return written, err
}
// Note this is generally only called when sending an HTTP error, so it's
// important to set the `code` value to 200 as a default
func (r *logWriter) WriteHeader(code int) {
r.code = code
r.ResponseWriter.WriteHeader(code)
}
// Hijack exposes the underlying ResponseWriter Hijacker implementation for
// WebSocket compatibility
func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := r.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("WebServer does not support hijacking")
}
return hj.Hijack()
}
// Middleware is the logging middleware where we log incoming and outgoing
// requests for a multiplexer. It should be the first middleware called so it
// can log request times accurately.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
addr := r.RemoteAddr
if colon := strings.LastIndex(addr, ":"); colon != -1 {
addr = addr[:colon]
}
arrow := subtleStyle.Render("<-")
method := methodStyle.Render(r.Method)
uri := uriStyle.Render(r.RequestURI)
address := addressStyle.Render(addr)
// Log request
log.Printf("%s %s %s %s", arrow, method, uri, address)
writer := &logWriter{
ResponseWriter: w,
code: http.StatusOK, // default. so important! see above.
}
arrow = subtleStyle.Render("->")
startTime := time.Now()
// Not sure why the request could possibly be nil, but it has happened
if r == nil {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
writer.code = http.StatusInternalServerError
} else {
next.ServeHTTP(writer, r)
}
elapsedTime := time.Now().Sub(startTime)
var statusStyle lipgloss.Style
if writer.code < 300 { // 200s
statusStyle = http200Style
} else if writer.code < 400 { // 300s
statusStyle = http300Style
} else if writer.code < 500 { // 400s
statusStyle = http400Style
} else { // 500s
statusStyle = http500Style
}
status := statusStyle.Render(fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)))
// The excellent humanize package adds a space between the integer and
// the unit as far as bytes are conerned (105 B). In our case that
// makes it a little harder on the eyes when scanning the logs, so
// we're stripping that space
formattedBytes := strings.Replace(
humanize.Bytes(uint64(writer.bytes)),
" ", "", 1)
bytes := subtleStyle.Render(formattedBytes)
time := timeStyle.Render(fmt.Sprintf("%s", elapsedTime))
// Log response
log.Printf("%s %s %s %v", arrow, status, bytes, time)
})
}
|