package middleware import ( "bufio" "fmt" "net" "net/http" "regexp" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( httpRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "streamapp_http_requests_total", Help: "Total number of HTTP requests.", }, []string{"method", "path", "status"}, ) httpRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "streamapp_http_request_duration_seconds", Help: "Duration of HTTP requests in seconds.", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}, ) ) // Metrics is an HTTP middleware that records request count and duration. func Metrics(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Wrap response writer to capture status code. wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) duration := time.Since(start).Seconds() path := sanitizePath(r.URL.Path) httpRequestsTotal.WithLabelValues(r.Method, path, strconv.Itoa(wrapped.status)).Inc() httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration) }) } type statusWriter struct { http.ResponseWriter status int written bool } func (w *statusWriter) WriteHeader(status int) { if !w.written { w.status = status w.written = true } w.ResponseWriter.WriteHeader(status) } // Hijack implements http.Hijacker, required for WebSocket upgrades. func (w *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := w.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, fmt.Errorf("underlying ResponseWriter does not implement http.Hijacker") } var ( uuidRegex = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) numericRegex = regexp.MustCompile(`/\d+(/|$)`) ) // sanitizePath normalizes URL paths to avoid high-cardinality labels. // Replaces dynamic segments (UUIDs, numeric IDs) with placeholders. func sanitizePath(path string) string { path = uuidRegex.ReplaceAllString(path, "{id}") path = numericRegex.ReplaceAllStringFunc(path, func(m string) string { if m[len(m)-1] == '/' { return "/{id}/" } return "/{id}" }) return path }