package server import ( "context" "fmt" "net/http" "time" "git.dcentral.systems/toolz/goplt/internal/health" "git.dcentral.systems/toolz/goplt/internal/metrics" "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/errorbus" "git.dcentral.systems/toolz/goplt/pkg/logger" "github.com/gin-gonic/gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel/trace" ) // Server wraps the HTTP server and Gin router. type Server struct { httpServer *http.Server router *gin.Engine } // NewServer creates a new HTTP server with all middleware and routes. func NewServer( cfg config.ConfigProvider, log logger.Logger, healthRegistry *health.Registry, metricsRegistry *metrics.Metrics, errorBus errorbus.ErrorPublisher, tracer trace.TracerProvider, ) (*Server, error) { // Set Gin mode env := cfg.GetString("environment") if env == "production" { gin.SetMode(gin.ReleaseMode) } router := gin.New() // Add middleware (order matters!) // OpenTelemetry tracing should be first to capture all requests if tracer != nil { router.Use(otelgin.Middleware("platform", otelgin.WithTracerProvider(tracer))) } router.Use(RequestIDMiddleware()) router.Use(LoggingMiddleware(log)) router.Use(PanicRecoveryMiddleware(errorBus)) router.Use(metricsRegistry.HTTPMiddleware()) router.Use(CORSMiddleware()) // Request timeout middleware (optional, can be configured per route if needed) // router.Use(TimeoutMiddleware(timeout)) // Register core routes registerRoutes(router, healthRegistry, metricsRegistry) // Get server configuration port := cfg.GetInt("server.port") if port == 0 { port = 8080 } host := cfg.GetString("server.host") if host == "" { host = "0.0.0.0" } readTimeout := cfg.GetDuration("server.read_timeout") if readTimeout == 0 { readTimeout = 30 * time.Second } writeTimeout := cfg.GetDuration("server.write_timeout") if writeTimeout == 0 { writeTimeout = 30 * time.Second } addr := fmt.Sprintf("%s:%d", host, port) httpServer := &http.Server{ Addr: addr, Handler: router, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: 120 * time.Second, } return &Server{ httpServer: httpServer, router: router, }, nil } // registerRoutes registers all core routes. func registerRoutes( router *gin.Engine, healthRegistry *health.Registry, metricsRegistry *metrics.Metrics, ) { // Health endpoints router.GET("/healthz", func(c *gin.Context) { status := healthRegistry.LivenessCheck(c.Request.Context()) if status.Status == "healthy" { c.JSON(http.StatusOK, status) } else { c.JSON(http.StatusServiceUnavailable, status) } }) router.GET("/ready", func(c *gin.Context) { status := healthRegistry.ReadinessCheck(c.Request.Context()) if status.Status == "healthy" { c.JSON(http.StatusOK, status) } else { c.JSON(http.StatusServiceUnavailable, status) } }) // Metrics endpoint router.GET("/metrics", gin.WrapH(metricsRegistry.Handler())) } // Start starts the HTTP server. func (s *Server) Start() error { // ListenAndServe will block until the server is closed // If there's an immediate error (like port in use), it will return immediately return s.httpServer.ListenAndServe() } // StartAsync starts the HTTP server in a goroutine and returns a channel that signals when it's ready or errored. func (s *Server) StartAsync() <-chan error { errChan := make(chan error, 1) go func() { if err := s.Start(); err != nil && err != http.ErrServerClosed { errChan <- err } close(errChan) }() return errChan } // Shutdown gracefully shuts down the HTTP server. func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) } // Router returns the Gin router (for adding additional routes). func (s *Server) Router() *gin.Engine { return s.router }