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.
137 lines
3.5 KiB
Go
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
|
|
}
|
|
}
|
|
}
|