go-app/pkg/srv/http_log.go

88 lines
1.9 KiB
Go

package srv
import (
"bytes"
"context"
"errors"
"net/http"
"regexp"
"time"
"github.com/rs/zerolog"
)
var ExcludeFromLogging = regexp.MustCompile(`\/(ready|live|metrics)$`)
type LoggingResponseWriter struct {
http.ResponseWriter
statusCode int
body *bytes.Buffer
}
func loggingMiddleware(appCtx context.Context, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ExcludeFromLogging.Match([]byte(r.URL.Path)) {
next.ServeHTTP(w, r)
return
}
log := zerolog.Ctx(appCtx)
start := time.Now()
lrr := newLoggingResponseWriter(w)
next.ServeHTTP(lrr, r)
log.Debug().
Str("path", r.URL.Path).
Any("query", r.URL.Query()).
Int("statusCode", lrr.statusCode).
Str("protocol", r.Proto).
Str("remote", r.RemoteAddr).
Dur("duration", time.Since(start)).
Msg("http request served")
// Log response body
trcLog := log.Trace().
Str("path", r.URL.Path).
Int("statusCode", lrr.statusCode)
// Check if it's JSON
firstByte, err := lrr.body.ReadByte()
if err != nil {
trcLog.Err(errors.New("invalid response body")).Send()
return
}
lrr.body.UnreadByte()
if firstByte == '{' {
trcLog = trcLog.RawJSON("response", lrr.body.Bytes())
} else {
trcLog = trcLog.Bytes("response", lrr.body.Bytes())
}
trcLog.Msg("response payload")
})
}
// Implement Flush to support the http.Flusher interface
func (w *LoggingResponseWriter) Flush() {
if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func (w *LoggingResponseWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
func (w *LoggingResponseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func newLoggingResponseWriter(w http.ResponseWriter) *LoggingResponseWriter {
return &LoggingResponseWriter{
ResponseWriter: w, statusCode: http.StatusOK, body: bytes.NewBuffer(nil),
}
}