//go:generate swag init -g ../../cmd/server/main.go package server import ( "context" "embed" "net" "net/http" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog/log" httpSwagger "github.com/swaggo/http-swagger" "DanceLessonsCoach/pkg/config" "DanceLessonsCoach/pkg/greet" "DanceLessonsCoach/pkg/telemetry" "DanceLessonsCoach/pkg/validation" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) //go:embed docs/swagger.json var swaggerJSON embed.FS type Server struct { router *chi.Mux readyCtx context.Context withOTEL bool config *config.Config tracerProvider *sdktrace.TracerProvider validator *validation.Validator } func NewServer(cfg *config.Config, readyCtx context.Context) *Server { // Create validator instance validator, err := validation.GetValidatorFromConfig(cfg) if err != nil { log.Error().Err(err).Msg("Failed to create validator, continuing without validation") } else { log.Trace().Msg("Validator created successfully") } s := &Server{ router: chi.NewRouter(), readyCtx: readyCtx, withOTEL: cfg.GetTelemetryEnabled(), config: cfg, validator: validator, } s.setupRoutes() return s } func (s *Server) setupRoutes() { // Use Zerolog middleware instead of Chi's default logger s.router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{ Logger: &log.Logger, NoColor: false, })) // Health endpoint at root level s.router.Get("/api/health", s.handleHealth) // Readiness endpoint at root level s.router.Get("/api/ready", s.handleReadiness) // API routes s.router.Route("/api/v1", func(r chi.Router) { r.Use(s.getAllMiddlewares()...) s.registerApiV1Routes(r) }) // Register v2 routes if enabled if s.config.GetV2Enabled() { s.router.Route("/api/v2", func(r chi.Router) { r.Use(s.getAllMiddlewares()...) s.registerApiV2Routes(r) }) } // Add Swagger UI with embedded spec // Serve the embedded swagger.json file s.router.Handle("/swagger/doc.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, err := swaggerJSON.ReadFile("docs/swagger.json") if err != nil { log.Error().Err(err).Msg("Failed to read embedded swagger.json") http.Error(w, "Failed to read swagger.json", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(data) })) // Setup Swagger UI handler s.router.Get("/swagger/*", httpSwagger.WrapHandler) } func (s *Server) registerApiV1Routes(r chi.Router) { greetService := greet.NewService() greetHandler := greet.NewApiV1GreetHandler(greetService) r.Route("/greet", func(r chi.Router) { greetHandler.RegisterRoutes(r) }) } func (s *Server) registerApiV2Routes(r chi.Router) { greetServiceV2 := greet.NewServiceV2() greetHandlerV2 := greet.NewApiV2GreetHandler(greetServiceV2, s.validator) r.Route("/greet", func(r chi.Router) { greetHandlerV2.RegisterRoutes(r) }) } // getAllMiddlewares returns all middleware including OpenTelemetry if enabled func (s *Server) getAllMiddlewares() []func(http.Handler) http.Handler { middlewares := []func(http.Handler) http.Handler{ middleware.StripSlashes, middleware.Recoverer, } if s.withOTEL { middlewares = append(middlewares, func(next http.Handler) http.Handler { return otelhttp.NewHandler(next, "") }) } return middlewares } // handleHealth godoc // @Summary Health check // @Description Check if the service is healthy // @Tags health // @Accept json // @Produce json // @Success 200 {object} map[string]string "Service is healthy" // @Router /health [get] func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { log.Trace().Msg("Health check requested") w.Write([]byte(`{"status":"healthy"}`)) } // handleReadiness godoc // @Summary Readiness check // @Description Check if the service is ready to accept traffic // @Tags health // @Accept json // @Produce json // @Success 200 {object} map[string]bool "Service is ready" // @Failure 503 {object} map[string]bool "Service is not ready" // @Router /ready [get] func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { log.Trace().Msg("Readiness check requested") select { case <-s.readyCtx.Done(): log.Trace().Msg("Readiness check: not ready (shutting down)") w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte(`{"ready":false}`)) default: log.Trace().Msg("Readiness check: ready") w.Write([]byte(`{"ready":true}`)) } } func (s *Server) Router() http.Handler { return s.router } // Run starts the HTTP server and handles graceful shutdown func (s *Server) Run() error { // Initialize OpenTelemetry if enabled var err error if s.withOTEL { log.Trace().Msg("Initializing OpenTelemetry tracing") telemetrySetup := &telemetry.Setup{ ServiceName: s.config.GetServiceName(), OTLPEndpoint: s.config.GetOTLPEndpoint(), Insecure: s.config.GetTelemetryInsecure(), SamplerType: s.config.GetSamplerType(), SamplerRatio: s.config.GetSamplerRatio(), } if s.tracerProvider, err = telemetrySetup.InitializeTracing(context.Background()); err != nil { log.Error().Err(err).Msg("Failed to initialize OpenTelemetry, continuing without tracing") s.withOTEL = false } else { log.Trace().Msg("OpenTelemetry tracing initialized successfully") } } // Setup signal context for graceful shutdown rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // Create ongoing context for active requests ongoingCtx, stopOngoingGracefully := context.WithCancel(context.Background()) defer stopOngoingGracefully() // Create HTTP server log.Trace().Str("address", s.config.GetServerAddress()).Msg("Server running") srv := &http.Server{ Addr: s.config.GetServerAddress(), Handler: s.router, BaseContext: func(_ net.Listener) context.Context { return ongoingCtx }, } // Start the HTTP server in a separate goroutine serverErrors := make(chan error, 1) go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { serverErrors <- err } close(serverErrors) }() // Wait for signal <-rootCtx.Done() stop() log.Trace().Msg("Shutdown signal received") // Cancel readiness context to stop accepting new requests if cancelReady, ok := s.readyCtx.(interface{ Cancel() }); ok { cancelReady.Cancel() } log.Trace().Msg("Readiness set to false, no longer accepting new requests") // Give time for readiness check to propagate time.Sleep(1 * time.Second) log.Trace().Msg("Readiness check propagated, now waiting for ongoing requests to finish.") // Create shutdown context with timeout from config shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), s.config.Shutdown.Timeout) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Error().Err(err).Msg("Server shutdown failed") return err } log.Trace().Msg("Server shutdown complete") // Shutdown OpenTelemetry tracer provider if s.tracerProvider != nil { if err := telemetry.Shutdown(context.Background(), s.tracerProvider); err != nil { log.Error().Err(err).Msg("Failed to shutdown OpenTelemetry tracer provider") } else { log.Trace().Msg("OpenTelemetry tracer provider shutdown complete") } } // Return any server errors if err, ok := <-serverErrors; ok { return err } return nil }