diff --git a/.gitignore b/.gitignore index bfa678e..603d510 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ Makefile NOTES.md tests configs -/config \ No newline at end of file +/config +compose.yaml \ No newline at end of file diff --git a/go.mod b/go.mod index f53fad8..087d6ec 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/mux v1.8.1 + github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 golang.org/x/oauth2 v0.24.0 gopkg.in/yaml.v3 v3.0.1 @@ -20,7 +21,15 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // 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.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 0721c90..a866903 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +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/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -15,12 +19,24 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -40,6 +56,8 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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/prometheus.go b/internal/prometheus.go new file mode 100644 index 0000000..a1c0273 --- /dev/null +++ b/internal/prometheus.go @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "net/http" + "strconv" +) + +type PrometheusRoute struct { + name string + path string +} + +var totalRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Number of get requests.", + }, + []string{"path"}, +) + +var responseStatus = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "response_status", + Help: "Status of HTTP response", + }, + []string{"status"}, +) + +var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "http_response_time_seconds", + Help: "Duration of HTTP requests.", +}, []string{"path"}) + +func prometheusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + path, _ := route.GetPathTemplate() + timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) + + responseStatus.WithLabelValues(strconv.Itoa(http.StatusOK)).Inc() + totalRequests.WithLabelValues(path).Inc() + + timer.ObserveDuration() + next.ServeHTTP(w, r) + }) + +} diff --git a/internal/proxy.go b/internal/proxy.go index 92d82c3..7188603 100644 --- a/internal/proxy.go +++ b/internal/proxy.go @@ -29,7 +29,7 @@ import ( // ProxyHandler proxies requests to the backend func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL, r.UserAgent()) + logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL.Path, r.UserAgent()) // Check Method if is allowed if len(proxyRoute.methods) > 0 { if !slices.Contains(proxyRoute.methods, r.Method) { diff --git a/internal/route.go b/internal/route.go index b333031..83b0987 100644 --- a/internal/route.go +++ b/internal/route.go @@ -20,9 +20,17 @@ import ( "github.com/jkaninda/goma-gateway/internal/middleware" "github.com/jkaninda/goma-gateway/pkg/logger" "github.com/jkaninda/goma-gateway/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "time" ) +func init() { + _ = prometheus.Register(totalRequests) + _ = prometheus.Register(responseStatus) + _ = prometheus.Register(httpDuration) +} + // Initialize the routes func (gatewayServer GatewayServer) Initialize() *mux.Router { gateway := gatewayServer.gateway @@ -32,11 +40,18 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { DisableRouteHealthCheckError: gateway.DisableRouteHealthCheckError, Routes: gateway.Routes, } + if gateway.EnableMetrics { + // Prometheus endpoint + r.Path("/metrics").Handler(promhttp.Handler()) + r.Use(prometheusMiddleware) + } + // Routes health check if !gateway.DisableHealthCheckStatus { r.HandleFunc("/healthz", heath.HealthCheckHandler).Methods("GET") r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") } + // Health check r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET") @@ -223,6 +238,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } else { router.PathPrefix("").Handler(proxyRoute.ProxyHandler()) } + } else { logger.Error("Error, path is empty in route %s", route.Name) logger.Error("Route path ignored: %s", route.Path) diff --git a/internal/types.go b/internal/types.go index 5a352f7..7fa4700 100644 --- a/internal/types.go +++ b/internal/types.go @@ -196,6 +196,7 @@ type Gateway struct { DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"` // DisableKeepAlive allows enabling and disabling KeepALive server DisableKeepAlive bool `yaml:"disableKeepAlive"` + EnableMetrics bool `yaml:"enableMetrics"` // InterceptErrors holds the status codes to intercept the error from backend InterceptErrors []int `yaml:"interceptErrors"` // Cors holds proxy global cors