diff --git a/go.mod b/go.mod index 9735609..e04b966 100644 --- a/go.mod +++ b/go.mod @@ -11,4 +11,17 @@ require ( github.com/lmittmann/tint v1.0.7 ) -require github.com/cespare/xxhash/v2 v2.3.0 // indirect +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.30.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/prometheus/client_golang v1.22.0 +) diff --git a/go.sum b/go.sum index a5e42e0..bdbb67b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -18,5 +20,19 @@ github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc= github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/internal/middleware/prometheus.go b/internal/middleware/prometheus.go new file mode 100644 index 0000000..850b2c1 --- /dev/null +++ b/internal/middleware/prometheus.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "net/http" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + httpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "path", "status"}, + ) + + httpRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "Duration of HTTP requests", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "path", "status"}, + ) +) + +func init() { + prometheus.MustRegister(httpRequestsTotal, httpRequestDuration) +} + +func PrometheusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rr := &responseRecorder{ResponseWriter: w, statusCode: 200} + next.ServeHTTP(rr, r) + duration := time.Since(start) + + method := r.Method + path := r.URL.Path + status := strconv.Itoa(rr.statusCode) + + httpRequestsTotal.WithLabelValues(method, path, status).Inc() + httpRequestDuration.WithLabelValues(method, path, status).Observe(duration.Seconds()) + }) +} + +type responseRecorder struct { + http.ResponseWriter + statusCode int +} + +func (rr *responseRecorder) WriteHeader(code int) { + rr.statusCode = code + rr.ResponseWriter.WriteHeader(code) +} diff --git a/internal/server/router.go b/internal/server/router.go index 82304fe..d8fc397 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -11,8 +11,10 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" + "github.com/prometheus/client_golang/prometheus/promhttp" "api/internal/handler" + app_middleware "api/internal/middleware" ) func getAllowedOrigins() []string { @@ -29,6 +31,7 @@ func NewRouter() { r := chi.NewRouter() // Middleware + r.Use(app_middleware.PrometheusMiddleware) r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) @@ -43,6 +46,9 @@ func NewRouter() { MaxAge: 300, })) + // Prometheus + r.Handle("/metrics", promhttp.Handler()) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{