diff --git a/.gitignore b/.gitignore index c371916..4344009 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.exe *.test *.out +server +greet # Dependency directories vendor/ diff --git a/cmd/server/main.go b/cmd/server/main.go index e4681e8..d0462d2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,19 +2,12 @@ package main import ( "context" - "net" - "net/http" "os" - "os/signal" - "syscall" - "time" "DanceLessonsCoach/pkg/config" "DanceLessonsCoach/pkg/server" - "DanceLessonsCoach/pkg/telemetry" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func main() { @@ -52,107 +45,13 @@ func main() { log.Info().Bool("json_logging", cfg.Logging.JSON).Msg("Logging configured") - // Initialize OpenTelemetry if enabled - var tracerProvider *sdktrace.TracerProvider - if cfg.GetTelemetryEnabled() { - log.Info().Msg("Initializing OpenTelemetry tracing") - - telemetrySetup := &telemetry.Setup{ - ServiceName: cfg.GetServiceName(), - OTLPEndpoint: cfg.GetOTLPEndpoint(), - Insecure: cfg.GetTelemetryInsecure(), - SamplerType: cfg.GetSamplerType(), - SamplerRatio: cfg.GetSamplerRatio(), - } - - var err error - if tracerProvider, err = telemetrySetup.InitializeTracing(context.Background()); err != nil { - log.Error().Err(err).Msg("Failed to initialize OpenTelemetry, continuing without tracing") - } else { - log.Info().Msg("OpenTelemetry tracing initialized successfully") - } - } - - // Setup signal context for graceful shutdown - rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - - // Create root context with cancellation - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create ongoing context for active requests - ongoingCtx, stopOngoingGracefully := context.WithCancel(context.Background()) - // Create readiness context to control readiness state readyCtx, readyCancel := context.WithCancel(context.Background()) defer readyCancel() - // Start server in goroutine + // Create and run server server := server.NewServer(cfg, readyCtx) - serverCtx, serverStop := context.WithCancel(ctx) - - go func() { - log.Info().Str("address", cfg.GetServerAddress()).Msg("Server running") - - srv := &http.Server{ - Addr: cfg.GetServerAddress(), - Handler: server.Router(), - BaseContext: func(_ net.Listener) context.Context { - return ongoingCtx - }, - } - - // Start the HTTP server in a separate goroutine - go func() { - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Error().Err(err).Msg("Server error") - } - }() - - // Wait for signal - <-rootCtx.Done() - stop() - log.Info().Msg("Shutdown signal received") - - // Cancel readiness context to stop accepting new requests - readyCancel() - log.Info().Msg("Readiness set to false, no longer accepting new requests") - - // Give time for readiness check to propagate (simplified for our case) - time.Sleep(1 * time.Second) - log.Info().Msg("Readiness check propagated, now waiting for ongoing requests to finish.") - - // Create shutdown context with timeout from config - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.Shutdown.Timeout) - defer shutdownCancel() - - if err := srv.Shutdown(shutdownCtx); err != nil { - log.Error().Err(err).Msg("Server shutdown failed") - } else { - log.Info().Msg("Server shutdown complete") - } - - // Stop ongoing requests context - stopOngoingGracefully() - cancel() - serverStop() - log.Info().Msg("Server exited") - - // Shutdown OpenTelemetry tracer provider - if tracerProvider != nil { - if err := telemetry.Shutdown(context.Background(), tracerProvider); err != nil { - log.Error().Err(err).Msg("Failed to shutdown OpenTelemetry tracer provider") - } else { - log.Info().Msg("OpenTelemetry tracer provider shutdown complete") - } - } - - // Force log flush by writing to stderr directly - // This ensures logs are written before process exits - time.Sleep(100 * time.Millisecond) - }() - - // Wait for shutdown - <-serverCtx.Done() + if err := server.Run(); err != nil { + log.Fatal().Err(err).Msg("Server failed") + } } diff --git a/pkg/server/server.go b/pkg/server/server.go index a793bcf..75861fd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,11 +2,17 @@ package server import ( "context" + "net" "net/http" + "os/signal" + "syscall" + "time" "DanceLessonsCoach/pkg/config" "DanceLessonsCoach/pkg/greet" + "DanceLessonsCoach/pkg/telemetry" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -14,9 +20,11 @@ import ( ) type Server struct { - router *chi.Mux - readyCtx context.Context - withOTEL bool + router *chi.Mux + readyCtx context.Context + withOTEL bool + config *config.Config + tracerProvider *sdktrace.TracerProvider } func NewServer(cfg *config.Config, readyCtx context.Context) *Server { @@ -24,6 +32,7 @@ func NewServer(cfg *config.Config, readyCtx context.Context) *Server { router: chi.NewRouter(), readyCtx: readyCtx, withOTEL: cfg.GetTelemetryEnabled(), + config: cfg, } s.setupRoutes() return s @@ -95,3 +104,99 @@ func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { 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.Info().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.Info().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.Info().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.Info().Msg("Shutdown signal received") + + // Cancel readiness context to stop accepting new requests + if cancelReady, ok := s.readyCtx.(interface{ Cancel() }); ok { + cancelReady.Cancel() + } + log.Info().Msg("Readiness set to false, no longer accepting new requests") + + // Give time for readiness check to propagate + time.Sleep(1 * time.Second) + log.Info().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.Info().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.Info().Msg("OpenTelemetry tracer provider shutdown complete") + } + } + + // Force log flush + time.Sleep(100 * time.Millisecond) + + // Return any server errors + if err, ok := <-serverErrors; ok { + return err + } + + return nil +} diff --git a/server b/server deleted file mode 100755 index 6aec654..0000000 Binary files a/server and /dev/null differ