package http import ( "bytes" "context" "fmt" "io" "net/http" "regexp" "time" "github.com/rs/zerolog" "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" ) 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 { appConfig := config.MustFromCtx(appCtx) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ExcludeFromLogging.Match([]byte(r.URL.Path)) { next.ServeHTTP(w, r) return } // User-configurable logging exclusions for _, re := range appConfig.HTTP.GetExcludeRegexps() { if re.MatchString(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 with body if not 204 if lrr.statusCode == http.StatusNoContent { trcLog := log.Trace(). Str("path", r.URL.Path). Int("statusCode", lrr.statusCode) trcLog.Msg("http response (no content)") // Explicitly log 204 return // No body to log for 204 No Content } trcLog := log.Trace(). Str("path", r.URL.Path). Int("statusCode", lrr.statusCode) firstByte, err := lrr.body.ReadByte() if err != nil { if err == io.EOF { // Body is empty, which might be valid for some non-204 responses. trcLog.Msg("http response (empty body)") } else { // Other error reading the body. Wrap the original error for context. trcLog.Err(fmt.Errorf("error reading response body: %w", err)).Send() } return // No further body processing if there was an error or it was empty } lrr.body.UnreadByte() // Put the byte back for Bytes() to read if firstByte == '{' { trcLog = trcLog.RawJSON("response", lrr.body.Bytes()) } else { trcLog = trcLog.Bytes("response", lrr.body.Bytes()) } trcLog.Msg("http response") }) } // Flush implements the http.Flusher interface to allow flushing buffered data. 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), } }