package xhttp import ( "bufio" "errors" "fmt" "log" "net" "net/http" "runtime/debug" "time" "golang.org/x/text/message" "oss.terrastruct.com/util-go/cmdlog" ) type ResponseWriter interface { http.ResponseWriter http.Hijacker http.Flusher writtenResponseWriter } var _ ResponseWriter = &responseWriter{} type responseWriter struct { rw http.ResponseWriter written bool status int length int } func (rw *responseWriter) Header() http.Header { return rw.rw.Header() } func (rw *responseWriter) WriteHeader(statusCode int) { if !rw.written { rw.written = true rw.status = statusCode } rw.rw.WriteHeader(statusCode) } func (rw *responseWriter) Write(p []byte) (int, error) { if !rw.written && len(p) > 0 { rw.written = true if rw.status == 0 { rw.status = http.StatusOK } } rw.length += len(p) return rw.rw.Write(p) } func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hj, ok := rw.rw.(http.Hijacker) if !ok { return nil, nil, fmt.Errorf("underlying response writer does not implement http.Hijacker: %T", rw.rw) } return hj.Hijack() } func (rw *responseWriter) Flush() { f, ok := rw.rw.(http.Flusher) if !ok { return } f.Flush() } func (rw *responseWriter) Written() bool { return rw.written } func Log(clog *cmdlog.Logger, next http.Handler) http.Handler { englishPrinter := message.NewPrinter(message.MatchLanguage("en")) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { rec := recover() if rec != nil { clog.Error.Printf("caught panic: %#v\n%s", rec, debug.Stack()) JSON(clog, w, http.StatusInternalServerError, map[string]interface{}{ "error": http.StatusText(http.StatusInternalServerError), }) } }() rw := &responseWriter{ rw: w, } start := time.Now() next.ServeHTTP(rw, r) dur := time.Since(start) if !rw.Written() { _, err := rw.Write(nil) if errors.Is(err, http.ErrHijacked) { clog.Success.Printf("%s %s %v: hijacked", r.Method, r.URL, dur) return } clog.Warn.Printf("%s %s %v: no response written", r.Method, r.URL, dur) return } var statusLogger *log.Logger switch { case 100 <= rw.status && rw.status <= 299: statusLogger = clog.Success case 300 <= rw.status && rw.status <= 399: statusLogger = clog.Info case 400 <= rw.status && rw.status <= 499: statusLogger = clog.Warn case 500 <= rw.status && rw.status <= 599: statusLogger = clog.Error } lengthStr := englishPrinter.Sprint(rw.length) statusLogger.Printf("%s %s %d %sB %v", r.Method, r.URL, rw.status, lengthStr, dur) }) }