middleware/logger/logger.go

204 lines
3.9 KiB
Go

package logger
import (
"io/ioutil"
"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 = ioutil.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]
}