204 lines
3.9 KiB
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]
|
|
}
|