package internal import ( "errors" "fmt" "log/slog" "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() otelEnabled := types.IsOtelEnabled(env) if 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 { slog.ErrorContext(ctx, "error shutting down OpenTelemetry SDK", "err", err) } cancel() }() slog.SetDefault(log.NewLogPropagator()) } slog.InfoContext(ctx, "Starting server...") // init server settings serverSettings, err := types.NewSettingsFromEnv(ctx, env) if err != nil { return err } // init db err = db.RunMigrations(ctx, database, migrationsPrefix) if err != nil { return fmt.Errorf("could not run migrations: %w", err) } // init server httpServer := &http.Server{ Addr: ":" + serverSettings.Port, Handler: createHandlerWithServices(ctx, database, serverSettings), ReadHeaderTimeout: 2 * time.Second, } go startServer(ctx, httpServer) // graceful shutdown var wg sync.WaitGroup wg.Add(1) go shutdownServer(ctx, httpServer, &wg) wg.Wait() return nil } func startServer(ctx context.Context, s *http.Server) { slog.InfoContext(ctx, "Starting server", "addr", s.Addr) if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.ErrorContext(ctx, "error listening and serving", "err", err) } } func shutdownServer(ctx context.Context, s *http.Server, 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 { slog.ErrorContext(ctx, "error shutting down http server", "err", err) } else { slog.InfoContext(ctx, "Gracefully stopped http server", "addr", s.Addr) } } func createHandlerWithServices(ctx context.Context, 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) dashboardService := service.NewDashboard(d) render := handler.NewRender() indexHandler := handler.NewIndex(render, clockService) dashboardHandler := handler.NewDashboard(render, dashboardService) 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) go dailyTaskTimer(ctx, transactionRecurringService, authService) indexHandler.Handle(router) dashboardHandler.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.SecurityHeaders(serverSettings), middleware.CacheControl, middleware.CrossSiteRequestForgery(authService), middleware.Authenticate(authService), middleware.Gzip, middleware.Log, ) wrapper = otelhttp.NewHandler(wrapper, "http.request") return wrapper } func dailyTaskTimer(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) { runDailyTasks(ctx, transactionRecurring, auth) ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: runDailyTasks(ctx, transactionRecurring, auth) } } } func runDailyTasks(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) { slog.InfoContext(ctx, "Running daily tasks") _ = transactionRecurring.GenerateTransactions(ctx) _ = auth.CleanupSessionsAndTokens(ctx) }