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  }