package server import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "git.eeqj.de/sneak/gohttpserver/internal/config" "git.eeqj.de/sneak/gohttpserver/internal/globals" "git.eeqj.de/sneak/gohttpserver/internal/handlers" "git.eeqj.de/sneak/gohttpserver/internal/logger" "git.eeqj.de/sneak/gohttpserver/internal/middleware" "github.com/rs/zerolog" "go.uber.org/fx" "github.com/getsentry/sentry-go" "github.com/go-chi/chi" // spooky action at a distance! // this populates the environment // from a ./.env file automatically // for development configuration. // .env contents should be things like // `DBURL=postgres://user:pass@.../` // (without the backticks, of course) _ "github.com/joho/godotenv/autoload" ) type ServerParams struct { fx.In Logger *logger.Logger Globals *globals.Globals Config *config.Config Middleware *middleware.Middleware Handlers *handlers.Handlers } type Server struct { startupTime time.Time exitCode int sentryEnabled bool log *zerolog.Logger ctx context.Context cancelFunc context.CancelFunc httpServer *http.Server router *chi.Mux params ServerParams mw *middleware.Middleware h *handlers.Handlers } func New(lc fx.Lifecycle, params ServerParams) (*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(ctx context.Context) error { s.startupTime = time.Now() go s.Run() // background FIXME return nil }, OnStop: func(ctx context.Context) error { // FIXME do server shutdown here return nil }, }) return s, nil } // FIXME change this to use uber/fx DI and an Invoke() // this is where we come in from package main. func (s *Server) Run() { // this does nothing if SENTRY_DSN is unset in env. // TODO remove: if s.sentryEnabled { sentry.CaptureMessage("It works!") } s.configure() // logging before sentry, because sentry logs s.enableSentry() s.serve() // FIXME deal with return value } 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.Fatal().Err(err).Msg("sentry init failure") return } s.log.Info().Msg("sentry error reporting activated") s.sentryEnabled = true } func (s *Server) serve() int { // FIXME fx will handle this for us s.ctx, s.cancelFunc = context.WithCancel(context.Background()) // signal watcher go func() { c := make(chan os.Signal, 1) signal.Ignore(syscall.SIGPIPE) signal.Notify(c, os.Interrupt, syscall.SIGTERM) // block and wait for signal sig := <-c s.log.Info().Msgf("signal received: %+v", sig) if s.cancelFunc != nil { // cancelling the main context will trigger a clean // shutdown. s.cancelFunc() } }() go s.serveUntilShutdown() for range s.ctx.Done() { // aforementioned clean shutdown upon main context // cancellation } s.cleanShutdown() return s.exitCode } func (s *Server) cleanupForExit() { s.log.Info().Msg("cleaning up") // FIXME unimplemented // close database connections or whatever } func (s *Server) cleanShutdown() { // initiate clean shutdown s.exitCode = 0 ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) if err := s.httpServer.Shutdown(ctxShutdown); err != nil { s.log.Error(). Err(err). Msg("server clean shutdown failed") } if shutdownCancel != nil { shutdownCancel() } s.cleanupForExit() if s.sentryEnabled { sentry.Flush(2 * time.Second) } } func (s *Server) MaintenanceMode() bool { return s.params.Config.MaintenanceMode } func (s *Server) configure() { // FIXME move most of this to dedicated places // if viper.GetBool("DEBUG") { // pp.Print(viper.AllSettings()) // } }