From 58c2f11f1e84b4919f0322efefa182d45640d259 Mon Sep 17 00:00:00 2001 From: Nickolay Kondratenko Date: Mon, 26 May 2025 23:41:16 +0300 Subject: [PATCH 1/2] prometheus middleware --- compose.yml | 1 + go.mod | 12 ++++++ go.sum | 18 +++++++++ internal/metrics/middleware.go | 63 +++++++++++++++++++++++++++++++ internal/router/router.go | 18 ++++++--- internal/server_app/server_app.go | 11 +++++- 6 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 internal/metrics/middleware.go diff --git a/compose.yml b/compose.yml index 6d22ba5..440f740 100644 --- a/compose.yml +++ b/compose.yml @@ -7,6 +7,7 @@ services: - MONGO_URL=mongodb://mongo:mongo@mongo:27017 - MONGO_DB=connections - MONGO_COLLECTION=connections + - PROMETHEUS_ENABLED=true depends_on: - mongo client_1: &common_client diff --git a/go.mod b/go.mod index 469c406..ff59c0d 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,24 @@ require ( require github.com/stretchr/testify v1.10.0 +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // 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.32.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.22.0 github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/go.sum b/go.sum index 3f1af6b..abf017f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -8,8 +12,18 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -46,6 +60,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -58,6 +74,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/metrics/middleware.go b/internal/metrics/middleware.go new file mode 100644 index 0000000..bcecbb8 --- /dev/null +++ b/internal/metrics/middleware.go @@ -0,0 +1,63 @@ +package metrics + +import ( + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + metricsPath = "/metrics" +) + +var httpRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "Duration of HTTP requests", + Buckets: prometheus.DefBuckets, + }, + []string{"path", "method"}, +) + +type Middleware struct { + enabled bool +} + +func NewMiddleware(enabled bool) *Middleware { + if enabled { + prometheus.MustRegister(httpRequestDuration) + } + + return &Middleware{enabled: enabled} +} + +func (m *Middleware) Routes(router *http.ServeMux) { + if !m.enabled { + return + } + + router.Handle(metricsPath, promhttp.Handler()) +} + +func (m *Middleware) Middleware(next http.Handler) http.Handler { + if !m.enabled { + return next + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + m.observeDuration(start, r.URL.Path, r.Method) + }) +} + +func (m *Middleware) observeDuration(start time.Time, path, method string) { + if !m.enabled { + return + } + + duration := time.Since(start).Seconds() + httpRequestDuration.WithLabelValues(path, method).Observe(duration) +} diff --git a/internal/router/router.go b/internal/router/router.go index 3a3160c..e7fa2f4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,6 +2,7 @@ package router import ( "bytes" + "connection_request_server/internal/metrics" "fmt" "io" "net/http" @@ -24,8 +25,9 @@ type APIHandlers interface { } type Config struct { - APIHandlers APIHandlers - Log *zap.Logger + APIHandlers APIHandlers + MetricsMiddleware *metrics.Middleware + Log *zap.Logger } type ResponseWriterWrapper struct { @@ -77,11 +79,17 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) { func New(config Config) *http.ServeMux { router := http.NewServeMux() - router.Handle(apiPath+connectPath, LoggingMiddleware(config.Log, http.HandlerFunc(config.APIHandlers.Connect))) - router.Handle(apiPath+disconnectPath, LoggingMiddleware(config.Log, http.HandlerFunc(config.APIHandlers.Disconnect))) - router.Handle(apiPath+heartbeatPath, LoggingMiddleware(config.Log, http.HandlerFunc(config.APIHandlers.Heartbeat))) + router.Handle(apiPath+connectPath, applyMiddlewares(config, config.APIHandlers.Connect)) + router.Handle(apiPath+disconnectPath, applyMiddlewares(config, config.APIHandlers.Disconnect)) + router.Handle(apiPath+heartbeatPath, applyMiddlewares(config, config.APIHandlers.Heartbeat)) router.Handle(healthPath, http.HandlerFunc(healthCheckHandler)) + config.MetricsMiddleware.Routes(router) + return router } + +func applyMiddlewares(config Config, handler func(w http.ResponseWriter, r *http.Request)) http.Handler { + return config.MetricsMiddleware.Middleware(LoggingMiddleware(config.Log, http.HandlerFunc(handler))) +} diff --git a/internal/server_app/server_app.go b/internal/server_app/server_app.go index 48f56c6..c1eaa17 100644 --- a/internal/server_app/server_app.go +++ b/internal/server_app/server_app.go @@ -1,11 +1,13 @@ package serverapp import ( + "connection_request_server/internal/metrics" adapter "connection_request_server/internal/mongo_adapter" "connection_request_server/internal/router" "connection_request_server/internal/service" "connection_request_server/pkg/server" "os" + "strconv" "go.uber.org/zap" ) @@ -31,7 +33,14 @@ func Run() { logger.Sugar().Panicf("failed to create mongo client: %v, url: %s", err, mongoURL) } appService := service.New(service.Config{Repository: repositoryClient}) - appRouter := router.New(router.Config{APIHandlers: appService, Log: logger}) + + enabledStr := os.Getenv("PROMETHEUS_ENABLED") + enabled, err := strconv.ParseBool(enabledStr) + if err != nil { + enabled = false + } + metricsMiddleware := metrics.NewMiddleware(enabled) + appRouter := router.New(router.Config{APIHandlers: appService, Log: logger, MetricsMiddleware: metricsMiddleware}) serverHostname := os.Getenv("SERVER_HOSTNAME") serverPort := os.Getenv("SERVER_PORT") From f051fe897de80b3444fb77639cfa434af890028d Mon Sep 17 00:00:00 2001 From: Nickolay Kondratenko Date: Mon, 26 May 2025 23:50:47 +0300 Subject: [PATCH 2/2] fix the tests --- internal/router/router.go | 8 ++++++-- internal/router/router_test.go | 11 +++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/router/router.go b/internal/router/router.go index e7fa2f4..ee503e4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,7 +2,6 @@ package router import ( "bytes" - "connection_request_server/internal/metrics" "fmt" "io" "net/http" @@ -24,9 +23,14 @@ type APIHandlers interface { Heartbeat(w http.ResponseWriter, r *http.Request) } +type MetricsMiddleware interface { + Middleware(next http.Handler) http.Handler + Routes(router *http.ServeMux) +} + type Config struct { APIHandlers APIHandlers - MetricsMiddleware *metrics.Middleware + MetricsMiddleware MetricsMiddleware Log *zap.Logger } diff --git a/internal/router/router_test.go b/internal/router/router_test.go index a17a9d5..789c68b 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -2,6 +2,7 @@ package router import ( "bytes" + "connection_request_server/internal/metrics" "net/http" "testing" @@ -16,8 +17,9 @@ func TestRouter(t *testing.T) { log := zap.NewNop() r := New(Config{ - APIHandlers: mock, - Log: log, + APIHandlers: mock, + Log: log, + MetricsMiddleware: metrics.NewMiddleware(false), }) tests := []struct { @@ -47,8 +49,9 @@ func TestHealth(t *testing.T) { log := zap.NewNop() r := New(Config{ - APIHandlers: mock, - Log: log, + APIHandlers: mock, + Log: log, + MetricsMiddleware: metrics.NewMiddleware(false), }) req := httptest.NewRequest(http.MethodGet, "/health", bytes.NewBufferString(""))