Merge pull request #82 from jkaninda/feature/metrics

Feature/metrics
This commit is contained in:
2024-11-10 17:16:48 +01:00
committed by GitHub
14 changed files with 316 additions and 38 deletions

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ Makefile
NOTES.md
tests
configs
/config
/config
compose.yaml

45
cmd/config/check.go Normal file
View File

@@ -0,0 +1,45 @@
/*
* 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 config
import (
pkg "github.com/jkaninda/goma-gateway/internal"
"github.com/spf13/cobra"
"log"
)
var CheckConfigCmd = &cobra.Command{
Use: "check",
Short: "Check Goma Gateway configuration file",
Run: func(cmd *cobra.Command, args []string) {
configFile, _ := cmd.Flags().GetString("config")
if configFile == "" {
log.Fatalln("no config file specified")
}
err := pkg.CheckConfig(configFile)
if err != nil {
log.Fatalf(" Error checking config file: %s\n", err)
}
log.Println("Goma Gateway configuration file checked successfully")
},
}
func init() {
CheckConfigCmd.Flags().StringP("config", "c", "", "Path to the configuration filename")
}

View File

@@ -17,8 +17,8 @@ limitations under the License.
package config
import (
"github.com/jkaninda/goma-gateway/pkg/logger"
"github.com/spf13/cobra"
"log"
)
var Cmd = &cobra.Command{
@@ -28,7 +28,7 @@ var Cmd = &cobra.Command{
if len(args) == 0 {
return
} else {
logger.Fatal(`"config" accepts no argument %q`, args)
log.Fatalf("Config accepts no argument %q", args)
}
@@ -37,4 +37,5 @@ var Cmd = &cobra.Command{
func init() {
Cmd.AddCommand(InitConfigCmd)
Cmd.AddCommand(CheckConfigCmd)
}

9
go.mod
View File

@@ -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
)

18
go.sum
View File

@@ -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=

67
internal/checkConfig.go Normal file
View File

@@ -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 (
"fmt"
"github.com/jkaninda/goma-gateway/util"
"gopkg.in/yaml.v3"
"log"
"os"
)
func CheckConfig(fileName string) error {
if !util.FileExists(fileName) {
return fmt.Errorf("config file not found: %s", fileName)
}
buf, err := os.ReadFile(fileName)
if err != nil {
return err
}
c := &GatewayConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return fmt.Errorf("parsing the configuration file %q: %w", fileName, err)
}
gateway := &GatewayServer{
ctx: nil,
version: c.Version,
gateway: c.GatewayConfig,
middlewares: c.Middlewares,
}
for index, route := range gateway.gateway.Routes {
if len(route.Name) == 0 {
log.Printf("Warning: route name is empty, index: [%d]", index)
}
if route.Destination == "" && len(route.Backends) == 0 {
log.Printf("Error: no destination or backends specified for route: %s | index: [%d] \n", route.Name, index)
}
}
//Check middleware
for index, mid := range c.Middlewares {
if util.HasWhitespace(mid.Name) {
log.Printf("Warning: Middleware contains whitespace: %s | index: [%d], please remove whitespace characters\n", mid.Name, index)
}
}
log.Printf("Routes count=%d Middlewares count=%d\n", len(gateway.gateway.Routes), len(gateway.middlewares))
return nil
}

View File

@@ -48,6 +48,7 @@ func (GatewayServer) Config(configFile string) (*GatewayServer, error) {
}
return &GatewayServer{
ctx: nil,
version: c.Version,
gateway: c.GatewayConfig,
middlewares: c.Middlewares,
}, nil
@@ -122,7 +123,7 @@ func initConfig(configFile string) {
GatewayConfig: Gateway{
WriteTimeout: 15,
ReadTimeout: 15,
IdleTimeout: 60,
IdleTimeout: 30,
AccessLog: "/dev/Stdout",
ErrorLog: "/dev/stderr",
DisableRouteHealthCheckError: false,
@@ -140,11 +141,14 @@ func initConfig(configFile string) {
Routes: []Route{
{
Name: "Public",
Path: "/public",
Path: "/",
Methods: []string{"GET"},
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
HealthCheck: RouteHealthCheck{
Path: "/",
HealthyStatuses: []int{200, 404},
},
Middlewares: []string{"api-forbidden-paths"},
},
{
@@ -152,7 +156,7 @@ func initConfig(configFile string) {
Path: "/protected",
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
HealthCheck: RouteHealthCheck{},
Cors: Cors{
Origins: []string{"http://localhost:3000", "https://dev.example.com"},
Headers: map[string]string{
@@ -164,12 +168,35 @@ func initConfig(configFile string) {
Middlewares: []string{"basic-auth", "api-forbidden-paths"},
},
{
Name: "Hostname example",
Hosts: []string{"example.com", "example.localhost"},
Path: "/",
Destination: "https://example.com",
Path: "/",
Name: "Hostname and load balancing example",
Hosts: []string{"example.com", "example.localhost"},
InterceptErrors: []int{404, 405, 500},
RateLimit: 60,
Backends: []string{
"https://example.com",
"https://example2.com",
"https://example4.com",
},
Rewrite: "/",
HealthCheck: "",
HealthCheck: RouteHealthCheck{},
},
{
Path: "/loadbalancing",
Name: "loadBalancing example",
Hosts: []string{"example.com", "example.localhost"},
Backends: []string{
"https://example.com",
"https://example2.com",
"https://example4.com",
},
Rewrite: "/",
HealthCheck: RouteHealthCheck{
Path: "/health/live",
HealthyStatuses: []int{200, 404},
Interval: 30,
Timeout: 10,
},
},
},
},
@@ -207,7 +234,6 @@ func initConfig(configFile string) {
"/swagger-ui/*",
"/v2/swagger-ui/*",
"/api-docs/*",
"/internal/*",
"/actuator/*",
},
},
@@ -234,12 +260,11 @@ func initConfig(configFile string) {
Name: "oauth-authentik",
Type: OAuth,
Paths: []string{
"/protected",
"/example-of-oauth",
"/*",
},
Rule: OauthRulerMiddleware{
ClientID: "xxx",
ClientSecret: "xxx",
ClientID: "xxxx",
ClientSecret: "xxxx",
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"email", "openid"},
JWTSecret: "your-strong-jwt-secret | It's optional",

View File

@@ -72,8 +72,8 @@ func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r *
for _, route := range heathRoute.Routes {
go func() {
defer wg.Done()
if route.HealthCheck != "" {
err := healthCheck(route.Destination + route.HealthCheck)
if route.HealthCheck.Path != "" {
err := healthCheck(route.Destination+route.HealthCheck.Path, route.HealthCheck.HealthyStatuses)
if err != nil {
if heathRoute.DisableRouteHealthCheckError {
routes = append(routes, HealthCheckRouteResponse{Name: route.Name, Status: "unhealthy", Error: "Route healthcheck errors disabled"})

View File

@@ -21,9 +21,10 @@ import (
"io"
"net/http"
"net/url"
"slices"
)
func healthCheck(healthURL string) error {
func healthCheck(healthURL string, healthyStatuses []int) error {
healthCheckURL, err := url.Parse(healthURL)
if err != nil {
return fmt.Errorf("error parsing HealthCheck URL: %v ", err)
@@ -45,10 +46,16 @@ func healthCheck(healthURL string) error {
if err != nil {
}
}(healthResp.Body)
if healthResp.StatusCode >= 400 {
logger.Debug("Error performing HealthCheck request: %v ", err)
return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode)
if len(healthyStatuses) > 0 {
if !slices.Contains(healthyStatuses, healthResp.StatusCode) {
logger.Error("Error performing HealthCheck request: %v ", err)
return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode)
}
} else {
if healthResp.StatusCode >= 400 {
logger.Debug("Error performing HealthCheck request: %v ", err)
return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode)
}
}
return nil
}

67
internal/prometheus.go Normal file
View File

@@ -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)
})
}

View File

@@ -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) {

View File

@@ -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")
@@ -199,7 +214,21 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
disableXForward: route.DisableHeaderXForward,
cors: route.Cors,
}
// create route
router := r.PathPrefix(route.Path).Subrouter()
// Apply common exploits to the route
// Enable common exploits
if route.BlockCommonExploits {
logger.Info("Block common exploits enabled")
router.Use(middleware.BlockExploitsMiddleware)
}
// Apply route rate limit
if route.RateLimit > 0 {
//rateLimiter := middleware.NewRateLimiter(gateway.RateLimit, time.Minute)
limiter := middleware.NewRateLimiterWindow(route.RateLimit, time.Minute, route.Cors.Origins) // requests per minute
// Add rate limit middleware to all routes, if defined
router.Use(limiter.RateLimitMiddleware())
}
// Apply route Cors
router.Use(CORSHandler(route.Cors))
if len(route.Hosts) > 0 {
@@ -209,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)

View File

@@ -143,27 +143,29 @@ type Route struct {
//
// E.g. /cart to / => It will rewrite /cart path to /
Rewrite string `yaml:"rewrite"`
// Destination Defines backend URL
Destination string `yaml:"destination"`
//
Backends []string `yaml:"backends"`
// Cors contains the route cors headers
Cors Cors `yaml:"cors"`
//RateLimit int `yaml:"rateLimit"`
// Methods allowed method
Methods []string `yaml:"methods"`
// HealthCheck Defines the backend is health
HealthCheck RouteHealthCheck `yaml:"healthCheck"`
// Destination Defines backend URL
Destination string `yaml:"destination"`
Backends []string `yaml:"backends"`
// Cors contains the route cors headers
Cors Cors `yaml:"cors"`
RateLimit int `yaml:"rateLimit"`
// DisableHeaderXForward Disable X-forwarded header.
//
// [X-Forwarded-Host, X-Forwarded-For, Host, Scheme ]
//
// It will not match the backend route
DisableHeaderXForward bool `yaml:"disableHeaderXForward"`
// HealthCheck Defines the backend is health check PATH
HealthCheck string `yaml:"healthCheck"`
// InterceptErrors intercepts backend errors based on the status codes
//
// Eg: [ 403, 405, 500 ]
InterceptErrors []int `yaml:"interceptErrors"`
// BlockCommonExploits enable, disable block common exploits
BlockCommonExploits bool `yaml:"blockCommonExploits"`
// Middlewares Defines route middleware from Middleware names
Middlewares []string `yaml:"middlewares"`
}
@@ -194,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
@@ -203,11 +206,10 @@ type Gateway struct {
}
type RouteHealthCheck struct {
Path string `yaml:"path"`
Interval int `yaml:"interval"`
Timeout int `yaml:"timeout"`
HealthyStatuses []int `yaml:"healthyStatuses"`
UnhealthyStatuses []int `yaml:"unhealthyStatuses"`
Path string `yaml:"path"`
Interval int `yaml:"interval"`
Timeout int `yaml:"timeout"`
HealthyStatuses []int `yaml:"healthyStatuses"`
}
type GatewayConfig struct {
Version string `yaml:"version"`
@@ -225,6 +227,7 @@ type ErrorResponse struct {
}
type GatewayServer struct {
ctx context.Context
version string
gateway Gateway
middlewares []Middleware
}

View File

@@ -12,6 +12,7 @@ You may get a copy of the License at
import (
"net/url"
"os"
"regexp"
"strconv"
"strings"
)
@@ -115,3 +116,7 @@ func UrlParsePath(uri string) string {
}
return parse.Path
}
func HasWhitespace(s string) bool {
return regexp.MustCompile(`\s`).MatchString(s)
}