Files
bookhoard/internal/middleware/request_tracing.go
T
john-okeefe 994afe8250 fix(middleware): improve HTTP status code tracking in request tracing
Enhanced the responseWriter wrapper to properly capture HTTP status codes
by implementing WriteHeader method and storing status code in the wrapper
struct. This ensures accurate status logging in request traces.

Changes:
- Added status field to responseWriter struct to track HTTP status codes
- Implemented WriteHeader method to capture status when written
- Added Hijack method pass-through for WebSocket/upgrade support
- Updated request logging to use captured status from recorder instead of
  accessing Echo's internal Response object

This fix addresses potential issues where status codes were not being
properly captured in request logs, particularly for error responses and
non-2xx status codes.
2026-03-06 14:26:33 -05:00

137 lines
3.5 KiB
Go

package middleware
import (
"bookhoard/internal/config"
"bufio"
"bytes"
"encoding/json"
"io"
"log"
"net"
"net/http"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
)
type responseWriter struct {
http.ResponseWriter
size int
status int
}
func (r *responseWriter) WriteHeader(statusCode int) {
r.status = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// Pass through Hijacker if underlying writer supports it
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, http.ErrNotSupported
}
func (r *responseWriter) Write(b []byte) (int, error) {
size, err := r.ResponseWriter.Write(b)
r.size += size
return size, err
}
type RequestLogEntry struct {
RequestID string `json:"request_id"`
Timestamp time.Time `json:"timestamp"`
Method string `json:"method"`
Path string `json:"path"`
QueryParams map[string]string `json:"query_params,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Body interface{} `json:"body,omitempty"`
UserID string `json:"user_id,omitempty"`
UserRole string `json:"user_role,omitempty"`
UserEmail string `json:"user_email,omitempty"`
RemoteAddr string `json:"remote_addr"`
UserAgent string `json:"user_agent,omitempty"`
Duration time.Duration `json:"duration"`
StatusCode int `json:"status_code"`
ResponseSize int64 `json:"response_size"`
Error string `json:"error,omitempty"`
}
func RequestTracingMiddleware(cfg *config.Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
start := time.Now()
requestID := c.Response().Header().Get(echo.HeaderXRequestID)
if requestID == "" {
requestID = uuid.New().String()
c.Response().Header().Set(echo.HeaderXRequestID, requestID)
}
recorder := &responseWriter{
ResponseWriter: c.Response(),
}
c.SetResponse(recorder)
var body interface{}
if c.Request().Body != nil && c.Request().Method != "GET" {
bodyBytes, _ := io.ReadAll(c.Request().Body)
c.Request().Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
if len(bodyBytes) > 0 && len(bodyBytes) < 10000 {
json.Unmarshal(bodyBytes, &body)
}
}
headers := make(map[string]string)
for k, v := range c.Request().Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
userID, _ := c.Get("user_id").(string)
userRole, _ := c.Get("user_role").(string)
userEmail, _ := c.Get("user_email").(string)
err := next(c)
duration := time.Since(start)
queryParams := make(map[string]string)
for k, v := range c.QueryParams() {
if len(v) > 0 {
queryParams[k] = v[0]
}
}
logEntry := RequestLogEntry{
RequestID: requestID,
Timestamp: start,
Method: c.Request().Method,
Path: c.Request().URL.Path,
QueryParams: queryParams,
Headers: headers,
Body: body,
UserID: userID,
UserRole: userRole,
UserEmail: userEmail,
RemoteAddr: c.RealIP(),
UserAgent: c.Request().UserAgent(),
Duration: duration,
StatusCode: recorder.status,
ResponseSize: int64(recorder.size),
}
if err != nil {
logEntry.Error = err.Error()
}
logJSON, _ := json.Marshal(logEntry)
log.Printf("[REQUEST] %s", string(logJSON))
return err
}
}
}