gitlab.com/gitlab-org/labkit@v1.21.0/log/access_logger.go (about) 1 package log 2 3 import ( 4 "bufio" 5 "net" 6 "net/http" 7 "time" 8 9 "github.com/sebest/xff" 10 "github.com/sirupsen/logrus" 11 "gitlab.com/gitlab-org/labkit/correlation" 12 "gitlab.com/gitlab-org/labkit/mask" 13 ) 14 15 // AccessLogger will generate access logs for the service. 16 func AccessLogger(h http.Handler, opts ...AccessLoggerOption) http.Handler { 17 config := applyAccessLoggerOptions(opts) 18 19 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 lrw := newLoggingResponseWriter(w, &config) 21 defer lrw.requestFinished(r) 22 23 h.ServeHTTP(lrw, r) 24 lrw.setStatus() 25 }) 26 } 27 28 func newLoggingResponseWriter(rw http.ResponseWriter, config *accessLoggerConfig) notifiableResponseWriter { 29 out := loggingResponseWriter{ 30 rw: rw, 31 started: time.Now(), 32 config: config, 33 } 34 35 // If the underlying response writer supports hijacking, 36 // we need to ensure that we do too 37 if _, ok := rw.(http.Hijacker); ok { 38 return &hijackingResponseWriter{out} 39 } 40 41 return &out 42 } 43 44 // notifiableResponseWriter is a response writer that can be notified when the request is complete, 45 // via the requestFinished method. 46 type notifiableResponseWriter interface { 47 http.ResponseWriter 48 49 // requestFinished is called by the middleware when the request has completed 50 requestFinished(r *http.Request) 51 setStatus() 52 } 53 54 type loggingResponseWriter struct { 55 rw http.ResponseWriter 56 status int 57 wroteHeader bool 58 written int64 59 started time.Time 60 ttfb time.Duration 61 config *accessLoggerConfig 62 contentType string 63 } 64 65 func (l *loggingResponseWriter) Header() http.Header { 66 return l.rw.Header() 67 } 68 69 func (l *loggingResponseWriter) Write(data []byte) (int, error) { 70 if !l.wroteHeader { 71 l.WriteHeader(http.StatusOK) 72 } 73 n, err := l.rw.Write(data) 74 75 l.written += int64(n) 76 return n, err 77 } 78 79 func (l *loggingResponseWriter) WriteHeader(status int) { 80 if l.wroteHeader { 81 return 82 } 83 l.wroteHeader = true 84 l.status = status 85 l.contentType = l.Header().Get("Content-Type") 86 l.ttfb = time.Since(l.started) 87 88 l.rw.WriteHeader(status) 89 } 90 91 // Unwrap lets http.ResponseController get the underlying http.ResponseWriter, 92 // by implementing the [rwUnwrapper](https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/net/http/responsecontroller.go;l=42-44) interface. 93 func (l *loggingResponseWriter) Unwrap() http.ResponseWriter { 94 return l.rw 95 } 96 97 func (l *loggingResponseWriter) setStatus() { 98 if !l.wroteHeader { 99 // If upstream never called WriteHeader, the Go net/http server will 100 // respond with status 200 to the client. We should also log status 200 101 // in that case. 102 l.status = http.StatusOK 103 } 104 } 105 106 //nolint:cyclop 107 func (l *loggingResponseWriter) accessLogFields(r *http.Request) logrus.Fields { 108 duration := time.Since(l.started) 109 110 fields := l.config.extraFields(r) 111 fieldsBitMask := l.config.fields 112 113 // Optionally add built in fields 114 if fieldsBitMask&CorrelationID != 0 { 115 fields[correlation.FieldName] = correlation.ExtractFromContext(r.Context()) 116 } 117 118 if fieldsBitMask&HTTPHost != 0 { 119 fields[httpHostField] = r.Host 120 } 121 122 if fieldsBitMask&HTTPRemoteIP != 0 { 123 fields[httpRemoteIPField] = l.getRemoteIP(r) 124 } 125 126 if fieldsBitMask&HTTPRemoteAddr != 0 { 127 fields[httpRemoteAddrField] = r.RemoteAddr 128 } 129 130 if fieldsBitMask&HTTPRequestMethod != 0 { 131 fields[httpRequestMethodField] = r.Method 132 } 133 134 if fieldsBitMask&HTTPURI != 0 { 135 fields[httpURIField] = mask.URL(r.RequestURI) 136 } 137 138 if fieldsBitMask&HTTPProto != 0 { 139 fields[httpProtoField] = r.Proto 140 } 141 142 if fieldsBitMask&HTTPResponseStatusCode != 0 { 143 fields[httpResponseStatusCodeField] = l.status 144 } 145 146 if fieldsBitMask&HTTPResponseSize != 0 { 147 fields[httpResponseSizeField] = l.written 148 } 149 150 if fieldsBitMask&HTTPRequestReferrer != 0 { 151 fields[httpRequestReferrerField] = mask.URL(r.Referer()) 152 } 153 154 if fieldsBitMask&HTTPUserAgent != 0 { 155 fields[httpUserAgentField] = r.UserAgent() 156 } 157 158 if fieldsBitMask&RequestDuration != 0 { 159 fields[requestDurationField] = int64(duration / time.Millisecond) 160 } 161 162 if fieldsBitMask&RequestTTFB != 0 && l.ttfb > 0 { 163 fields[requestTTFBField] = l.ttfb.Milliseconds() 164 } 165 166 if fieldsBitMask&System != 0 { 167 fields[systemField] = "http" 168 } 169 170 if fieldsBitMask&HTTPResponseContentType != 0 { 171 fields[httpResponseContentTypeField] = l.contentType 172 } 173 174 return fields 175 } 176 177 func (l *loggingResponseWriter) requestFinished(r *http.Request) { 178 l.config.logger.WithFields(l.accessLogFields(r)).Info("access") 179 } 180 181 func (l *loggingResponseWriter) getRemoteIP(r *http.Request) string { 182 remoteAddr := xff.GetRemoteAddrIfAllowed(r, l.config.xffAllowed) 183 host, _, err := net.SplitHostPort(remoteAddr) 184 if err != nil { 185 return r.RemoteAddr 186 } 187 188 return host 189 } 190 191 // hijackingResponseWriter is like a loggingResponseWriter that supports the http.Hijacker interface. 192 type hijackingResponseWriter struct { 193 loggingResponseWriter 194 } 195 196 func (l *hijackingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 197 // The only way to get here is through NewStatsCollectingResponseWriter(), which 198 // checks that this cast will be valid. 199 hijacker := l.rw.(http.Hijacker) 200 return hijacker.Hijack() 201 }