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), } }