package logger import ( "io" "log" "net/http" "net/url" "strings" "time" ) // DefaultOptions represents default logger middleware options var DefaultOptions = Options( WithBody(), SetMaxBodySize(1*1024*1024), SetIPFn(defaultIPFn), SetLogHandler(DefaultLogHandler), ) // Options turns a list of option instances into an option func Options(opts ...Option) Option { return func(l *logger) { for _, opt := range opts { opt(l) } } } // Option configures logger middleware type Option func(l *logger) // WithBody enables request body logging func WithBody() Option { return func(l *logger) { l.logBody = true } } // WithoutBody disables request body logging func WithoutBody() Option { return func(l *logger) { l.logBody = false } } // SetMaxBodySize sets maximum request body size func SetMaxBodySize(size int) Option { return func(l *logger) { l.maxBodySize = size } } // SetIPFn sets function that extracts ip from request func SetIPFn(fn func(r *http.Request) string) Option { return func(l *logger) { l.ipFn = fn } } // SetUserFn sets function that extracts user from request func SetUserFn(fn func(r *http.Request) string) Option { return func(l *logger) { l.userFn = fn } } // SetSanitizeFn sets function that sanitizes request query or body func SetSanitizeFn(fn func(input string) string) Option { return func(l *logger) { l.sanitizeFn = fn } } // SetLogHandler sets log handler func SetLogHandler(fn func(entry LogEntry)) Option { return func(l *logger) { l.logHandler = fn } } // LogEntry is a http log entry type LogEntry struct { Method string RawURL string Body string RemoteIP string StatusCode int Written int Duration time.Duration User string TraceID string } type logger struct { logBody bool maxBodySize int ipFn func(r *http.Request) string userFn func(r *http.Request) string sanitizeFn func(input string) string logHandler func(entry LogEntry) } func (s *logger) body(r *http.Request) string { if !s.logBody { return "" } rdr, body, hasMore, err := peek(r.Body, int64(s.maxBodySize)) if err != nil { return "" } r.Body = io.NopCloser(rdr) if len(body) > 0 { body = strings.ReplaceAll(body, "\n", " ") body = regexpMultiWhitespace.ReplaceAllString(body, " ") } if hasMore { body += "..." } return body } // Middleware is a http logger middleware func Middleware(opts ...Option) func(http.Handler) http.Handler { l := &logger{} opts = append([]Option{DefaultOptions}, opts...) for _, opt := range opts { opt(l) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ww := newTrackingResponseWriter(w) body := l.body(r) if l.sanitizeFn != nil { body = l.sanitizeFn(body) } startTS := time.Now() defer func() { stopTS := time.Now() query := r.URL.String() if qun, err := url.QueryUnescape(query); err == nil { query = qun } if l.sanitizeFn != nil { query = l.sanitizeFn(query) } var ip string if l.ipFn != nil { ip = l.ipFn(r) } var user string if l.userFn != nil { user = l.userFn(r) } entry := LogEntry{ Method: r.Method, RawURL: query, RemoteIP: ip, Body: body, StatusCode: ww.status, Written: ww.size, Duration: stopTS.Sub(startTS), User: user, TraceID: r.Header.Get("X-Request-ID"), } l.logHandler(entry) }() next.ServeHTTP(ww, r) }) } } // DefaultLogHandler is a default log handler func DefaultLogHandler(entry LogEntry) { // nolint:gocritic // For backwards compatibility log.Printf( "%s - %s - %s - %d (%d) - %v", entry.Method, entry.RawURL, entry.RemoteIP, entry.StatusCode, entry.Written, entry.Duration, ) } func defaultIPFn(r *http.Request) string { return strings.Split(r.RemoteAddr, ":")[0] }