package main import ( "context" "errors" "fmt" "io" "log/slog" "os" "os/signal" "time" "golang.org/x/sync/errgroup" "git.loyso.art/frx/devsim" "git.loyso.art/frx/devsim/internal/api/http" "git.loyso.art/frx/devsim/internal/probe" "git.loyso.art/frx/devsim/internal/store" "git.loyso.art/frx/devsim/internal/store/memory" "git.loyso.art/frx/devsim/internal/store/mongo" "git.loyso.art/frx/devsim/internal/store/pg" "git.loyso.art/frx/devsim/pkg/collections" ) var availableStoreTypes = collections.NewSet([]string{ "pg", "mongo", "memory", }...) func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, })) log.InfoContext( ctx, "running application", slog.Int("pid", os.Getpid()), slog.String("version", devsim.Version()), slog.String("revision", devsim.Revision()), slog.String("buildtime", devsim.BuildTime()), ) settings := loadConfigFromEnv() err := settings.validate() if err != nil { log.ErrorContext(ctx, "unable to validate settings", slog.String("err", err.Error())) os.Exit(1) } log.InfoContext( ctx, "config parsed", slog.Any("settings", settings), ) err = app(ctx, settings, log) if err != nil { log.ErrorContext(ctx, "unable to run app", slog.String("err", err.Error())) os.Exit(1) } } type mongoSettings struct { DSN string } func (s *mongoSettings) fromEnv() { *s = mongoSettings{ DSN: os.Getenv("DEVSIM_MONGO_DSN"), } } type pgSettings struct { DSN string } func (s *pgSettings) fromEnv() { *s = pgSettings{ DSN: os.Getenv("DEVSIM_PG_DSN"), } } type applicationSettings struct { listenAddr string monitorAddr string storeType string pg pgSettings mongo mongoSettings } func loadConfigFromEnv() applicationSettings { const webaddr = ":9123" const monitoraddr = ":9124" var cfg applicationSettings cfg.listenAddr = valueOrDefault(os.Getenv("DEVSIM_HTTP_ADDR"), webaddr) cfg.monitorAddr = valueOrDefault(os.Getenv("DEVSIM_MONITOR_ADDR"), monitoraddr) cfg.storeType = os.Getenv("DEVSIM_STORE_TYPE") cfg.pg.fromEnv() cfg.mongo.fromEnv() return cfg } func (s applicationSettings) validate() (err error) { if !availableStoreTypes.Contains(s.storeType) { err = errors.Join(err, errors.New("store_type value is unsupported")) } switch s.storeType { case "pg": if s.pg.DSN == "" { err = errors.Join(err, errors.New("no postgres dsn provided")) } case "mongo": if s.mongo.DSN == "" { err = errors.Join(err, errors.New("no mongo dsn provided")) } case "memory": // no things to validate } if s.listenAddr == "" { err = errors.Join(err, errors.New("no listen address provided")) } return err } type namedCloser struct { closer io.Closer name string } func app(ctx context.Context, settings applicationSettings, log *slog.Logger) (err error) { var repo store.Stats var closers []namedCloser livenessBase, livenessToggle := probe.SimpleLivenessSwitcher() readinessBase, readinessToggle := probe.SimpleReadinessSwitcher() pr := probe.NewReporter(time.Second * 15) pr.RegisterLiveness(livenessBase) pr.RegisterReadiness(readinessBase) mb := http.NewHandlersBuilder() mb.MountProbeHandlers(pr) monitorServer := http.NewServer(settings.monitorAddr) closers = append(closers, namedCloser{ name: "monitorhttp", closer: monitorServer, }) monitorServer.RegisterHandler(mb.Build()) eg, _ := errgroup.WithContext(ctx) eg.Go(func() error { return monitorServer.Run() }) switch settings.storeType { case "pg": pgconn, errDial := pg.Dial(ctx, settings.pg.DSN) if errDial != nil { return fmt.Errorf("connecting to postgres: %w", errDial) } repo = pgconn.StatsRepository() closers = append(closers, namedCloser{ name: "postgres", closer: pgconn, }) case "mongo": mongoconn, errDial := mongo.Dial(ctx, settings.mongo.DSN) if errDial != nil { return fmt.Errorf("connecting to mongo: %w", errDial) } repo = mongoconn.StatsRepository() closers = append(closers, namedCloser{ name: "mongo", closer: mongoconn, }) case "memory": repo = memory.NewStore() } hb := http.NewHandlersBuilder() hb.MountStatsHandlers(repo, log) httpServer := http.NewServer(settings.listenAddr) closers = append(closers, namedCloser{ name: "http", closer: httpServer, }) httpServer.RegisterHandler(hb.Build()) eg.Go(func() error { return httpServer.Run() }) eg.Go(func() error { <-ctx.Done() log.InfoContext(ctx, "got interruption signal") for _, closer := range closers { name := closer.name closerUnit := closer.closer errClose := closerUnit.Close() if errClose != nil { log.ErrorContext(ctx, "unable to close component", slog.String("component", name), slog.Any("err", errClose)) } } return nil }) livenessToggle() readinessToggle(probe.ReadinessOk) err = eg.Wait() if err != nil { if !errors.Is(err, context.Canceled) { log.ErrorContext(ctx, "unable to proceed the app", slog.Any("err", err)) } else { log.InfoContext(ctx, "finished processing apps") } } return nil } func valueOrDefault[x comparable](value, fallback x) x { var v x if value == v { return fallback } return value }