mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
Add Prometheus metrics endpoint with local-bind listener support and baseline metrics collectors.
95 lines
2.3 KiB
Go
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
|
|
}
|
|
}
|