Files
devv-eve f864a07bd5 feat: add server Prometheus metrics endpoint
Add Prometheus metrics endpoint with local-bind listener support and baseline metrics collectors.
2026-04-28 14:29:01 +08:00

95 lines
2.3 KiB
Go

package metrics
import (
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus"
)
type HTTPMetrics struct {
requests *prometheus.CounterVec
duration *prometheus.HistogramVec
inFlight prometheus.Gauge
}
func NewHTTPMetrics() *HTTPMetrics {
return &HTTPMetrics{
requests: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "multica",
Subsystem: "http",
Name: "requests_total",
Help: "Total HTTP requests served by the API server.",
}, []string{"method", "route", "status"}),
duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "multica",
Subsystem: "http",
Name: "request_duration_seconds",
Help: "HTTP request duration observed by the API server.",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
}, []string{"method", "route", "status"}),
inFlight: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "multica",
Subsystem: "http",
Name: "in_flight_requests",
Help: "Current number of in-flight HTTP requests served by the API server.",
}),
}
}
func (m *HTTPMetrics) Collectors() []prometheus.Collector {
return []prometheus.Collector{m.requests, m.duration, m.inFlight}
}
func (m *HTTPMetrics) Middleware(next http.Handler) http.Handler {
if m == nil {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isHealthProbePath(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
m.inFlight.Inc()
defer m.inFlight.Dec()
start := time.Now()
ww := chimw.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(ww, r)
status := ww.Status()
if status == 0 {
status = http.StatusOK
}
labels := prometheus.Labels{
"method": r.Method,
"route": routePattern(r),
"status": strconv.Itoa(status),
}
m.requests.With(labels).Inc()
m.duration.With(labels).Observe(time.Since(start).Seconds())
})
}
func routePattern(r *http.Request) string {
if rctx := chi.RouteContext(r.Context()); rctx != nil {
if pattern := rctx.RoutePattern(); pattern != "" {
return pattern
}
}
return "unmatched"
}
func isHealthProbePath(path string) bool {
switch path {
case "/health", "/healthz", "/readyz":
return true
default:
return false
}
}