github.com/lingyao2333/mo-zero@v1.4.1/rest/handler/loghandler.go (about) 1 package handler 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "errors" 8 "fmt" 9 "io" 10 "net" 11 "net/http" 12 "net/http/httputil" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/lingyao2333/mo-zero/core/color" 18 "github.com/lingyao2333/mo-zero/core/iox" 19 "github.com/lingyao2333/mo-zero/core/logx" 20 "github.com/lingyao2333/mo-zero/core/syncx" 21 "github.com/lingyao2333/mo-zero/core/timex" 22 "github.com/lingyao2333/mo-zero/core/utils" 23 "github.com/lingyao2333/mo-zero/rest/httpx" 24 "github.com/lingyao2333/mo-zero/rest/internal" 25 ) 26 27 const ( 28 limitBodyBytes = 1024 29 defaultSlowThreshold = time.Millisecond * 500 30 ) 31 32 var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold) 33 34 type loggedResponseWriter struct { 35 w http.ResponseWriter 36 r *http.Request 37 code int 38 } 39 40 func (w *loggedResponseWriter) Flush() { 41 if flusher, ok := w.w.(http.Flusher); ok { 42 flusher.Flush() 43 } 44 } 45 46 func (w *loggedResponseWriter) Header() http.Header { 47 return w.w.Header() 48 } 49 50 // Hijack implements the http.Hijacker interface. 51 // This expands the Response to fulfill http.Hijacker if the underlying http.ResponseWriter supports it. 52 func (w *loggedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 53 if hijacked, ok := w.w.(http.Hijacker); ok { 54 return hijacked.Hijack() 55 } 56 57 return nil, nil, errors.New("server doesn't support hijacking") 58 } 59 60 func (w *loggedResponseWriter) Write(bytes []byte) (int, error) { 61 return w.w.Write(bytes) 62 } 63 64 func (w *loggedResponseWriter) WriteHeader(code int) { 65 w.w.WriteHeader(code) 66 w.code = code 67 } 68 69 // LogHandler returns a middleware that logs http request and response. 70 func LogHandler(next http.Handler) http.Handler { 71 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 timer := utils.NewElapsedTimer() 73 logs := new(internal.LogCollector) 74 lrw := loggedResponseWriter{ 75 w: w, 76 r: r, 77 code: http.StatusOK, 78 } 79 80 var dup io.ReadCloser 81 r.Body, dup = iox.DupReadCloser(r.Body) 82 next.ServeHTTP(&lrw, r.WithContext(context.WithValue(r.Context(), internal.LogContext, logs))) 83 r.Body = dup 84 logBrief(r, lrw.code, timer, logs) 85 }) 86 } 87 88 type detailLoggedResponseWriter struct { 89 writer *loggedResponseWriter 90 buf *bytes.Buffer 91 } 92 93 func newDetailLoggedResponseWriter(writer *loggedResponseWriter, buf *bytes.Buffer) *detailLoggedResponseWriter { 94 return &detailLoggedResponseWriter{ 95 writer: writer, 96 buf: buf, 97 } 98 } 99 100 func (w *detailLoggedResponseWriter) Flush() { 101 w.writer.Flush() 102 } 103 104 func (w *detailLoggedResponseWriter) Header() http.Header { 105 return w.writer.Header() 106 } 107 108 // Hijack implements the http.Hijacker interface. 109 // This expands the Response to fulfill http.Hijacker if the underlying http.ResponseWriter supports it. 110 func (w *detailLoggedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 111 if hijacked, ok := w.writer.w.(http.Hijacker); ok { 112 return hijacked.Hijack() 113 } 114 115 return nil, nil, errors.New("server doesn't support hijacking") 116 } 117 118 func (w *detailLoggedResponseWriter) Write(bs []byte) (int, error) { 119 w.buf.Write(bs) 120 return w.writer.Write(bs) 121 } 122 123 func (w *detailLoggedResponseWriter) WriteHeader(code int) { 124 w.writer.WriteHeader(code) 125 } 126 127 // DetailedLogHandler returns a middleware that logs http request and response in details. 128 func DetailedLogHandler(next http.Handler) http.Handler { 129 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 timer := utils.NewElapsedTimer() 131 var buf bytes.Buffer 132 lrw := newDetailLoggedResponseWriter(&loggedResponseWriter{ 133 w: w, 134 r: r, 135 code: http.StatusOK, 136 }, &buf) 137 138 var dup io.ReadCloser 139 r.Body, dup = iox.DupReadCloser(r.Body) 140 logs := new(internal.LogCollector) 141 next.ServeHTTP(lrw, r.WithContext(context.WithValue(r.Context(), internal.LogContext, logs))) 142 r.Body = dup 143 logDetails(r, lrw, timer, logs) 144 }) 145 } 146 147 // SetSlowThreshold sets the slow threshold. 148 func SetSlowThreshold(threshold time.Duration) { 149 slowThreshold.Set(threshold) 150 } 151 152 func dumpRequest(r *http.Request) string { 153 reqContent, err := httputil.DumpRequest(r, true) 154 if err != nil { 155 return err.Error() 156 } 157 158 return string(reqContent) 159 } 160 161 func isOkResponse(code int) bool { 162 // not server error 163 return code < http.StatusInternalServerError 164 } 165 166 func logBrief(r *http.Request, code int, timer *utils.ElapsedTimer, logs *internal.LogCollector) { 167 var buf bytes.Buffer 168 duration := timer.Duration() 169 logger := logx.WithContext(r.Context()).WithDuration(duration) 170 buf.WriteString(fmt.Sprintf("[HTTP] %s - %s %s - %s - %s", 171 wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())) 172 if duration > slowThreshold.Load() { 173 logger.Slowf("[HTTP] %s - %s %s - %s - %s - slowcall(%s)", 174 wrapStatusCode(code), wrapMethod(r.Method), r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(), 175 timex.ReprOfDuration(duration)) 176 } 177 178 ok := isOkResponse(code) 179 if !ok { 180 fullReq := dumpRequest(r) 181 limitReader := io.LimitReader(strings.NewReader(fullReq), limitBodyBytes) 182 body, err := io.ReadAll(limitReader) 183 if err != nil { 184 buf.WriteString(fmt.Sprintf("\n%s", fullReq)) 185 } else { 186 buf.WriteString(fmt.Sprintf("\n%s", string(body))) 187 } 188 } 189 190 body := logs.Flush() 191 if len(body) > 0 { 192 buf.WriteString(fmt.Sprintf("\n%s", body)) 193 } 194 195 if ok { 196 logger.Info(buf.String()) 197 } else { 198 logger.Error(buf.String()) 199 } 200 } 201 202 func logDetails(r *http.Request, response *detailLoggedResponseWriter, timer *utils.ElapsedTimer, 203 logs *internal.LogCollector) { 204 var buf bytes.Buffer 205 duration := timer.Duration() 206 code := response.writer.code 207 logger := logx.WithContext(r.Context()) 208 buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n", 209 r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r))) 210 if duration > defaultSlowThreshold { 211 logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr, 212 fmt.Sprintf("slowcall(%s)", timex.ReprOfDuration(duration)), dumpRequest(r)) 213 } 214 215 body := logs.Flush() 216 if len(body) > 0 { 217 buf.WriteString(fmt.Sprintf("%s\n", body)) 218 } 219 220 respBuf := response.buf.Bytes() 221 if len(respBuf) > 0 { 222 buf.WriteString(fmt.Sprintf("<= %s", respBuf)) 223 } 224 225 if isOkResponse(code) { 226 logger.Info(buf.String()) 227 } else { 228 logger.Error(buf.String()) 229 } 230 } 231 232 func wrapMethod(method string) string { 233 var colour color.Color 234 switch method { 235 case http.MethodGet: 236 colour = color.BgBlue 237 case http.MethodPost: 238 colour = color.BgCyan 239 case http.MethodPut: 240 colour = color.BgYellow 241 case http.MethodDelete: 242 colour = color.BgRed 243 case http.MethodPatch: 244 colour = color.BgGreen 245 case http.MethodHead: 246 colour = color.BgMagenta 247 case http.MethodOptions: 248 colour = color.BgWhite 249 } 250 251 if colour == color.NoColor { 252 return method 253 } 254 255 return logx.WithColorPadding(method, colour) 256 } 257 258 func wrapStatusCode(code int) string { 259 var colour color.Color 260 switch { 261 case code >= http.StatusOK && code < http.StatusMultipleChoices: 262 colour = color.BgGreen 263 case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: 264 colour = color.BgBlue 265 case code >= http.StatusBadRequest && code < http.StatusInternalServerError: 266 colour = color.BgMagenta 267 default: 268 colour = color.BgYellow 269 } 270 271 return logx.WithColorPadding(strconv.Itoa(code), colour) 272 }