package server import ( "context" "errors" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "go.uber.org/fx" "gorm.io/gorm" "base/config" "base/internal/delivery/http/platform" "base/internal/dto" "base/internal/server/middleware" "base/pkg/health" ) type Params struct { fx.In Engine *gin.Engine Config *config.AppConfig Logger zerolog.Logger Public *platform.Controller DB *gorm.DB } // StartHTTPServer starts the HTTP server func StartHTTPServer(lifecycle fx.Lifecycle, params Params) { server := &http.Server{ Addr: fmt.Sprintf("%s:%s", params.Config.Server.WebHost, params.Config.Server.WebPort), Handler: params.Engine, } lifecycle.Append( fx.Hook{ OnStart: func(ctx context.Context) error { params.Logger.Info().Str("module", "http").Msg("Starting HTTP server") go func() { if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { params.Logger.Error().Err(err).Msg("HTTP server failed to start") } }() return nil }, OnStop: func(ctx context.Context) error { params.Logger.Info().Str("module", "http").Msg("Stopping HTTP server") return server.Shutdown(ctx) }, }) } // NewGinEngine creates a new Gin HTTP engine func NewGinEngine(logger zerolog.Logger) *gin.Engine { // Set Gin mode gin.SetMode(gin.ReleaseMode) r := gin.New() // Use custom middlewares r.Use(gin.CustomRecovery(CustomRecoveryWithLogger(logger))) // Use logger middleware r.Use( func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path c.Next() end := time.Now() latency := end.Sub(start) if path == "/health" || path == "/health/live" || path == "/metrics" { return } logger.Info(). Str("method", c.Request.Method). Str("path", path). Str("ip", c.ClientIP()). Int("status", c.Writer.Status()). Dur("latency", latency). Msg("request completed") }) return r } func registerRoutes(engine *gin.Engine, mid middleware.Middleware) { // Prometheus metrics endpoint engine.GET("/metrics", gin.WrapH(promhttp.Handler())) engine.Use(mid.Metrics()) engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) } func healthCheckers(db *gorm.DB) []health.Checker { return []health.Checker{ health.DatabaseHealthChecker(db), } } func registerHealthRoute(engine *gin.Engine, params Params) { engine.GET("/health", func(c *gin.Context) { checkers := healthCheckers(params.DB) response := health.Health(c.Request.Context(), "1.0.0", checkers...) statusCode := http.StatusOK if response.Status == health.StatusUnhealthy { statusCode = http.StatusServiceUnavailable } else if response.Status == health.StatusDegraded { statusCode = http.StatusOK // Degraded is still considered OK for HTTP } c.JSON(statusCode, response) }) // Simple health check endpoint for load balancers engine.GET("/health/ready", func(c *gin.Context) { checkers := healthCheckers(params.DB) response := health.Health(c.Request.Context(), "1.0.0", checkers...) // For readiness, we only care if the service is healthy or degraded // Unhealthy means the service is not ready to serve traffic if response.Status == health.StatusUnhealthy { c.JSON(http.StatusServiceUnavailable, gin.H{ "status": "not ready", "timestamp": time.Now(), "message": "Service is not ready to serve traffic", }) return } c.JSON(http.StatusOK, gin.H{ "status": "ready", "timestamp": time.Now(), "message": "Service is ready to serve traffic", }) }) // Liveness check endpoint engine.GET("/health/live", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "alive", "timestamp": time.Now(), "message": "Service is alive", }) }) } var Server = fx.Module( "server", fx.Provide(NewGinEngine), fx.Invoke(StartHTTPServer, registerRoutes, registerHealthRoute), ) func CustomRecoveryWithLogger(logger zerolog.Logger) gin.RecoveryFunc { return func(c *gin.Context, err interface{}) { logger.Error(). Interface("error", err). Str("path", c.Request.URL.Path). Str("method", c.Request.Method). Msg("panic recovered") c.JSON(http.StatusInternalServerError, dto.InternalServerError()) c.Abort() } }