github.com/gofunct/common@v0.0.0-20190131174352-fd058c7fbf22/pkg/transport/handlers/logging.go (about) 1 package handlers 2 3 import ( 4 "fmt" 5 "io" 6 "net" 7 "net/http" 8 "net/url" 9 "strconv" 10 "time" 11 "unicode/utf8" 12 ) 13 14 // Logging 15 16 // FormatterParams is the structure any formatter will be handed when time to log comes 17 type LogFormatterParams struct { 18 Request *http.Request 19 URL url.URL 20 TimeStamp time.Time 21 StatusCode int 22 Size int 23 } 24 25 // LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler 26 type LogFormatter func(writer io.Writer, params LogFormatterParams) 27 28 // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its 29 // friends 30 31 type loggingHandler struct { 32 writer io.Writer 33 handler http.Handler 34 formatter LogFormatter 35 } 36 37 func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 38 t := time.Now() 39 logger := makeLogger(w) 40 url := *req.URL 41 42 h.handler.ServeHTTP(logger, req) 43 44 params := LogFormatterParams{ 45 Request: req, 46 URL: url, 47 TimeStamp: t, 48 StatusCode: logger.Status(), 49 Size: logger.Size(), 50 } 51 52 h.formatter(h.writer, params) 53 } 54 55 func makeLogger(w http.ResponseWriter) loggingResponseWriter { 56 var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK} 57 if _, ok := w.(http.Hijacker); ok { 58 logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}} 59 } 60 h, ok1 := logger.(http.Hijacker) 61 c, ok2 := w.(http.CloseNotifier) 62 if ok1 && ok2 { 63 return hijackCloseNotifier{logger, h, c} 64 } 65 if ok2 { 66 return &closeNotifyWriter{logger, c} 67 } 68 return logger 69 } 70 71 type commonLoggingResponseWriter interface { 72 http.ResponseWriter 73 http.Flusher 74 Status() int 75 Size() int 76 } 77 78 const lowerhex = "0123456789abcdef" 79 80 func appendQuoted(buf []byte, s string) []byte { 81 var runeTmp [utf8.UTFMax]byte 82 for width := 0; len(s) > 0; s = s[width:] { 83 r := rune(s[0]) 84 width = 1 85 if r >= utf8.RuneSelf { 86 r, width = utf8.DecodeRuneInString(s) 87 } 88 if width == 1 && r == utf8.RuneError { 89 buf = append(buf, `\x`...) 90 buf = append(buf, lowerhex[s[0]>>4]) 91 buf = append(buf, lowerhex[s[0]&0xF]) 92 continue 93 } 94 if r == rune('"') || r == '\\' { // always backslashed 95 buf = append(buf, '\\') 96 buf = append(buf, byte(r)) 97 continue 98 } 99 if strconv.IsPrint(r) { 100 n := utf8.EncodeRune(runeTmp[:], r) 101 buf = append(buf, runeTmp[:n]...) 102 continue 103 } 104 switch r { 105 case '\a': 106 buf = append(buf, `\a`...) 107 case '\b': 108 buf = append(buf, `\b`...) 109 case '\f': 110 buf = append(buf, `\f`...) 111 case '\n': 112 buf = append(buf, `\n`...) 113 case '\r': 114 buf = append(buf, `\r`...) 115 case '\t': 116 buf = append(buf, `\t`...) 117 case '\v': 118 buf = append(buf, `\v`...) 119 default: 120 switch { 121 case r < ' ': 122 buf = append(buf, `\x`...) 123 buf = append(buf, lowerhex[s[0]>>4]) 124 buf = append(buf, lowerhex[s[0]&0xF]) 125 case r > utf8.MaxRune: 126 r = 0xFFFD 127 fallthrough 128 case r < 0x10000: 129 buf = append(buf, `\u`...) 130 for s := 12; s >= 0; s -= 4 { 131 buf = append(buf, lowerhex[r>>uint(s)&0xF]) 132 } 133 default: 134 buf = append(buf, `\U`...) 135 for s := 28; s >= 0; s -= 4 { 136 buf = append(buf, lowerhex[r>>uint(s)&0xF]) 137 } 138 } 139 } 140 } 141 return buf 142 143 } 144 145 // buildCommonLogLine builds a log entry for req in Apache Common Log Format. 146 // ts is the timestamp with which the entry should be logged. 147 // status and size are used to provide the response HTTP status and size. 148 func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte { 149 username := "-" 150 if url.User != nil { 151 if name := url.User.Username(); name != "" { 152 username = name 153 } 154 } 155 156 host, _, err := net.SplitHostPort(req.RemoteAddr) 157 158 if err != nil { 159 host = req.RemoteAddr 160 } 161 162 uri := req.RequestURI 163 164 // Requests using the CONNECT method over HTTP/2.0 must use 165 // the authority field (aka r.Host) to identify the target. 166 // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT 167 if req.ProtoMajor == 2 && req.Method == "CONNECT" { 168 uri = req.Host 169 } 170 if uri == "" { 171 uri = url.RequestURI() 172 } 173 174 buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) 175 buf = append(buf, host...) 176 buf = append(buf, " - "...) 177 buf = append(buf, username...) 178 buf = append(buf, " ["...) 179 buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...) 180 buf = append(buf, `] "`...) 181 buf = append(buf, req.Method...) 182 buf = append(buf, " "...) 183 buf = appendQuoted(buf, uri) 184 buf = append(buf, " "...) 185 buf = append(buf, req.Proto...) 186 buf = append(buf, `" `...) 187 buf = append(buf, strconv.Itoa(status)...) 188 buf = append(buf, " "...) 189 buf = append(buf, strconv.Itoa(size)...) 190 return buf 191 } 192 193 // writeLog writes a log entry for req to w in Apache Common Log Format. 194 // ts is the timestamp with which the entry should be logged. 195 // status and size are used to provide the response HTTP status and size. 196 func writeLog(writer io.Writer, params LogFormatterParams) { 197 buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) 198 buf = append(buf, '\n') 199 writer.Write(buf) 200 } 201 202 // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format. 203 // ts is the timestamp with which the entry should be logged. 204 // status and size are used to provide the response HTTP status and size. 205 func writeCombinedLog(writer io.Writer, params LogFormatterParams) { 206 buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) 207 buf = append(buf, ` "`...) 208 buf = appendQuoted(buf, params.Request.Referer()) 209 buf = append(buf, `" "`...) 210 buf = appendQuoted(buf, params.Request.UserAgent()) 211 buf = append(buf, '"', '\n') 212 writer.Write(buf) 213 } 214 215 // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in 216 // Apache Combined Log Format. 217 // 218 // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. 219 // 220 // LoggingHandler always sets the ident field of the log to - 221 func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { 222 return loggingHandler{out, h, writeCombinedLog} 223 } 224 225 // LoggingHandler return a http.Handler that wraps h and logs requests to out in 226 // Apache Common Log Format (CLF). 227 // 228 // See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format. 229 // 230 // LoggingHandler always sets the ident field of the log to - 231 // 232 // Example: 233 // 234 // r := mux.NewRouter() 235 // r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 236 // w.Write([]byte("This is a catch-all route")) 237 // }) 238 // loggedRouter := handlers.LoggingHandler(os.Stdout, r) 239 // http.ListenAndServe(":1123", loggedRouter) 240 // 241 func LoggingHandler(out io.Writer, h http.Handler) http.Handler { 242 return loggingHandler{out, h, writeLog} 243 } 244 245 // CustomLoggingHandler provides a way to supply a custom log formatter 246 // while taking advantage of the mechanisms in this package 247 func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler { 248 return loggingHandler{out, h, f} 249 } 250 251 type loggingResponseWriter interface { 252 commonLoggingResponseWriter 253 http.Pusher 254 } 255 256 func (l *responseLogger) Push(target string, opts *http.PushOptions) error { 257 p, ok := l.w.(http.Pusher) 258 if !ok { 259 return fmt.Errorf("responseLogger does not implement http.Pusher") 260 } 261 return p.Push(target, opts) 262 } 263 264 func (c *compressResponseWriter) Push(target string, opts *http.PushOptions) error { 265 p, ok := c.ResponseWriter.(http.Pusher) 266 if !ok { 267 return fmt.Errorf("compressResponseWriter does not implement http.Pusher") 268 } 269 return p.Push(target, opts) 270 }