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  }