From 3cff30ea895944f6e155ff7cfcc3914ac62d9b23 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 29 Jan 2026 09:49:56 -0500 Subject: [PATCH] feat(middleware): add request tracing and logging middleware - Add RequestTracingMiddleware for comprehensive HTTP request logging - Log request ID, timestamp, method, path, user info, duration, status code - Generate and propagate unique request IDs for tracing - Structured JSON logging for easy parsing and analysis - Capture request body, headers, query params, and user context --- internal/middleware/request_tracing.go | 122 +++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 internal/middleware/request_tracing.go diff --git a/internal/middleware/request_tracing.go b/internal/middleware/request_tracing.go new file mode 100644 index 0000000..c6549fb --- /dev/null +++ b/internal/middleware/request_tracing.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "bookmann/internal/config" + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +type responseWriter struct { + http.ResponseWriter + size int +} + +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().Writer, + } + c.Response().Writer = 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: c.Response().Status, + ResponseSize: int64(recorder.size), + } + + if err != nil { + logEntry.Error = err.Error() + } + + logJSON, _ := json.Marshal(logEntry) + log.Printf("[REQUEST] %s", string(logJSON)) + + return err + } + } +}