github.com/safedep/dry@v0.0.0-20241016050132-a15651f0548b/adapters/http/router_echo.go (about) 1 package http 2 3 import ( 4 "fmt" 5 "net/http" 6 "regexp" 7 8 "github.com/labstack/echo-contrib/echoprometheus" 9 echo "github.com/labstack/echo/v4" 10 "github.com/labstack/echo/v4/middleware" 11 "github.com/labstack/gommon/log" 12 drylog "github.com/safedep/dry/log" 13 "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" 14 ) 15 16 type EchoRouterConfig struct { 17 ServiceName string 18 SkipHealthEndpoint bool 19 SkipMetricsEndpoint bool 20 21 // Used to configure prometheus middleware 22 MetricsNamespace string 23 MetricsSubsystem string 24 } 25 26 type EchoRouter struct { 27 config EchoRouterConfig 28 router *echo.Echo 29 } 30 31 func NewEchoRouter(config EchoRouterConfig) (Router, error) { 32 router := echo.New() 33 router.Logger.SetLevel(log.INFO) 34 35 router.Use(middleware.Logger()) 36 router.Use(middleware.Recover()) 37 router.Use(middleware.RequestID()) 38 router.Use(otelecho.Middleware(config.ServiceName)) 39 40 // Copy the request id from response header to the request 41 // header so that downstream services can use it. This should 42 // come after echo's requestID middleware. 43 router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 44 return func(c echo.Context) error { 45 requestID := c.Request().Header.Get(echo.HeaderXRequestID) 46 if requestID == "" { 47 requestID = c.Response().Header().Get(echo.HeaderXRequestID) 48 c.Request().Header.Set(echo.HeaderXRequestID, requestID) 49 } 50 51 return next(c) 52 } 53 }) 54 55 if !config.SkipHealthEndpoint { 56 router.GET(HealthPath, func(c echo.Context) error { 57 return c.String(200, "OK") 58 }) 59 } 60 61 if !config.SkipMetricsEndpoint { 62 // https://prometheus.io/docs/concepts/data_model/ 63 metricsNameRegex := regexp.MustCompile("^[a-zA-Z_:][a-zA-Z0-9_:]*$") 64 if config.MetricsSubsystem != "" && !metricsNameRegex.MatchString(config.MetricsSubsystem) { 65 return nil, 66 fmt.Errorf("subsystem name %s is invalid. Must match regex %s", config.MetricsSubsystem, 67 metricsNameRegex.String()) 68 } 69 70 if config.MetricsNamespace != "" && !metricsNameRegex.MatchString(config.MetricsNamespace) { 71 return nil, 72 fmt.Errorf("namespace name %s is invalid. Must match regex %s", config.MetricsNamespace, 73 metricsNameRegex.String()) 74 } 75 76 router.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ 77 Subsystem: config.MetricsSubsystem, 78 Namespace: config.MetricsNamespace, 79 Skipper: func(c echo.Context) bool { 80 if c.Path() == MetricsPath { 81 return true 82 } 83 84 if c.Path() == HealthPath { 85 return true 86 } 87 88 return false 89 }, 90 })) 91 92 router.GET(MetricsPath, echoprometheus.NewHandler()) 93 } 94 95 // This must be to the end of the middleware chain 96 router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 97 return func(c echo.Context) error { 98 logger := drylog.With(map[string]interface{}{"request_id": c.Response().Header().Get(echo.HeaderXRequestID)}) 99 c.Set("dry_logger", logger) 100 101 // TODO: Figure out a way to pass the logger trasparently 102 // to the business logic layer. We can also switch to a context logger 103 // which flushes the log at the end of the request. 104 return next(c) 105 } 106 }) 107 108 return &EchoRouter{ 109 config: config, 110 router: router, 111 }, nil 112 } 113 114 func (r *EchoRouter) AddRoute(method RouteMethod, path string, handler http.Handler) { 115 switch method { 116 case GET: 117 r.router.GET(path, echo.WrapHandler(handler)) 118 case POST: 119 r.router.POST(path, echo.WrapHandler(handler)) 120 case PUT: 121 r.router.PUT(path, echo.WrapHandler(handler)) 122 case DELETE: 123 r.router.DELETE(path, echo.WrapHandler(handler)) 124 case OPTIONS: 125 r.router.OPTIONS(path, echo.WrapHandler(handler)) 126 case ANY: 127 r.router.Any(path, echo.WrapHandler(handler)) 128 } 129 } 130 131 func (r *EchoRouter) Handler() http.Handler { 132 return r.router 133 } 134 135 func (r *EchoRouter) ListenAndServe(address string) error { 136 return r.router.Start(address) 137 }