...
1 package xhttp
2
3 import (
4 "bufio"
5 "errors"
6 "fmt"
7 "log"
8 "net"
9 "net/http"
10 "runtime/debug"
11 "time"
12
13 "golang.org/x/text/message"
14
15 "oss.terrastruct.com/util-go/cmdlog"
16 )
17
18 type ResponseWriter interface {
19 http.ResponseWriter
20 http.Hijacker
21 http.Flusher
22 writtenResponseWriter
23 }
24
25 var _ ResponseWriter = &responseWriter{}
26
27 type responseWriter struct {
28 rw http.ResponseWriter
29
30 written bool
31 status int
32 length int
33 }
34
35 func (rw *responseWriter) Header() http.Header {
36 return rw.rw.Header()
37 }
38
39 func (rw *responseWriter) WriteHeader(statusCode int) {
40 if !rw.written {
41 rw.written = true
42 rw.status = statusCode
43 }
44 rw.rw.WriteHeader(statusCode)
45 }
46
47 func (rw *responseWriter) Write(p []byte) (int, error) {
48 if !rw.written && len(p) > 0 {
49 rw.written = true
50 if rw.status == 0 {
51 rw.status = http.StatusOK
52 }
53 }
54 rw.length += len(p)
55 return rw.rw.Write(p)
56 }
57
58 func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
59 hj, ok := rw.rw.(http.Hijacker)
60 if !ok {
61 return nil, nil, fmt.Errorf("underlying response writer does not implement http.Hijacker: %T", rw.rw)
62 }
63 return hj.Hijack()
64 }
65
66 func (rw *responseWriter) Flush() {
67 f, ok := rw.rw.(http.Flusher)
68 if !ok {
69 return
70 }
71 f.Flush()
72 }
73
74 func (rw *responseWriter) Written() bool {
75 return rw.written
76 }
77
78 func Log(clog *cmdlog.Logger, next http.Handler) http.Handler {
79 englishPrinter := message.NewPrinter(message.MatchLanguage("en"))
80 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 defer func() {
82 rec := recover()
83 if rec != nil {
84 clog.Error.Printf("caught panic: %#v\n%s", rec, debug.Stack())
85 JSON(clog, w, http.StatusInternalServerError, map[string]interface{}{
86 "error": http.StatusText(http.StatusInternalServerError),
87 })
88 }
89 }()
90
91 rw := &responseWriter{
92 rw: w,
93 }
94
95 start := time.Now()
96 next.ServeHTTP(rw, r)
97 dur := time.Since(start)
98
99 if !rw.Written() {
100 _, err := rw.Write(nil)
101 if errors.Is(err, http.ErrHijacked) {
102 clog.Success.Printf("%s %s %v: hijacked", r.Method, r.URL, dur)
103 return
104 }
105
106 clog.Warn.Printf("%s %s %v: no response written", r.Method, r.URL, dur)
107 return
108 }
109
110 var statusLogger *log.Logger
111 switch {
112 case 100 <= rw.status && rw.status <= 299:
113 statusLogger = clog.Success
114 case 300 <= rw.status && rw.status <= 399:
115 statusLogger = clog.Info
116 case 400 <= rw.status && rw.status <= 499:
117 statusLogger = clog.Warn
118 case 500 <= rw.status && rw.status <= 599:
119 statusLogger = clog.Error
120 }
121 lengthStr := englishPrinter.Sprint(rw.length)
122 statusLogger.Printf("%s %s %d %sB %v", r.Method, r.URL, rw.status, lengthStr, dur)
123 })
124 }
125
View as plain text