log/access_logger.go (162 lines of code) (raw):
package log
import (
"bufio"
"net"
"net/http"
"sync/atomic"
"time"
"github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/labkit/correlation"
"gitlab.com/gitlab-org/labkit/mask"
"gitlab.com/gitlab-org/labkit/remoteip"
)
// AccessLogger will generate access logs for the service.
func AccessLogger(h http.Handler, opts ...AccessLoggerOption) http.Handler {
config := applyAccessLoggerOptions(opts)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var bodyLength atomic.Int64
r.Body = &bodyByteCounter{
body: r.Body,
count: &bodyLength,
}
lrw := newLoggingResponseWriter(w, &config, &bodyLength)
defer lrw.requestFinished(r)
h.ServeHTTP(lrw, r)
lrw.setStatus()
})
}
func newLoggingResponseWriter(rw http.ResponseWriter, config *accessLoggerConfig, bodyByteCount *atomic.Int64) notifiableResponseWriter {
out := loggingResponseWriter{
rw: rw,
started: time.Now(),
config: config,
bodyByteCount: bodyByteCount,
}
// If the underlying response writer supports hijacking,
// we need to ensure that we do too
if _, ok := rw.(http.Hijacker); ok {
return &hijackingResponseWriter{out}
}
return &out
}
// notifiableResponseWriter is a response writer that can be notified when the request is complete,
// via the requestFinished method.
type notifiableResponseWriter interface {
http.ResponseWriter
// requestFinished is called by the middleware when the request has completed
requestFinished(r *http.Request)
setStatus()
}
type loggingResponseWriter struct {
rw http.ResponseWriter
status int
wroteHeader bool
written int64
started time.Time
ttfb time.Duration
config *accessLoggerConfig
contentType string
bodyByteCount *atomic.Int64
}
func (l *loggingResponseWriter) Header() http.Header {
return l.rw.Header()
}
func (l *loggingResponseWriter) Write(data []byte) (int, error) {
if !l.wroteHeader {
l.WriteHeader(http.StatusOK)
}
n, err := l.rw.Write(data)
l.written += int64(n)
return n, err
}
func (l *loggingResponseWriter) WriteHeader(status int) {
if l.wroteHeader {
return
}
l.wroteHeader = true
l.status = status
l.contentType = l.Header().Get("Content-Type")
l.ttfb = time.Since(l.started)
l.rw.WriteHeader(status)
}
// Unwrap lets http.ResponseController get the underlying http.ResponseWriter,
// by implementing the [rwUnwrapper](https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/net/http/responsecontroller.go;l=42-44) interface.
func (l *loggingResponseWriter) Unwrap() http.ResponseWriter {
return l.rw
}
func (l *loggingResponseWriter) setStatus() {
if !l.wroteHeader {
// If upstream never called WriteHeader, the Go net/http server will
// respond with status 200 to the client. We should also log status 200
// in that case.
l.status = http.StatusOK
}
}
//nolint:cyclop
func (l *loggingResponseWriter) accessLogFields(r *http.Request) logrus.Fields {
duration := time.Since(l.started)
fields := l.config.extraFields(r)
fieldsBitMask := l.config.fields
// Optionally add built in fields
if fieldsBitMask&CorrelationID != 0 {
fields[correlation.FieldName] = correlation.ExtractFromContext(r.Context())
}
if fieldsBitMask&HTTPHost != 0 {
fields[httpHostField] = r.Host
}
if fieldsBitMask&HTTPRemoteIP != 0 {
fields[httpRemoteIPField] = l.getRemoteIP(r)
}
if fieldsBitMask&HTTPRemoteAddr != 0 {
fields[httpRemoteAddrField] = r.RemoteAddr
}
if fieldsBitMask&HTTPRequestMethod != 0 {
fields[httpRequestMethodField] = r.Method
}
if fieldsBitMask&HTTPURI != 0 {
fields[httpURIField] = mask.URL(r.RequestURI)
}
if fieldsBitMask&HTTPProto != 0 {
fields[httpProtoField] = r.Proto
}
if fieldsBitMask&HTTPResponseStatusCode != 0 {
fields[httpResponseStatusCodeField] = l.status
}
if fieldsBitMask&HTTPResponseSize != 0 {
fields[httpResponseSizeField] = l.written
}
if fieldsBitMask&HTTPRequestSize != 0 {
fields[httpRequestSizeField] = computeApproximateRequestSize(r, l.bodyByteCount.Load())
}
if fieldsBitMask&HTTPRequestReferrer != 0 {
fields[httpRequestReferrerField] = mask.URL(r.Referer())
}
if fieldsBitMask&HTTPUserAgent != 0 {
fields[httpUserAgentField] = r.UserAgent()
}
if fieldsBitMask&RequestDuration != 0 {
fields[requestDurationField] = int64(duration / time.Millisecond)
}
if fieldsBitMask&RequestTTFB != 0 && l.ttfb > 0 {
fields[requestTTFBField] = l.ttfb.Milliseconds()
}
if fieldsBitMask&System != 0 {
fields[systemField] = "http"
}
if fieldsBitMask&HTTPResponseContentType != 0 {
fields[httpResponseContentTypeField] = l.contentType
}
return fields
}
func (l *loggingResponseWriter) requestFinished(r *http.Request) {
l.config.logger.WithFields(l.accessLogFields(r)).Info("access")
}
func (l *loggingResponseWriter) getRemoteIP(r *http.Request) string {
sip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil || sip == "" {
return ""
}
if !l.config.xffAllowed(sip) {
return sip
}
parser := remoteip.NewRemoteIPParser(l.config.trustedProxies)
remote_ip, err := parser.GetIP(r)
if err == nil {
return remote_ip
}
return sip
}
// hijackingResponseWriter is like a loggingResponseWriter that supports the http.Hijacker interface.
type hijackingResponseWriter struct {
loggingResponseWriter
}
func (l *hijackingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// The only way to get here is through NewStatsCollectingResponseWriter(), which
// checks that this cast will be valid.
hijacker := l.rw.(http.Hijacker)
return hijacker.Hijack()
}