readysite / pkg / application / middleware.go
2.4 KB
middleware.go
package application

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
	"time"
)

// RequestLog represents a logged request.
type RequestLog struct {
	Timestamp  string `json:"timestamp"`
	Method     string `json:"method"`
	Path       string `json:"path"`
	Status     int    `json:"status"`
	Duration   string `json:"duration"`
	DurationMs int64  `json:"duration_ms"`
	IP         string `json:"ip"`
	UserAgent  string `json:"user_agent,omitempty"`
}

// responseWriter wraps http.ResponseWriter to capture status code.
// It also implements http.Flusher to support SSE streaming.
type responseWriter struct {
	http.ResponseWriter
	status int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.status = code
	rw.ResponseWriter.WriteHeader(code)
}

// Flush implements http.Flusher for SSE streaming support.
func (rw *responseWriter) Flush() {
	if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
		flusher.Flush()
	}
}

// LoggingMiddleware returns middleware that logs all requests.
func LoggingMiddleware(next http.Handler) http.Handler {
	isProduction := os.Getenv("ENV") == "production"

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// Wrap response writer to capture status
		wrapped := &responseWriter{ResponseWriter: w, status: http.StatusOK}

		// Process request
		next.ServeHTTP(wrapped, r)

		// Calculate duration
		duration := time.Since(start)

		// Get client IP
		ip := r.RemoteAddr
		if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
			ip = forwarded
		}

		// Log request
		if isProduction {
			// Structured JSON logging for production
			logEntry := RequestLog{
				Timestamp:  time.Now().UTC().Format(time.RFC3339),
				Method:     r.Method,
				Path:       r.URL.Path,
				Status:     wrapped.status,
				Duration:   duration.String(),
				DurationMs: duration.Milliseconds(),
				IP:         ip,
				UserAgent:  r.UserAgent(),
			}
			if data, err := json.Marshal(logEntry); err == nil {
				log.Println(string(data))
			}
		} else {
			// Human-readable logging for development
			log.Printf("%s %s %d %s", r.Method, r.URL.Path, wrapped.status, duration)
		}
	})
}

// WithLogging returns an option that enables request logging.
// Note: logging is always enabled by default. This option is kept for API
// compatibility but has no additional effect.
func WithLogging() Option {
	return func(app *App) {}
}
← Back