// Package server implements the main HTTP server for the chat application. package server import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "git.eeqj.de/sneak/chat/internal/config" "git.eeqj.de/sneak/chat/internal/globals" "git.eeqj.de/sneak/chat/internal/handlers" "git.eeqj.de/sneak/chat/internal/logger" "git.eeqj.de/sneak/chat/internal/middleware" "go.uber.org/fx" "github.com/getsentry/sentry-go" "github.com/go-chi/chi" _ "github.com/joho/godotenv/autoload" // loads .env file ) const ( shutdownTimeout = 5 * time.Second sentryFlushTime = 2 * time.Second ) // Params defines the dependencies for creating a Server. type Params struct { fx.In Logger *logger.Logger Globals *globals.Globals Config *config.Config Middleware *middleware.Middleware Handlers *handlers.Handlers } // Server is the main HTTP server. It manages routing, middleware, and lifecycle. type Server struct { startupTime time.Time exitCode int sentryEnabled bool log *slog.Logger ctx context.Context //nolint:containedctx // signal handling pattern cancelFunc context.CancelFunc httpServer *http.Server router *chi.Mux params Params mw *middleware.Middleware h *handlers.Handlers } // New creates a new Server and registers its lifecycle hooks. func New(lc fx.Lifecycle, params Params) (*Server, error) { s := new(Server) s.params = params s.mw = params.Middleware s.h = params.Handlers s.log = params.Logger.Get() lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { s.startupTime = time.Now() go s.Run() //nolint:contextcheck return nil }, OnStop: func(_ context.Context) error { return nil }, }) return s, nil } // Run starts the server configuration, Sentry, and begins serving. func (s *Server) Run() { s.configure() s.enableSentry() s.serve() } // ServeHTTP delegates to the chi router. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.router.ServeHTTP(w, r) } // MaintenanceMode reports whether the server is in maintenance mode. func (s *Server) MaintenanceMode() bool { return s.params.Config.MaintenanceMode } func (s *Server) enableSentry() { s.sentryEnabled = false if s.params.Config.SentryDSN == "" { return } err := sentry.Init(sentry.ClientOptions{ Dsn: s.params.Config.SentryDSN, Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version), }) if err != nil { s.log.Error("sentry init failure", "error", err) os.Exit(1) } s.log.Info("sentry error reporting activated") s.sentryEnabled = true } func (s *Server) serve() int { s.ctx, s.cancelFunc = context.WithCancel(context.Background()) go func() { c := make(chan os.Signal, 1) signal.Ignore(syscall.SIGPIPE) signal.Notify(c, os.Interrupt, syscall.SIGTERM) sig := <-c s.log.Info("signal received", "signal", sig) if s.cancelFunc != nil { s.cancelFunc() } }() go s.serveUntilShutdown() <-s.ctx.Done() s.cleanShutdown() return s.exitCode } func (s *Server) cleanupForExit() { s.log.Info("cleaning up") } func (s *Server) cleanShutdown() { s.exitCode = 0 ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout) err := s.httpServer.Shutdown(ctxShutdown) if err != nil { s.log.Error("server clean shutdown failed", "error", err) } if shutdownCancel != nil { shutdownCancel() } s.cleanupForExit() if s.sentryEnabled { sentry.Flush(sentryFlushTime) } } func (s *Server) configure() { // server configuration placeholder } func (s *Server) serveUntilShutdown() { listenAddr := fmt.Sprintf(":%d", s.params.Config.Port) s.httpServer = &http.Server{ Addr: listenAddr, ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, MaxHeaderBytes: maxHeaderBytes, Handler: s, } s.SetupRoutes() s.log.Info("http begin listen", "listenaddr", listenAddr) err := s.httpServer.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { s.log.Error("listen error", "error", err) if s.cancelFunc != nil { s.cancelFunc() } } }