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