veylant/internal/metrics/middleware.go
2026-02-23 13:35:04 +01:00

67 lines
1.8 KiB
Go

package metrics
import (
"net/http"
"strconv"
"time"
)
// responseWriter wraps http.ResponseWriter to capture the status code written
// by downstream handlers.
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
// statusCode returns the captured status code, defaulting to 200 if WriteHeader
// was never called (Go's default behaviour).
func (rw *responseWriter) statusCode() string {
if rw.status == 0 {
return "200"
}
return strconv.Itoa(rw.status)
}
// Middleware records request_count, request_duration, and request_errors for
// every HTTP request. It must be added to the chi middleware chain after
// RequestID but before the route handlers.
func Middleware(provider string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wrapped, r)
status := wrapped.statusCode()
duration := time.Since(start).Seconds()
path := r.URL.Path
RequestsTotal.WithLabelValues(r.Method, path, status, provider).Inc()
RequestDuration.WithLabelValues(r.Method, path, status).Observe(duration)
// Count 4xx/5xx as errors.
code := wrapped.status
if code == 0 {
code = http.StatusOK
}
if code >= 400 {
errorType := "server_error"
if code == http.StatusUnauthorized {
errorType = "authentication_error"
} else if code == http.StatusTooManyRequests {
errorType = "rate_limit_error"
} else if code < 500 {
errorType = "client_error"
}
RequestErrors.WithLabelValues(r.Method, path, errorType).Inc()
}
})
}
}