package internal import ( "errors" "fmt" "spend-sparrow/internal/db" "spend-sparrow/internal/handler" "spend-sparrow/internal/handler/middleware" "spend-sparrow/internal/log" "spend-sparrow/internal/service" "spend-sparrow/internal/types" "context" "net/http" "os/signal" "sync" "syscall" "time" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env func(string) string) error { ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() log.L.Info("Starting server...") // init server settings serverSettings, err := types.NewSettingsFromEnv(env) if err != nil { return err } // init db err = db.RunMigrations(database, migrationsPrefix) if err != nil { return fmt.Errorf("could not run migrations: %w", err) } if serverSettings.OtelEnabled { // use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled otelShutdown, err := setupOTelSDK(context.Background()) if err != nil { return fmt.Errorf("could not setup OpenTelemetry SDK: %w", err) } defer func() { // User context.Background(), as the main context is already cancelled ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) err = otelShutdown(ctx) if err != nil { log.L.Error("error shutting down OpenTelemetry SDK", "err", err) } cancel() }() log.InitOtelLogger() } // init server httpServer := &http.Server{ Addr: ":" + serverSettings.Port, Handler: createHandler(database, serverSettings), ReadHeaderTimeout: 2 * time.Second, } go startServer(httpServer) // graceful shutdown var wg sync.WaitGroup wg.Add(1) go shutdownServer(httpServer, ctx, &wg) wg.Wait() return nil } func startServer(s *http.Server) { log.L.Info("Starting server", "addr", s.Addr) if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.L.Error("error listening and serving", "err", err) } } func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() if s == nil { return } <-ctx.Done() shutdownCtx := context.Background() shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) defer cancel() if err := s.Shutdown(shutdownCtx); err != nil { log.L.Error("error shutting down http server", "err", err) } else { log.L.Info("Gracefully stopped http server", "addr", s.Addr) } } func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { var router = http.NewServeMux() authDb := db.NewAuthSqlite(d) randomService := service.NewRandom() clockService := service.NewClock() mailService := service.NewMail(serverSettings) authService := service.NewAuth(authDb, randomService, clockService, mailService, serverSettings) accountService := service.NewAccount(d, randomService, clockService) treasureChestService := service.NewTreasureChest(d, randomService, clockService) transactionService := service.NewTransaction(d, randomService, clockService) transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService) render := handler.NewRender() indexHandler := handler.NewIndex(render) authHandler := handler.NewAuth(authService, render) accountHandler := handler.NewAccount(accountService, render) treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render) transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render) transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render) indexHandler.Handle(router) accountHandler.Handle(router) treasureChestHandler.Handle(router) authHandler.Handle(router) transactionHandler.Handle(router) transactionRecurringHandler.Handle(router) // Serve static files (CSS, JS and images) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) wrapper := middleware.Wrapper( router, middleware.GenerateRecurringTransactions(transactionRecurringService), middleware.SecurityHeaders(serverSettings), middleware.CacheControl, middleware.CrossSiteRequestForgery(authService), middleware.Authenticate(authService), middleware.Gzip, middleware.Log, ) wrapper = otelhttp.NewHandler(wrapper, "http.request") return wrapper }